Skip to content

A failed-review verdict is permanent, but the branch it judged is not - #575

Draft
agent-relay-code[bot] wants to merge 4 commits into
mainfrom
relayflow/flows-software-garden-0e37dfb3
Draft

agent-relay-code[bot] wants to merge 4 commits into
mainfrom
relayflow/flows-software-garden-0e37dfb3

Conversation

@agent-relay-code

@agent-relay-code agent-relay-code Bot commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

Bind draft review verdicts to the commit they judged

Draft PRs from the canonical Software Factory flow now name their reviewed
40-hex head, include a single machine-readable review marker, and explain that
a new head supersedes the verdict. Missing verification prerequisites use
NOT VERIFIED, name the prerequisite, and end with needs_human (exit 3) after
opening a draft. Defects, absent or contradictory verdicts, and empty unverified
markers remain BLOCKED / step_failed (exit 1). Completion details identify the
head and publication state. Hook-blocked drafts use the same scope contract.

Publication rejects malformed heads and duplicate/forged scope markers before
push, including GitHub inputs. Passed PR body formatting is preserved.
Regression coverage exercises these cases using the real /bin/sh body-generation
and validation commands; the harness now throws for unhandled commands.

The flow header is bumped to 2.0.23, and the hosted loader's reviewed-base pins
move with it (next section). No workflow files were changed.

The reviewed-base pins move with the flow

packages/sdk/src/hosted-extension-runtime.ts pins the exact Software Factory
flow bytes the hosted capability sandbox will accept, plus the identity it
assigns to that base. Both are functions of
examples/software-factory/software-factory.flow.ts:

$ git show HEAD~1:examples/software-factory/software-factory.flow.ts | sha256sum
49c993220b9c34fab2d4b0e51911656f62b8b657f534d988691960d45bb9d9b6  -

$ sha256sum examples/software-factory/software-factory.flow.ts
ee56899fcb5c0a968d845620db3d4229673a3b732dd4d6131ab43b81822bf97b  examples/software-factory/software-factory.flow.ts

Changing the flow and leaving the pins behind makes the loader reject the base it
ships with, which is exactly what it should do — and it took the whole
babysitter-native-extension suite down with it. SOFTWARE_FACTORY_SHA256 and
the assigned version are therefore updated to the new bytes and the new 2.0.23
header, and docs/BABYSITTER-CATALOG-HANDOFF.md now states that the two move
together. The check itself is unchanged: any source other than the reviewed one
is still refused, as the untouched rejection regressions in
tests/hosted-base-snapshot.test.ts still prove.

Scope and limitations

This implements reviewed-plan.md's record-the-head portion and three-verdict
protocol in the tracked canonical flow. It does not amend stale PR comments,
clear drafts, rerun reviews on push, or backfill the seven PRs. The future
resident shepherd (examples/babysitter, not yet ready for unattended deployment)
can consume the new marker. The running Garden flow is not tracked here:
the equivalent change must be transplanted into its reviewBlockedCommand and
review.clean check. This does not claim the running Garden is fixed.

22 tests in three files still fail on this machine, all of them the bubblewrap
sandbox failing to start, which no code change here can clear; the numbers and
the evidence are below and in .relayflow/repair-notes.md. One live-kernel
case that drives the real Claude analyzer is skipped
(RELAYFLOWS_ALLOW_ANALYZER_SKIP=1, as CI sets it), so this is not gate-2
acceptance evidence.

Captured verification — repair pass

The whole repository check, .relayflow/check.sh, which mirrors the four
PR-triggered workflows (cloud-runtime-artifact.yml, surface-package.yml,
schema-publish.yml's validate job, and the offline half of
review-swarm-wrapper-guard.yml):

$ sh .relayflow/check.sh
node: v25.6.0
npm:  11.8.0
bun:  1.4.0   (CI pins 1.4.0)
cargo: cargo 1.98.1 (797e8a9bc 2026-08-05)
...
 Test Files  3 failed | 192 passed | 1 skipped (196)
      Tests  22 failed | 3194 passed | 4 skipped (3220)
...
FAILED: the SDK suite above exited nonzero (sections 2-4 still ran; see their output)

Kernel workspace: 27 test result: ok lines, 278 tests, 0 failed. Surface
package gate: 52 source tests, PACKED_RUNTIME_REFUSAL_OK,
PACKED_TYPESCRIPT_OK, 34 packed-consumer tests. Schema: regenerated twice,
git diff --exit-code clean, 79 schema tests pass. Review-gate parity:
lens-parity-check: PASS, lens-cli-parity-check: PASS, 21 passed, 0 failed.

Every one of the 22 failures is the bubblewrap sandbox — the machine cannot
create unprivileged user namespaces
(kernel.apparmor_restrict_unprivileged_userns=1, /proc/sys is a sysbox FUSE
mount that sudo sysctl -w cannot write, and Debian's bubblewrap has no setuid
support). Full diagnosis, including the probes that rule out every workaround,
is in .relayflow/repair-notes.md.

The two suites this change actually touches:

$ cd packages/sdk && ./node_modules/.bin/vitest run tests/canonical-software-factory.test.ts
Stopped: invalid pull-request metadata (duplicate-github-closing-reference). No branch was pushed and no pull request was opened.
Stopped: invalid pull-request metadata (malformed-review-scope). No branch was pushed and no pull request was opened.
Stopped: invalid pull-request metadata (malformed-review-scope). No branch was pushed and no pull request was opened.
Stopped: could not read the reviewed head commit. Nothing was pushed.
Stopped: could not read the reviewed head commit. Nothing was pushed.
Stopped: could not read the reviewed head commit. Nothing was pushed.
 ✓ tests/canonical-software-factory.test.ts (18 tests) 693ms

 Test Files  1 passed (1)
      Tests  18 passed (18)
$ cd packages/sdk && ./node_modules/.bin/vitest run tests/babysitter-native-extension.test.ts
 Test Files  1 failed (1)
      Tests  1 failed | 40 passed (41)

The one remaining failure there is
runs the exact published 2.0.26 native bytes in the isolated capability path,
the bubblewrap case. Before the pin update the same file could not load at all:
Tests 41 skipped (41), Error: Hosted capability isolation accepts only the reviewed Software Factory base source.

Mutation checks — the reviewed-base pins

Each mutation changed one pin, ran the suite, then restored the file from a saved
byte copy with cmp asserting byte equality, and re-ran.

$ sed -i "s/ee56899fcb5c0a968d845620db3d4229673a3b732dd4d6131ab43b81822bf97b/49c993220b9c34fab2d4b0e51911656f62b8b657f534d988691960d45bb9d9b6/" src/hosted-extension-runtime.ts
$ ./node_modules/.bin/vitest run tests/babysitter-native-extension.test.ts
 FAIL  tests/babysitter-native-extension.test.ts [ tests/babysitter-native-extension.test.ts ]
Error: Hosted capability isolation accepts only the reviewed Software Factory base source.
 ❯ baseAt src/hosted-extension-runtime.ts:239:13
 Test Files  1 failed (1)
      Tests  41 skipped (41)

$ cp /tmp/hosted-extension-runtime.ts.fixed src/hosted-extension-runtime.ts && cmp /tmp/hosted-extension-runtime.ts.fixed src/hosted-extension-runtime.ts && echo "restored byte-for-byte"
restored byte-for-byte
$ ./node_modules/.bin/vitest run tests/babysitter-native-extension.test.ts
 Test Files  1 failed (1)
      Tests  1 failed | 40 passed (41)
$ sed -i "246s/'2.0.23'/'2.0.22'/" src/hosted-extension-runtime.ts   # revert the assigned version
$ ./node_modules/.bin/vitest run tests/babysitter-native-extension.test.ts -t 'composes onto Software Factory'
    "name": "software-factory",
-   "version": "2.0.23",
+   "version": "2.0.22",
  }
 ❯ tests/babysitter-native-extension.test.ts:112:32
 Test Files  1 failed (1)
      Tests  1 failed | 40 skipped (41)

$ cp /tmp/hosted-extension-runtime.ts.fixed src/hosted-extension-runtime.ts && cmp /tmp/hosted-extension-runtime.ts.fixed src/hosted-extension-runtime.ts && echo "restored byte-for-byte"
restored byte-for-byte
$ ./node_modules/.bin/vitest run tests/babysitter-native-extension.test.ts -t 'composes onto Software Factory'
 ✓ tests/babysitter-native-extension.test.ts (41 tests | 40 skipped) 259ms
 Test Files  1 passed (1)
      Tests  1 passed | 40 skipped (41)

Captured verification — implementation pass

Before implementation, with only the harness made strict:

$ cd packages/sdk && ./node_modules/.bin/vitest run tests/canonical-software-factory.test.ts

 RUN  v2.1.9 /home/daytona/.relayflow-v2-supervisor/durable/repository/packages/sdk

Stopped: invalid pull-request metadata (duplicate-github-closing-reference). No branch was pushed and no pull request was opened.
 ✓ tests/canonical-software-factory.test.ts (3 tests) 164ms

 Test Files  1 passed (1)
      Tests  3 passed (3)
   Start at  05:42:22
   Duration  865ms (transform 311ms, setup 0ms, collect 524ms, tests 164ms, environment 0ms, prepare 43ms)

exit=0

Final selected suites:

$ cd packages/sdk && ./node_modules/.bin/vitest run tests/canonical-software-factory.test.ts tests/flow-requirements.test.ts tests/babysitter-native-extension.test.ts

 RUN  v2.1.9 /home/daytona/.relayflow-v2-supervisor/durable/repository/packages/sdk

Stopped: invalid pull-request metadata (duplicate-github-closing-reference). No branch was pushed and no pull request was opened.
Stopped: invalid pull-request metadata (malformed-review-scope). No branch was pushed and no pull request was opened.
Stopped: invalid pull-request metadata (malformed-review-scope). No branch was pushed and no pull request was opened.
Stopped: could not read the reviewed head commit. Nothing was pushed.
Stopped: could not read the reviewed head commit. Nothing was pushed.
Stopped: could not read the reviewed head commit. Nothing was pushed.
 ✓ tests/canonical-software-factory.test.ts (18 tests) 787ms
 ❯ tests/babysitter-native-extension.test.ts (41 tests | 41 skipped) 279ms
 ✓ tests/flow-requirements.test.ts (14 tests) 727ms
   ✓ flows check prints REQUIRES > names the helper, the harness and the mcp server of an authored flow 388ms

⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯

 FAIL  tests/babysitter-native-extension.test.ts [ tests/babysitter-native-extension.test.ts ]
Error: Hosted capability isolation accepts only the reviewed Software Factory base source.
 ❯ baseAt src/hosted-extension-runtime.ts:239:13
    237|   try {
    238|     if (snapshot.snapshotFlowSha256 !== SOFTWARE_FACTORY_SHA256) {
    239|       throw new PluginError(
       |             ^
    240|         'plugin_source_invalid',
    241|         'Hosted capability isolation accepts only the reviewed Softwar…
 ❯ Module.loadHostedExtensionRuntime src/hosted-extension-runtime.ts:90:16
 ❯ composed tests/babysitter-native-extension.test.ts:64:25
 ❯ tests/babysitter-native-extension.test.ts:100:37

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯

 Test Files  1 failed | 2 passed (3)
      Tests  32 passed | 41 skipped (73)
   Start at  05:45:22
   Duration  2.80s (transform 1.88s, setup 0ms, collect 4.07s, tests 1.79s, environment 0ms, prepare 128ms)

exit=1

Test TypeScript compilation:

$ cd packages/sdk && ./node_modules/.bin/tsc -p tsconfig.tests.json
exit=0

Examples typecheck produced the same errors before and after implementation
in an untouched file (neither run passes):

$ cd packages/surface && ./node_modules/.bin/tsc -p ../../examples/tsconfig.json
../../workflows/stuck-run-triage.flow.ts(77,12): error TS2304: Cannot find name 'URL'.
../../workflows/stuck-run-triage.flow.ts(79,15): error TS2552: Cannot find name 'URL'. Did you mean 'url'?
exit=2

Mutation checks — scope placement, verdict classification, head validation

Re-run at this branch's head against the committed flow. Each mutation edits
examples/software-factory/software-factory.flow.ts, runs the named regression,
restores the file from a copy saved before the first mutation
(cp examples/software-factory/software-factory.flow.ts /tmp/flow.fixed), proves
the restore with cmp, and re-runs the same regression. Failure and pass are
both captured below; …/vitest is packages/sdk/node_modules/.bin/vitest, run
from packages/sdk. Output is filtered to the result lines
(grep -E "✓|×|Tests |Test Files |AssertionError|Expected:|Received:").

M1 — the scope guard must not sit behind the GitHub branch. The standalone
guard becomes an elif after the arm that already answers valid for GitHub
sources, so a GitHub PR body would never be scope-checked:

$ git diff -U0 -- examples/software-factory/software-factory.flow.ts
@@ -38 +37,0 @@ const VALIDATE_CHANGE_METADATA = [
-  `if [ -n "$scope" ]; then scope_count=$(grep -cE '^<!-- relayflow-review ' ${WORK}/pr-body.md || true); if [ "$scope_count" -ne 1 ]; then echo malformed-review-scope; exit 0; fi; fi`,
@@ -45,0 +45 @@ const VALIDATE_CHANGE_METADATA = [
+  `elif [ -n "$scope" ] && [ "$(grep -cE '^<!-- relayflow-review ' ${WORK}/pr-body.md || true)" -ne 1 ]; then echo malformed-review-scope`,

$ …/vitest run tests/canonical-software-factory.test.ts -t 'rejects a forged scope for'
   × canonical software-factory review scope > rejects a forged scope for github before publication 57ms
AssertionError: expected 'step_failed' to be 'needs_human' // Object.is equality
Expected: "needs_human"
Received: "step_failed"
 Test Files  1 failed (1)
      Tests  1 failed | 1 passed | 16 skipped (18)

$ cp /tmp/flow.fixed examples/software-factory/software-factory.flow.ts && cmp /tmp/flow.fixed examples/software-factory/software-factory.flow.ts && echo restored-byte-for-byte
restored-byte-for-byte

$ …/vitest run tests/canonical-software-factory.test.ts -t 'rejects a forged scope for'
 ✓ tests/canonical-software-factory.test.ts (18 tests | 16 skipped) 81ms
 Test Files  1 passed (1)
      Tests  2 passed | 16 skipped (18)

The local case passes under this mutation and the github case does not,
which is the point: the guard's placement, not its existence, is what makes it
reach a GitHub body.

M2 — the classifier's final else must stay BLOCKED. else echo BLOCKED
becomes else echo PASSED, which is the arm an empty review.unverified falls
through to:

$ git diff -U0 -- examples/software-factory/software-factory.flow.ts
@@ -161 +161 @@ export default flow<Input>("software-factory", {
-  const verdict = await f.run(`count=0; for v in blocked unverified passed; do [ -f ${WORK}/review.$v ] && count=$((count+1)); done; if [ "$count" -ne 1 ]; then echo BLOCKED; elif [ -f ${WORK}/review.blocked ]; then echo BLOCKED; elif [ -s ${WORK}/review.unverified ]; then echo UNVERIFIED; elif [ -f ${WORK}/review.passed ]; then echo PASSED; else echo BLOCKED; fi`);
+  const verdict = await f.run(`count=0; for v in blocked unverified passed; do [ -f ${WORK}/review.$v ] && count=$((count+1)); done; if [ "$count" -ne 1 ]; then echo BLOCKED; elif [ -f ${WORK}/review.blocked ]; then echo BLOCKED; elif [ -s ${WORK}/review.unverified ]; then echo UNVERIFIED; elif [ -f ${WORK}/review.passed ]; then echo PASSED; else echo PASSED; fi`);

$ …/vitest run tests/canonical-software-factory.test.ts -t 'fails closed for empty unverified'
   × canonical software-factory review scope > fails closed for empty unverified 137ms
AssertionError: expected 'success' to be 'step_failed' // Object.is equality
Expected: "step_failed"
Received: "success"
 Test Files  1 failed (1)
      Tests  1 failed | 17 skipped (18)

$ cp /tmp/flow.fixed examples/software-factory/software-factory.flow.ts && cmp /tmp/flow.fixed examples/software-factory/software-factory.flow.ts && echo restored-byte-for-byte
restored-byte-for-byte

$ …/vitest run tests/canonical-software-factory.test.ts -t 'fails closed for empty unverified'
 ✓ tests/canonical-software-factory.test.ts (18 tests | 17 skipped) 48ms
 Test Files  1 passed (1)
      Tests  1 passed | 17 skipped (18)

M3 — "not exactly one verdict" must stay BLOCKED. The count != 1 arm
becomes PASSED; that is the arm a silent adversary (no verdict file at all)
takes:

$ git diff -U0 -- examples/software-factory/software-factory.flow.ts
@@ -161 +161 @@ export default flow<Input>("software-factory", {
-  const verdict = await f.run(`count=0; for v in blocked unverified passed; do [ -f ${WORK}/review.$v ] && count=$((count+1)); done; if [ "$count" -ne 1 ]; then echo BLOCKED; elif [ -f ${WORK}/review.blocked ]; then echo BLOCKED; elif [ -s ${WORK}/review.unverified ]; then echo UNVERIFIED; elif [ -f ${WORK}/review.passed ]; then echo PASSED; else echo BLOCKED; fi`);
+  const verdict = await f.run(`count=0; for v in blocked unverified passed; do [ -f ${WORK}/review.$v ] && count=$((count+1)); done; if [ "$count" -ne 1 ]; then echo PASSED; elif [ -f ${WORK}/review.blocked ]; then echo BLOCKED; elif [ -s ${WORK}/review.unverified ]; then echo UNVERIFIED; elif [ -f ${WORK}/review.passed ]; then echo PASSED; else echo BLOCKED; fi`);

$ …/vitest run tests/canonical-software-factory.test.ts -t 'fails closed for no verdict'
   × canonical software-factory review scope > fails closed for no verdict 48ms
AssertionError: expected 'success' to be 'step_failed' // Object.is equality
Expected: "step_failed"
Received: "success"
 Test Files  1 failed (1)
      Tests  1 failed | 17 skipped (18)

$ cp /tmp/flow.fixed examples/software-factory/software-factory.flow.ts && cmp /tmp/flow.fixed examples/software-factory/software-factory.flow.ts && echo restored-byte-for-byte
restored-byte-for-byte

$ …/vitest run tests/canonical-software-factory.test.ts -t 'fails closed for no verdict'
 ✓ tests/canonical-software-factory.test.ts (18 tests | 17 skipped) 46ms
 Test Files  1 passed (1)
      Tests  1 passed | 17 skipped (18)

M4 — the reviewed head must be validated before it reaches a command. The
40-hex test is widened to match anything:

$ git diff -U0 -- examples/software-factory/software-factory.flow.ts
@@ -97 +97 @@ export default flow<Input>("software-factory", {
-    if (!/^[0-9a-f]{40}$/.test(reviewedHead)) {
+    if (!/^.*$/.test(reviewedHead)) {

$ …/vitest run tests/canonical-software-factory.test.ts -t 'rejects malformed head'
   × canonical software-factory review scope > rejects malformed head "" before publication 52ms
   × canonical software-factory review scope > rejects malformed head "not-a-sha" before publication 38ms
   × canonical software-factory review scope > rejects malformed head "a'; touch injected; #" before publication 42ms
AssertionError: expected 'step_failed' to be 'needs_human' // Object.is equality
Expected: "needs_human"
Received: "step_failed"
 Test Files  1 failed (1)
      Tests  3 failed | 15 skipped (18)

$ cp /tmp/flow.fixed examples/software-factory/software-factory.flow.ts && cmp /tmp/flow.fixed examples/software-factory/software-factory.flow.ts && echo restored-byte-for-byte
restored-byte-for-byte

$ …/vitest run tests/canonical-software-factory.test.ts -t 'rejects malformed head'
 ✓ tests/canonical-software-factory.test.ts (18 tests | 15 skipped) 64ms
 Test Files  1 passed (1)
      Tests  3 passed | 15 skipped (18)

After all four, the flow file is the committed file — which is the restore proof
that outlives /tmp:

$ sha256sum examples/software-factory/software-factory.flow.ts
ee56899fcb5c0a968d845620db3d4229673a3b732dd4d6131ab43b81822bf97b  examples/software-factory/software-factory.flow.ts

$ git status --short -- examples packages docs
 M examples/software-factory/README.md

(The one modified file is a stray double blank line removed from the README
prose added by this branch; the flow, the tests and the pins are untouched.
summary.md — this file — is modified too, which is why the status above is
scoped to the code paths.)

Re-verified at this head

$ cd packages/sdk && npm run typecheck --silent && npm run typecheck:tests --silent && echo "TYPECHECK OK"
TYPECHECK OK

$ cd packages/sdk && ./node_modules/.bin/vitest run tests/canonical-software-factory.test.ts tests/hosted-base-snapshot.test.ts
Stopped: invalid pull-request metadata (duplicate-github-closing-reference). No branch was pushed and no pull request was opened.
Stopped: invalid pull-request metadata (malformed-review-scope). No branch was pushed and no pull request was opened.
Stopped: invalid pull-request metadata (malformed-review-scope). No branch was pushed and no pull request was opened.
Stopped: could not read the reviewed head commit. Nothing was pushed.
Stopped: could not read the reviewed head commit. Nothing was pushed.
Stopped: could not read the reviewed head commit. Nothing was pushed.
 ✓ tests/canonical-software-factory.test.ts (18 tests) 705ms
 ✓ tests/hosted-base-snapshot.test.ts (18 tests) 2054ms
 Test Files  2 passed (2)
      Tests  36 passed (36)

$ cd packages/sdk && ./node_modules/.bin/vitest run tests/flow-requirements.test.ts tests/catalog-plugins.test.ts tests/babysitter-catalog-export.test.ts tests/babysitter-native-extension.test.ts
 FAIL  tests/babysitter-native-extension.test.ts > native Babysitter extension > runs the exact published 2.0.26 native bytes in the isolated capability path
Caused by: Error: Hosted extension sandbox exited without a valid completion (exit 1): bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted
 Test Files  1 failed | 3 passed (4)
      Tests  1 failed | 70 passed (71)

The single failure is the bubblewrap case described above and in
.relayflow/repair-notes.md; it fails identically on an unmodified checkout of
this machine and no change here can clear it.

Checks

The checks fail on the base commit too, so these failures were not introduced by this change: they come from the repository itself or from the environment the checks ran in. This pull request is a draft until someone looks.

What ran (.relayflow/check.sh)
#!/bin/sh
# Fresh-machine self-check for this repository, mirroring what CI runs on a
# pull request.
#
# The repo has no Makefile/justfile and no root package.json; CI *is* the
# definition of "checked". Four workflows fire on a pull request:
#
#   .github/workflows/cloud-runtime-artifact.yml  kernel + SDK + artifact (main job)
#   .github/workflows/surface-package.yml         surface source/regressions/packed consumers
#   .github/workflows/schema-publish.yml          validate job: schema codegen + parity
#   .github/workflows/review-swarm-wrapper-guard.yml  offline gate-parity checks
#
# and two more that are not machine-checkable here (see "Deliberately not run"
# at the bottom of this file).
#
# Every command below is the command CI runs, in CI's order, with only the
# deviations that are called out in comments.

set -e

repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd -P)
cd "$repo_root"

# ---------------------------------------------------------------------------
# Toolchain
# ---------------------------------------------------------------------------
# CI pins actions/setup-node@22, oven-sh/setup-bun@1.4.0 and
# dtolnay/rust-toolchain@stable. Print what this machine actually has: a
# version skew here (notably Bun, which builds the surface, the schema tests
# and the standalone CLI) is the first thing to check when a step fails here
# but is green in CI.
echo "node: $(node --version)"
echo "npm:  $(npm --version)"

# oven-sh/setup-bun@v2 with bun-version "1.4.0" equivalent. Not cosmetic: the
# standalone-CLI suite asserts `bun --version` is exactly 1.4.0
# (packages/sdk/tests/authored-node-runtime.test.ts:18), because the packed
# runtime is built by that bun. This machine ships 1.3.6 on PATH, so install the
# pinned version beside it and prefer it, exactly as the action does.
if [ "$(bun --version 2>/dev/null)" != "1.4.0" ]; then
  if [ "$("$HOME/.bun/bin/bun" --version 2>/dev/null)" != "1.4.0" ]; then
    echo "installing bun 1.4.0 (CI pins it; this machine has $(bun --version 2>/dev/null || echo none))"
    curl -fsSL https://bun.sh/install | bash -s "bun-v1.4.0"
  fi
  PATH="$HOME/.bun/bin:$PATH"
  export PATH
fi
echo "bun:  $(bun --version)   (CI pins 1.4.0)"

# Node decides whether an extension-less file is ESM or CommonJS from the
# NEAREST ancestor package.json. This sandbox has /home/daytona/package.json
# with "type": "commonjs" above the checkout; a GitHub runner has no
# package.json above the workspace at all. Under that inherited "commonjs" the
# extension-less agent-CLI fixtures in testdata/preflight (ESM with top-level
# await) run as CommonJS and emit nothing at all, so live-kernel.test.ts sees
# null agent output and eight of its cases fail for a reason that has nothing to
# do with the kernel. Shadow it with a neutral manifest immediately OUTSIDE the
# repository: the repo deliberately has no root package.json, and adding one
# there would change what CI checks.
repo_parent=$(dirname "$repo_root")
if [ ! -e "$repo_parent/package.json" ]; then
  echo '{}' > "$repo_parent/package.json"
  echo "wrote $repo_parent/package.json to shadow an inherited \"type\": \"commonjs\""
fi

# dtolnay/rust-toolchain@stable equivalent. Plain `cargo`, NOT ops/cargo.sh:
# that wrapper redirects RUSTUP_HOME and CARGO_TARGET_DIR for a cloud sandbox
# whose tree-propagation drops large files, and it puts the build output
# somewhere other than kernel/target, which the SDK step below reads from.
# cloud-runtime-artifact.yml uses plain cargo for the same reason.
if ! command -v cargo >/dev/null 2>&1; then
  if [ ! -x "$HOME/.cargo/bin/cargo" ]; then
    echo "installing a stable Rust toolchain (kernel/ is edition 2024)"
    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
      | sh -s -- -y --default-toolchain stable --profile minimal --no-modify-path
  fi
  PATH="$HOME/.cargo/bin:$PATH"
  export PATH
fi
echo "cargo: $(cargo --version)"

# ---------------------------------------------------------------------------
# cloud-runtime-artifact.yml — "Provision hosted extension sandbox"
# ---------------------------------------------------------------------------
# packages/sdk/src/hosted-extension-sandbox.ts execs /usr/bin/bwrap, and
# tests/hosted-extension-isolation.test.ts exercises the real production
# namespace command, so the SDK suite below needs bubblewrap on the host.
if ! command -v bwrap >/dev/null 2>&1; then
  sudo DEBIAN_FRONTEND=noninteractive apt-get update
  sudo DEBIAN_FRONTEND=noninteractive apt-get install --yes --no-install-recommends bubblewrap
fi
# Ubuntu 24.04 restricts unprivileged user namespaces via AppArmor. CI guards
# this the same way, because the knob does not exist on every kernel.
# Unlike CI's ephemeral VM, this machine is a container whose sudo cannot write
# that key ("permission denied on key ..."), so the write is advisory here: the
# bwrap smoke immediately below is the real check of whether the namespace
# command works, and it fails loudly if it does not.
if sudo sysctl kernel.apparmor_restrict_unprivileged_userns >/dev/null 2>&1; then
  sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || \
    echo "note: could not clear apparmor_restrict_unprivileged_userns; relying on the bwrap smoke below"
fi
/usr/bin/bwrap --version
# Advisory here, fatal in CI. On this machine the namespace command cannot work
# for a non-root user at all: kernel.apparmor_restrict_unprivileged_userns is 1
# and not writable (sysbox fuse-mounts /proc/sys), and Debian's bubblewrap is
# built without setuid support, so there is no way to hand the unprivileged test
# user the capability. `sudo /usr/bin/bwrap --unshare-all ...` succeeds. The
# bwrap-dependent cases in the SDK suite therefore fail below for that reason
# and that reason only; see .relayflow/repair-notes.md.
if ! /usr/bin/bwrap --unshare-all --die-with-parent --new-session --ro-bind / / /bin/true; then
  echo "note: unprivileged bwrap namespaces are unavailable on this machine (see .relayflow/repair-notes.md)"
fi

# ---------------------------------------------------------------------------
# 1. cloud-runtime-artifact.yml — the main test job
# ---------------------------------------------------------------------------
node --test scripts/cloud-artifact.test.mjs

# Kernel: build the release binary the SDK suite and the artifact step consume,
# then run the Rust workspace suite.
( cd kernel && cargo build --locked --release -p relayflowd )
( cd kernel && cargo test --workspace )

# Authoring surface, built from source before anything typechecks against it.
( cd packages/surface && bun install --frozen-lockfile --ignore-scripts && bun run build )

# --ignore-scripts: the surface is already built above, and without it npm runs
# the file: dependency's prepare before its own devDependencies exist.
npm ci --prefix packages/sdk --ignore-scripts

# npm ci installs the last *published* @relayflows/surface from the registry.
# Override it with the local build so an SDK change that imports a symbol which
# has not shipped yet still typechecks. --no-save keeps the manifests clean;
# the './' prefix is load-bearing (npm reads a bare path as a GitHub shorthand).
npm install ./packages/surface --prefix packages/sdk --no-save --ignore-scripts

# workflows/*.flow.ts import '@relayflows/surface' from the repo root, where
# there is no node_modules of its own; link the same local surface there.
mkdir -p node_modules/@relayflows
ln -sfn ../../packages/sdk/node_modules/@relayflows/surface node_modules/@relayflows/surface
node -e "console.log(require.resolve('@relayflows/surface'))"

# The SDK suite, expanded exactly as CI expands it rather than via `npm test`:
# `npm test` runs test:prep, which shells out to ops/cargo.sh to compile a
# second, debug relayflowd through a wrapper that points RELAYFLOWD_BIN outside
# kernel/target. The release binary built above is the one used instead.
# test:prep's other half is kept: the preflight CLI fixtures must be executable,
# and without that the failure is an opaque EACCES deep inside a preflight test.
[ ! -d testdata/preflight ] || find testdata/preflight -name '*-cli' -type f -exec chmod +x {} +
npm run typecheck --prefix packages/sdk
npm run build --prefix packages/sdk
npm run typecheck:tests --prefix packages/sdk
# RELAYFLOWS_ALLOW_ANALYZER_SKIP=1 for the same reason CI sets it: one case in
# tests/live-kernel.test.ts drives the real Claude analyzer and fails closed
# when it cannot, which is correct — an unavailable analyzer is diagnostics,
# never acceptance. This machine has no `claude` binary and no model access, so
# that case is skipped and says so in its own output. THIS SCRIPT IS THEREFORE
# NOT GATE-2 ACCEPTANCE EVIDENCE. Everything else in the suite still runs.
#
# This one step does not abort the script, and the deviation is deliberate: the
# four sections below are four INDEPENDENT jobs in CI, where one failing job
# never stops the others, and on this machine the SDK suite has a permanent
# environmental failure (the bubblewrap sandbox cannot start; see the note above
# and .relayflow/repair-notes.md). Aborting here would hide sections 2-4 behind
# a failure that no code change can clear. The status is remembered and the
# script still exits nonzero at the very bottom.
sdk_suite_status=0
( cd packages/sdk \
  && RELAYFLOWD_BIN="$repo_root/kernel/target/release/relayflowd" \
     RELAYFLOWS_ALLOW_ANALYZER_SKIP=1 \
     ./node_modules/.bin/vitest run ) || sdk_suite_status=1

# Standalone CLI, artifact assembly, and the artifact smoke: the packaged
# runtime is a product of this repo, and CI proves the exact bytes it ships
# both verify and execute.
mkdir -p dist/cloud-artifact-input
node scripts/build-standalone-cli.mjs bun-linux-x64 dist/cloud-artifact-input/flows
node scripts/cloud-artifact.mjs build \
  --relayflowd kernel/target/release/relayflowd \
  --flows-executable dist/cloud-artifact-input/flows \
  --output-dir dist/cloud-artifact \
  --source-commit "$(git rev-parse HEAD)"
archive=$(find dist/cloud-artifact -name '*.tar.gz' -type f -print -quit)
node scripts/cloud-artifact.mjs verify \
  --archive "$archive" \
  --sha256 "$(awk '{print $1}' "$archive.sha256")"
rm -rf dist/cloud-artifact-smoke
mkdir -p dist/cloud-artifact-smoke
tar -xzf "$archive" -C dist/cloud-artifact-smoke
dist/cloud-artifact-smoke/bin/relayflowd --help
flows_output=$(dist/cloud-artifact-smoke/bin/flows check --json testdata/hello-deterministic.flow.yaml)
printf '%s\n' "$flows_output"
node -e '
  const report = JSON.parse(process.argv[1]);
  if (report.ok !== true) throw new Error("flows smoke report was not ok");
  if (report.path !== "testdata/hello-deterministic.flow.yaml") {
    throw new Error(`flows smoke reported unexpected path: ${report.path}`);
  }
' "$flows_output"

# ---------------------------------------------------------------------------
# 2. surface-package.yml
# ---------------------------------------------------------------------------
# Surface source tests, regression typechecks, and the packed-tarball consumers
# (runtime + TypeScript). Invoked with bash, as CI does: the gate script is
# bash, not POSIX sh. It re-runs `npm ci --prefix packages/sdk` and installs the
# packed surface over the directory install above — harmless, and the reason
# this section comes after the SDK suite rather than before it.
bash scripts/surface-package-gate.sh

# ---------------------------------------------------------------------------
# 3. schema-publish.yml — the `validate` job (the only job that runs on a PR)
# ---------------------------------------------------------------------------
# Code generation is checked by regenerating and diffing against the committed
# artifact, then regenerating again to prove the generator is deterministic.
node scripts/generate-json-schema.mjs
git diff --exit-code -- packages/schema/flows.schema.json
cp packages/schema/flows.schema.json /tmp/flows.schema.first.json
node scripts/generate-json-schema.mjs
diff -q /tmp/flows.schema.first.json packages/schema/flows.schema.json
( cd packages/schema && bun run test )

# ---------------------------------------------------------------------------
# 4. review-swarm-wrapper-guard.yml — the offline half
# ---------------------------------------------------------------------------
# Lens parity between the pre-swarm runner and the post-push swarm definition.
# PRESWARM_ALLOW_MISSING_CLI=1 exactly as the workflow sets it: the CLI-presence
# half of the check belongs to the pre-swarm runner's own environment, and no
# generic runner (or this machine) has claude/codex/opencode on PATH.
sh ops/preswarm-check/lens-parity-check.sh
PRESWARM_ALLOW_MISSING_CLI=1 sh ops/preswarm-check/lens-cli-parity-check.sh

# The review gate's own hermetic self-test: agent-relay and gh are stubbed, so
# it runs offline in seconds. It asserts the gate can still say no.
bash .github/workflows/scripts/swarm-gate.test.sh

# ---------------------------------------------------------------------------
# The remembered SDK-suite status
# ---------------------------------------------------------------------------
if [ "$sdk_suite_status" -ne 0 ]; then
  echo "FAILED: the SDK suite above exited nonzero (sections 2-4 still ran; see their output)"
  exit 1
fi

# ---------------------------------------------------------------------------
# Deliberately not run here
# ---------------------------------------------------------------------------
# - .github/workflows/scripts/swarm-definition.test.sh (wrapper-guard/review-swarm):
#   swarm-definition.sh parses YAML with Ruby's Psych, and this machine has no
#   ruby. CI installs none either — it asserts `ruby --version` and relies on
#   the GitHub runner image shipping it.
# - .github/workflows/scripts/swarm-wrapper-guard.sh: queries the PR's changed
#   files through `gh` with GH_TOKEN; there is no pull request in scope here.
# - review-swarm.yml: launches the adversarial review swarm through
#   `agent-relay cloud run` against Agent Relay Cloud. Needs CLOUD_API_KEY and
#   RELAY_WORKSPACE_KEY, and its preflight authenticates against the live Cloud
#   API. Credentials + external service.
# - publish.yml: release automation (workflow_dispatch only). Publishes to npm
#   with OIDC provenance, pushes a release commit and tag, and creates a GitHub
#   Release. Deployment + registry credentials.
#   Its one secret-free step, `node --test scripts/publish.test.mjs`, is not
#   part of any PR-triggered check, so it is left out for the same reason the
#   rest of that workflow is.
# - schema-publish.yml `npm` and `pages` jobs: guarded by
#   `github.event_name != 'pull_request'`; they publish to npm (NPM_TOKEN) and
#   deploy GitHub Pages. Deployment + secrets.
# - The macOS half of publish.yml (build-darwin-arm64) and any darwin runtime
#   packing: this is a linux-x64 machine, and pack-release.mjs skips execution
#   smokes for a foreign host by design.
Output on this branch (last 80 lines)
(pass) structural parity: tools.fs: single grant [0.09ms]
(pass) structural parity: workspace: grant array [0.07ms]
(pass) structural parity: tools.fs: grant array [0.06ms]
(pass) structural parity: workspace: non-string item [0.06ms]
(pass) structural parity: tools.fs: non-string item [0.07ms]
(pass) structural parity: workspace: nested array [0.06ms]
(pass) structural parity: tools.fs: nested array [0.06ms]
(pass) structural parity: llm: output object [12.49ms]
(pass) structural parity: llm: boolean output [0.13ms]
(pass) structural parity: llm: output and verification [9.96ms]
(pass) structural parity: llm: exit gate [0.20ms]
(pass) structural parity: llm: trimmed model [0.08ms]
(pass) structural parity: llm: control in model [0.08ms]
(pass) structural parity: agent: output object [12.21ms]
(pass) structural parity: agent: boolean output [0.15ms]
(pass) structural parity: agent: output and verification [9.77ms]
(pass) structural parity: agent: exit gate [0.19ms]
(pass) structural parity: agent: trimmed model [0.14ms]
(pass) structural parity: agent: control in model [0.07ms]
(pass) step examples compile and validate [0.41ms]
(pass) generated schema satisfies the existing bounded-reference rule [5.49ms]
(pass) semantic checks remain explicit runtime responsibilities [0.60ms]
(pass) embedded dialect http://json-schema.org/draft-04/schema# [10.57ms]
(pass) embedded dialect http://json-schema.org/draft-06/schema# [14.00ms]
(pass) embedded dialect http://json-schema.org/draft-07/schema# [9.14ms]
(pass) embedded dialect https://json-schema.org/draft/2019-09/schema [20.31ms]
(pass) embedded dialect https://json-schema.org/draft/2020-12/schema [14.75ms]
(pass) header hint is warning-only, first-line aware, and never edits input [19.19ms]
(pass) canonical surface parity: "repo" [0.25ms]
(pass) canonical surface parity: "/repo/src" [0.10ms]
(pass) canonical surface parity: "pr://github/example" [0.08ms]
(pass) canonical surface parity: "/" [0.08ms]
(pass) canonical surface parity: "pr://" [0.06ms]
(pass) canonical surface parity: "" [0.09ms]
(pass) canonical surface parity: " repo" [0.10ms]
(pass) canonical surface parity: "repo " [0.08ms]
(pass) canonical surface parity: "repo//src" [0.08ms]
(pass) canonical surface parity: "repo/../src" [0.10ms]
(pass) canonical surface parity: "repo/." [0.11ms]
(pass) canonical surface parity: ":/bad//path" [0.08ms]
(pass) named declarations and selected input paths use authoring shapes [43.33ms]

tests/smoke.test.ts:
(pass) all exported spec type nodes have documented definitions [16.93ms]
(pass) regeneration is byte-stable and committed schema has not drifted [452.14ms]
(pass) npm tarball contains only data and documentation with no runtime dependencies [276.79ms]

 79 pass
 0 fail
 3980 expect() calls
Ran 79 tests across 2 files. [1.69s]
lens-parity-check: PASS — all three lenses carry every canonical clause.
lens-cli-parity-check: PASS — three lenses, runner and swarm agree, every CLI present.
== verdict extraction ==
  ok   a bare REVIEW_FAILED is FAILED
  ok   a bare REVIEW_PASSED is PASSED
  ok   trailing blank lines do not hide the marker
  ok   surrounding whitespace is trimmed
  ok   a marker followed by a sign-off is UNCLEAR (PR #240 bug)
  ok   REVIEW_FAILED is never upgraded by surrounding prose
  ok   an empty transcript is UNCLEAR, not PASSED
  ok   a transcript merely containing the word is UNCLEAR
== lens selection ==
  ok   no reviews directory yields MISSING
  ok   an absent transcript yields MISSING
  ok   a transcript predating the run yields STALE
  ok   a transcript with the marker's exact mtime yields STALE
  ok   the newest fresh transcript wins
== swarm-post.sh end to end ==
  ok   one lens REVIEW_FAILED fails the gate (exit 1)
  ok   the objection is reported as FAILED, not STALE
  ok   a failing run still reports its verdict to the PR
  ok   three clean passes pass the gate (exit 0)
  ok   an UNCLEAR lens fails the gate
  ok   a lens with no transcript at all fails the gate
  ok   an empty sync fails the gate (exit 1)
  ok   the empty-sync reason reaches the step log

21 passed, 0 failed
FAILED: the SDK suite above exited nonzero (sections 2-4 still ran; see their output)
Output on the base commit (last 80 lines)
node: v25.6.0
npm:  11.8.0
bun:  1.4.0   (CI pins 1.4.0)
/tmp/relayflow-recipe.eZR8EK: 62: cannot create //package.json: Permission denied

What the repair agent found

.relayflow/check.sh on this machine — what failed, and why

.relayflow/check.sh mirrors the four PR-triggered CI workflows. Run on this
branch it stopped on the very first step, and the failures behind it split into
three kinds: missing setup (fixed in check.sh, which is not committed),
a bug in this branch (fixed and committed), and one environment limit that
cannot be fixed here
(left failing, documented below).

Final state of a full run: 22 failing tests in 3 files, every one of them the
bubblewrap sandbox failing to start.
Everything else — 3194 SDK tests, the
kernel workspace suite, the surface-package gate, the schema codegen/parity job
and the offline review-gate checks — passes.

 Test Files  3 failed | 192 passed | 1 skipped (196)
      Tests  22 failed | 3194 passed | 4 skipped (3220)

1. Outside our control: bubblewrap cannot create namespaces here

Failing: tests/hosted-extension-isolation.test.ts (13),
tests/hosted-extension-protocol.test.ts (8), and one case in
tests/babysitter-native-extension.test.ts
(runs the exact published 2.0.26 native bytes in the isolated capability path).
Every failure reads
Hosted extension sandbox exited without a valid completion (exit 1): bwrap: ….

These tests exercise the real production namespace command
(packages/sdk/src/hosted-extension-sandbox.ts), so they need a working
unprivileged bwrap. CI provisions it by clearing an AppArmor knob on its
ephemeral VM (cloud-runtime-artifact.yml:124-137). That knob is not writable
here — /proc/sys is a sysbox FUSE mount — and there is no other route to the
capability:

$ sysctl kernel.apparmor_restrict_unprivileged_userns
kernel.apparmor_restrict_unprivileged_userns = 1

$ echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns
tee: /proc/sys/kernel/apparmor_restrict_unprivileged_userns: Permission denied
0

$ grep " /proc/sys " /proc/self/mountinfo
13262 13092 0:439 /proc/sys /proc/sys rw,nosuid,nodev,relatime - fuse sysboxfs rw,user_id=0,group_id=0,default_permissions,allow_other

$ /usr/bin/bwrap --unshare-all --die-with-parent --new-session --ro-bind / / /bin/true; echo "exit=$?"
bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted
exit=1

Handing the unprivileged user the capability another way does not work either —
Debian's bubblewrap is built without setuid support:

$ sudo setcap cap_sys_admin+ep /usr/bin/bwrap && /usr/bin/bwrap --unshare-all --ro-bind / / /bin/true
bwrap: Unexpected capabilities but not setuid, old file caps config?
exit=1

$ sudo chmod u+s /usr/bin/bwrap && /usr/bin/bwrap --unshare-all --ro-bind / / /bin/true
bwrap: setuid use of bubblewrap is not supported
exit=1

Both probes were reverted:

$ ls -l /usr/bin/bwrap; getcap /usr/bin/bwrap
-rwxr-xr-x 1 root root 80248 Aug 26 11:04 /usr/bin/bwrap

Running the suite as root only moves the wall: the smoke command succeeds, and
the real sandbox then fails one step later, because the container will not let a
nested namespace mount proc:

$ sudo /usr/bin/bwrap --unshare-all --die-with-parent --new-session --ro-bind / / /bin/true; echo "exit=$?"
exit=0

$ cd packages/sdk && sudo -E env "PATH=$PATH" HOME=/home/daytona ./node_modules/.bin/vitest run tests/hosted-extension-isolation.test.ts 2>&1 | head -6

 RUN  v2.1.9 /home/daytona/.relayflow-v2-supervisor/durable/repository/packages/sdk

 ❯ tests/hosted-extension-isolation.test.ts (22 tests | 13 failed) 4737ms
   × hosted extension capability isolation > executes the exact capability-only handler for a queued receipt 258ms
     → Hosted extension sandbox exited without a valid completion (exit 1): bwrap: Can't mount proc on /proc: Operation not permitted

Left failing. No change to this branch's code can clear it, and the tests are
right to fail closed rather than skip: they are the isolation gate. The bwrap
smoke in check.sh was therefore made advisory (it prints a note and continues)
so the rest of the run is reachable; the tests themselves were not touched.

2. Missing setup: Bun 1.3.6 where CI pins 1.4.0

tests/authored-node-runtime.test.ts failed at beforeAll:

AssertionError: expected '1.3.6' to be '1.4.0'
 ❯ tests/authored-node-runtime.test.ts:18:77

The assertion is correct — that suite builds and runs the packed standalone CLI,
and CI pins bun-version: "1.4.0" in all four workflows. This machine ships
1.3.6 on PATH. check.sh now installs the pinned Bun beside it and prefers it,
which is what oven-sh/setup-bun@v2 does. After that the suite passes:

$ cd packages/sdk && ./node_modules/.bin/vitest run tests/authored-node-runtime.test.ts
 Test Files  1 passed (1)
      Tests  14 passed (14)

3. Missing setup: an inherited "type": "commonjs" above the checkout

Eight tests/live-kernel.test.ts cases failed with output: null and
verification: { gate: 'execution', verdict: 'fail' }, or
TypeError: Cannot read properties of null.

Cause: Node decides whether an extension-less file is ESM or CommonJS from the
nearest ancestor package.json. This sandbox has one at /home/daytona:

$ grep '"type"' /home/daytona/package.json
  "type": "commonjs",

The agent-CLI fixtures in testdata/preflight are extension-less ESM with
top-level await. Under that inherited "commonjs" they run, exit 0, and print
nothing, so the worker records a null agent output:

# in testdata/preflight, with the inherited "type": "commonjs" in effect
$ ./analyze-story-stub-cli; echo "exit=$?"
exit=0

# the same two files copied to /tmp/pf, where no ancestor manifest applies
$ ./analyze-story-stub-cli; echo "exit=$?"
{"story_title":"stub","relevance_score":5,"reasoning":"stub agent runtime — deterministic output for gate-2 clause-2 demo"}
exit=0

A GitHub runner has no package.json above the workspace. check.sh restores
that by writing a neutral {} manifest immediately outside the repository
($(dirname "$repo_root")/package.json) — never inside it: this repo
deliberately has no root package.json, and adding one would change what CI
checks. With the shim the whole file passes:

$ cd packages/sdk && ./node_modules/.bin/vitest run tests/live-kernel.test.ts
 Test Files  1 passed (1)
      Tests  30 passed | 1 skipped (31)

4. A bug in this branch (fixed and committed)

tests/babysitter-native-extension.test.ts failed as a whole suite:

Error: Hosted capability isolation accepts only the reviewed Software Factory base source.
 ❯ baseAt src/hosted-extension-runtime.ts:239:13

SOFTWARE_FACTORY_SHA256 and the assigned base version in
packages/sdk/src/hosted-extension-runtime.ts are bytes-of-the-flow pins:

$ git show HEAD~1:examples/software-factory/software-factory.flow.ts | sha256sum
49c993220b9c34fab2d4b0e51911656f62b8b657f534d988691960d45bb9d9b6  -

This branch changed that flow and bumped its header to 2.0.23 without moving the
pins, so the hosted loader rejected the base it ships with. Both pins were
updated to the new bytes and version; the commit message records the mutation
check. This is the only committed fix.

What was changed in check.sh (not committed — it is gitignored)

  1. The AppArmor sysctl -w and the bwrap smoke are advisory instead of fatal,
    with a comment naming §1 as the reason.
  2. Bun 1.4.0 is installed and preferred (§2).
  3. The neutral package.json shim above the repository is written if absent (§3).
  4. The SDK suite no longer aborts the script. Sections 2-4 of check.sh are
    independent jobs in CI — one failing job never stops the others — and with
    §1 permanent here, aborting hid three whole workflows' worth of checks behind
    it. The status is remembered and the script still exits 1 at the bottom:
    FAILED: the SDK suite above exited nonzero (sections 2-4 still ran; see their output).

Fixes #571


Summary by cubic

Binds Software Factory draft review verdicts to the exact commit they judged, so a failed review stays final without condemning a superseding head.

Behavior

  • Draft PRs name the reviewed 40-hex head, carry exactly one machine-readable review marker, and state that a new head supersedes the verdict.
  • Missing verification prerequisites end needs_human (exit 3) after opening a NOT VERIFIED draft; defects, contradictory or absent verdicts, and empty unverified markers remain BLOCKED / step_failed (exit 1).
  • Publication rejects malformed heads and duplicate or forged scope markers before push; hook-blocked drafts use the same scope contract.

Follow-on

  • The flow header bumps to 2.0.23, moving the hosted reviewed-base pins in packages/sdk/src/hosted-extension-runtime.ts to the new flow bytes.

Written for commit 5d65052. Summary will update on new commits.

Review in cubic

Relayflow and others added 4 commits September 24, 2026 05:46
The hosted capability loader accepts exactly one Software Factory base: the
bytes of examples/software-factory/software-factory.flow.ts, named by
SOFTWARE_FACTORY_SHA256, with the identity it assigns hard-coded beside it.
This branch changed that flow and bumped its header to 2.0.23 without moving
either pin, so loadHostedExtensionRuntime rejected the base the SDK ships with
and tests/babysitter-native-extension.test.ts could not load at all:

    Error: Hosted capability isolation accepts only the reviewed Software
    Factory base source.
     ❯ baseAt src/hosted-extension-runtime.ts:239:13
     Test Files  1 failed (1)
          Tests  41 skipped (41)

Repin both to the reviewed source:

    $ git show HEAD~1:examples/software-factory/software-factory.flow.ts | sha256sum
    49c993220b9c34fab2d4b0e51911656f62b8b657f534d988691960d45bb9d9b6  -
    $ sha256sum examples/software-factory/software-factory.flow.ts
    ee56899fcb5c0a968d845620db3d4229673a3b732dd4d6131ab43b81822bf97b  examples/software-factory/software-factory.flow.ts

The check itself is untouched and no test was weakened: every other source is
still refused, as the unmodified rejection cases in
tests/hosted-base-snapshot.test.ts show (18 passed). The forged-digest case in
babysitter-native-extension.test.ts keeps its meaning by carrying the new pin —
it poisons Hash.digest to return the *expected* value and asserts the captured
intrinsics are used instead. docs/BABYSITTER-CATALOG-HANDOFF.md now says the
flow bytes and the two pins move together.

After the repin the suite runs: 40 passed, 1 failed — the bubblewrap isolation
case, which cannot start a namespace on this machine for reasons unrelated to
this branch (.relayflow/repair-notes.md).

Mutation-verified, both pins, each restored from a saved byte copy with cmp:

    $ sed -i "s/ee56899…/49c9932…/" src/hosted-extension-runtime.ts
    $ vitest run tests/babysitter-native-extension.test.ts
      Tests  41 skipped (41)          # suite fails to load
    $ cp /tmp/hosted-extension-runtime.ts.fixed src/hosted-extension-runtime.ts
    $ cmp … && echo restored byte-for-byte
    restored byte-for-byte
    $ vitest run tests/babysitter-native-extension.test.ts
      Tests  1 failed | 40 passed (41)

    $ sed -i "246s/'2.0.23'/'2.0.22'/" src/hosted-extension-runtime.ts
    $ vitest run … -t 'composes onto Software Factory'
      - "version": "2.0.23"  + "version": "2.0.22"   # identity test fails
    $ cp /tmp/hosted-extension-runtime.ts.fixed src/hosted-extension-runtime.ts
    $ vitest run … -t 'composes onto Software Factory'
      Tests  1 passed | 40 skipped (41)

Full commands and output are in summary.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The report claimed the verdict-classification mutations were "restored using a
saved byte buffer with equality asserted" but pasted no restore evidence, which
is exactly the shape AGENTS.md rejects. Re-run all of them at this head with
the diff, the failure, the cmp, and the pass captured literally, and add a
fourth mutation for the 40-hex reviewed-head guard, which had no mutation check
before:

  M1  scope guard moved behind the GitHub arm -> 'rejects a forged scope for
      github' fails, 'local' still passes
  M2  classifier's final else BLOCKED -> PASSED -> 'fails closed for empty
      unverified' fails
  M3  count != 1 arm BLOCKED -> PASSED -> 'fails closed for no verdict' fails
  M4  /^[0-9a-f]{40}$/ -> /^.*$/ -> all three 'rejects malformed head' cases fail

Each restored with cmp asserting byte equality and re-run green; the flow file
still hashes to the pinned ee56899…, so the hosted reviewed-base pin is intact.

No production code changed here. The README loses one stray double blank line
in the prose this branch added.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 24, 2026

Copy link
Copy Markdown

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: e5c3eaa3-b7d3-4cad-b8a6-d6970d161430

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A failed-review verdict is permanent, but the branch it judged is not

0 participants