Skip to content

feat(sdk): settle data and code gate contract - #139

Merged
kjgbot merged 12 commits into
mainfrom
feat/v2-gate-contract
Sep 4, 2026
Merged

kjgbot merged 12 commits into
mainfrom
feat/v2-gate-contract

Conversation

@kjgbot

@kjgbot kjgbot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes issue #132 item 6 without adding an expression language or changing kernel vocabulary.

  • classifies named verification data as kernel-evaluated, preflightable, and journal-replayable
  • classifies author callbacks as runtime-only and never serializes the closure
  • exposes the exact named gate plan in human and JSON flows check reports
  • keeps v1 verification: lowering byte-for-byte unchanged
  • settles SURFACE §6 on named checks only

Red-first evidence

Before sdk/src/gate-contract.ts existed:

$ ./node_modules/.bin/vitest run tests/gate-contract.test.ts

 RUN  v2.1.9 /Users/khaliqgant/AgentWorkforce/flows-132-gates-wt/sdk

 ❯ tests/gate-contract.test.ts (0 test)

 FAIL  tests/gate-contract.test.ts [ tests/gate-contract.test.ts ]
Error: Failed to load url ../src/gate-contract.js (resolved id: ../src/gate-contract.js) in /Users/khaliqgant/AgentWorkforce/flows-132-gates-wt/sdk/tests/gate-contract.test.ts. Does the file exist?

 Test Files  1 failed (1)
      Tests  no tests

exit=1

Verification

$ ./node_modules/.bin/vitest run tests/gate-contract.test.ts tests/cli.test.ts tests/preflight.test.ts

 RUN  v2.1.9 /Users/khaliqgant/AgentWorkforce/flows-132-gates-wt/sdk

 ✓ tests/preflight.test.ts (14 tests) 100ms
 ✓ tests/gate-contract.test.ts (7 tests) 175ms
 ✓ tests/cli.test.ts (50 tests) 1964ms

 Test Files  3 passed (3)
      Tests  71 passed (71)
   Duration  7.54s

exit=0
$ ./node_modules/.bin/vitest run --exclude tests/live-kernel.test.ts

 Test Files  17 passed (17)
      Tests  227 passed (227)
   Duration  14.12s

exit=0
$ RELAYFLOWD_BIN="$HOME/.relayflows-toolchain/target/gate-contract/debug/relayflowd" ./node_modules/.bin/vitest run tests/live-kernel.test.ts

 ✓ tests/live-kernel.test.ts (17 tests) 71747ms

 Test Files  1 passed (1)
      Tests  17 passed (17)
   Duration  73.52s

exit=0
$ CARGO_HOME="$HOME/.cargo" RUSTUP_HOME="$HOME/.rustup" CARGO_TARGET_DIR="$HOME/.relayflows-toolchain/target/gate-contract" "$HOME/.cargo/bin/cargo" test -p relayflowd-core

running 26 tests
test result: ok. 26 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.10s

running 5 tests
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s

exit=0
$ ./node_modules/.bin/tsc --noEmit

exit=0
$ node dist/cli.js check ../testdata/hello-deterministic.flow.yaml
WARNING [unprovable_effects] Step "greet" command "echo" resolves, but its effects cannot be proven before execution.
WARNING [unprovable_effects] Step "shout" command "echo" resolves, but its effects cannot be proven before execution.
GATE step "greet" exit_code+output_contains from data (kernel, journal-replayable)
GATE step "shout" exit_code+output_contains from data (kernel, journal-replayable)
CHECK PASSED ../testdata/hello-deterministic.flow.yaml
exit=0

Honest aggregate-run note

The first all-files SDK run had 242 passing tests and one 5-second timeout in the first live-kernel case while files ran in parallel. The complete live-kernel file then passed 17/17 alone as captured above; the remaining files passed 227/227 together. No mutation-verification claim is made.

@coderabbitai

coderabbitai Bot commented Sep 2, 2026 •

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 07a5de76-ad0b-4104-8762-421f86a089b2

📥 Commits

Reviewing files that changed from the base of the PR and between ee28397 and 1ed9023.

⛔ Files ignored due to path filters (1)
  • sdk/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (38)
  • docs/SURFACE.md
  • kernel/DESIGN.md
  • kernel/relayflowd-core/src/lib.rs
  • kernel/relayflowd-core/src/schema.rs
  • kernel/relayflowd-core/src/spec.rs
  • kernel/relayflowd-core/src/state/tests.rs
  • kernel/relayflowd-core/src/verify.rs
  • kernel/relayflowd/src/server.rs
  • kernel/relayflowd/src/server/protocol.rs
  • kernel/relayflowd/tests/invalid_schema_preflight.rs
  • ops/reviews/20260903-pr139-repair-0903.md
  • sdk/package.json
  • sdk/src/canonical.ts
  • sdk/src/cli.ts
  • sdk/src/cli/check.ts
  • sdk/src/compile.ts
  • sdk/src/failure-kinds.ts
  • sdk/src/gate-contract.ts
  • sdk/src/index.ts
  • sdk/src/json-schema-bound.ts
  • sdk/src/json-schema.ts
  • sdk/src/json-value.ts
  • sdk/src/output-schema.ts
  • sdk/src/preflight.ts
  • sdk/src/spec.ts
  • sdk/src/validate.ts
  • sdk/tests/cli.test.ts
  • sdk/tests/dependency-validation.test.ts
  • sdk/tests/gate-contract.test.ts
  • sdk/tests/json-schema-bound.test.ts
  • sdk/tests/preflight.test.ts
  • sdk/tests/spec-parity.test.ts
  • sdk/tests/validate.test.ts
  • sdk/tests/verb-field-lint.test.ts
  • testdata/json-schema-bound-cases.json
  • testdata/json-schema-invalid.flow.yaml
  • testdata/json-schema-invalid.json
  • testdata/json-schema-valid.json

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The change defines a shared gate contract, freezes JSON-compatible inputs, bounds recursive JSON Schema validation, adds preflight gate reporting, and rejects invalid declarations before kernel storage or execution.

Changes

Gate validation and schema-bound verification

Layer / File(s) Summary
SDK gate contracts and input snapshots
sdk/src/{json-value,canonical,spec,validate,compile,gate-contract}.ts, sdk/tests/*
The SDK snapshots inputs, validates gate types by step kind, lowers supported checks, rejects unsafe values, and exposes gate metadata.
Bounded JSON Schema validation
sdk/src/{json-schema,json-schema-bound}.ts, kernel/relayflowd-core/src/schema.rs, testdata/*schema*, sdk/tests/json-schema-bound.test.ts
The SDK and kernel resolve references and reject unbounded same-instance cycles while accepting terminating recursive schemas.
Preflight gate inspection and reporting
sdk/src/{preflight,cli,cli/check,failure-kinds}.ts, sdk/tests/{preflight,cli,dependency-validation,verb-field-lint}.test.ts
Preflight compiles before probes, returns gate plans, reports vacuous schemas, and maps compilation failures to invalid_spec.
Kernel declaration enforcement and replay
kernel/relayflowd-core/src/{spec,verify,state/tests}.rs, kernel/relayflowd/src/{server,server/protocol}.rs, kernel/relayflowd/tests/invalid_schema_preflight.rs
Kernel startup rejects invalid schemas before journal creation, returns invalid_spec, and replays completed data-gate verdicts without rerunning completed steps.
Contract and verification records
docs/*, ops/reviews/*
The gate contract, startup behavior, repair history, corpus coverage, verification results, and verification limits are documented.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Author
  participant SDK
  participant Preflight
  participant Kernel
  participant Journal
  Author->>SDK: submit flow with gates
  SDK->>Preflight: compile and inspect gates
  Preflight-->>SDK: gate plan or invalid_spec
  SDK->>Kernel: send validated specification
  Kernel->>Kernel: validate JSON Schema declaration
  Kernel->>Journal: create journal for valid run
  Kernel-->>SDK: run result or invalid_spec
Loading

Poem

A rabbit checks schemas by moonlight bright
Frozen fields rest in a neat little line
Cycles are bounded and gates name their way
Journals replay what was finished today
“Hop!” says the rabbit, “the checks now align”

Warning

Some tools did not complete. Review the errors below.

🔧 Clippy (1.97.1)

Clippy execution failed


Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing.

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

@kjgbot

kjgbot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Repair evidence for exact pushed head 8880122c3726351b71f0bd8d3b881ff0393db9d5.

The three reviewer-authored ops/reviews/20260902-1845-pr139-*.md files were read and left untouched; they are not part of this commit.

Red before implementation: direct lowering silently accepted callbacks and expressions

Literal command:

cd sdk
./node_modules/.bin/vitest run tests/gate-contract.test.ts --reporter=verbose

Captured output:

 RUN  v2.1.9 /Users/khaliqgant/AgentWorkforce/flows-132-gates-wt/sdk

 ✓ tests/gate-contract.test.ts > data/code gate contract > describes the implicit and explicit checks the kernel will journal
 ✓ tests/gate-contract.test.ts > data/code gate contract > makes the preflightable gate plan visible through flows check
 ✓ tests/gate-contract.test.ts > data/code gate contract > prints the gate plan in the human flows check report
 ✓ tests/gate-contract.test.ts > data/code gate contract > preserves v1 verification at the unchanged kernel boundary
 × tests/gate-contract.test.ts > data/code gate contract > does not admit an expression language into serializable verification
   → expected [Function] to throw an error
 × tests/gate-contract.test.ts > data/code gate contract > rejects author callbacks at both serializable compiler boundaries
   → expected [Function] to throw an error

⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯

 FAIL  tests/gate-contract.test.ts > data/code gate contract > does not admit an expression language into serializable verification
AssertionError: expected [Function] to throw an error

- Expected:
null

+ Received:
undefined

 ❯ tests/gate-contract.test.ts:93:66

 FAIL  tests/gate-contract.test.ts > data/code gate contract > rejects author callbacks at both serializable compiler boundaries
AssertionError: expected [Function] to throw an error

- Expected:
null

+ Received:
undefined

 ❯ tests/gate-contract.test.ts:110:66

 Test Files  1 failed (1)
      Tests  2 failed | 4 passed (6)
   Duration  4.04s

Red before implementation: malformed JSON Schema passed both pre-run validators

Literal SDK command:

cd sdk
./node_modules/.bin/vitest run tests/gate-contract.test.ts --reporter=verbose

Captured output:

 RUN  v2.1.9 /Users/khaliqgant/AgentWorkforce/flows-132-gates-wt/sdk

 ✓ tests/gate-contract.test.ts > data/code gate contract > describes the implicit and explicit checks the kernel will journal
 ✓ tests/gate-contract.test.ts > data/code gate contract > makes the preflightable gate plan visible through flows check
 ✓ tests/gate-contract.test.ts > data/code gate contract > prints the gate plan in the human flows check report
 ✓ tests/gate-contract.test.ts > data/code gate contract > preserves v1 verification at the unchanged kernel boundary
 ✓ tests/gate-contract.test.ts > data/code gate contract > does not admit an expression language into serializable verification
 ✓ tests/gate-contract.test.ts > data/code gate contract > rejects author callbacks at both serializable compiler boundaries
 × tests/gate-contract.test.ts > data/code gate contract > preflights the same valid and invalid JSON Schemas as the kernel
   → expected [Function] to throw an error

⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯

 FAIL  tests/gate-contract.test.ts > data/code gate contract > preflights the same valid and invalid JSON Schemas as the kernel
AssertionError: expected [Function] to throw an error

- Expected:
null

+ Received:
undefined

 ❯ tests/gate-contract.test.ts:132:68

 Test Files  1 failed (1)
      Tests  1 failed | 6 passed (7)
   Duration  2.10s

Literal kernel command:

CARGO_HOME="$HOME/.cargo" RUSTUP_HOME="$HOME/.rustup" CARGO_TARGET_DIR="$HOME/.relayflows-toolchain/target/gate-contract" "$HOME/.cargo/bin/cargo" test --locked --manifest-path kernel/Cargo.toml -p relayflowd-core spec::tests::json_schema_declarations_match_the_shared_preflight_fixtures -- --exact --nocapture

Captured output:

   Compiling relayflowd-core v0.1.0 (/Users/khaliqgant/AgentWorkforce/flows-132-gates-wt/kernel/relayflowd-core)
error[E0599]: no variant named `InvalidJsonSchema` found for enum `spec::SpecError`
   --> relayflowd-core/src/spec/tests.rs:204:24
    |
204 |         Err(SpecError::InvalidJsonSchema { step, .. }) if step == "schema"
    |                        ^^^^^^^^^^^^^^^^^ variant not found in `spec::SpecError`
    |
   ::: relayflowd-core/src/spec.rs:495:1
    |
495 | pub enum SpecError {
    | ------------------ variant `InvalidJsonSchema` not found here

For more information about this error, try `rustc --explain E0599`.
error: could not compile `relayflowd-core` (lib test) due to 1 previous error

Green boundary probe

Literal command:

cd sdk
node --input-type=module <<'EOF'
import * as sdk from './dist/index.js';
const cases = {
  expression: { type: 'expression', expression: 'length < 200' },
  callback: (value) => value.length < 200,
};
console.log('classifyGate_exported=' + ('classifyGate' in sdk));
for (const [name, verification] of Object.entries(cases)) {
  const candidate = { version: '0.1.0', steps: [{ id: name, type: 'deterministic', command: 'printf ok', verification }] };
  for (const boundary of ['compileSpec', 'toKernelSpec']) {
    try {
      sdk[boundary](candidate);
      console.log(`${boundary}_${name}=ACCEPTED`);
    } catch (error) {
      console.log(`${boundary}_${name}=REJECTED ${error.name}: ${error.message.replaceAll('\n', ' | ')}`);
    }
  }
}
EOF

Captured output:

classifyGate_exported=false
compileSpec_expression=REJECTED CompileError: spec compile failed: |   - spec.steps[0].verification.type: expected exit_code | output_contains | json_schema
toKernelSpec_expression=REJECTED CompileError: spec compile failed: |   - spec.steps[0].verification.type: expected exit_code | output_contains | json_schema
compileSpec_callback=REJECTED CompileError: spec compile failed: |   - spec.steps[0].verification: expected an object
toKernelSpec_callback=REJECTED CompileError: spec compile failed: |   - spec.steps[0].verification: expected an object

Green preflight refusal and no-journal/no-command regression

Literal CLI command:

cd sdk
set +e
node dist/cli.js check ../testdata/json-schema-invalid.flow.yaml
task_status=$?
set -e
printf 'check_exit=%s\n' "$task_status"
test "$task_status" -eq 2

Captured output:

REFUSED [invalid_spec] spec.steps[0].verification.schema: invalid JSON Schema: schema is invalid: data/type must be equal to one of the allowed values, data/type must be array, data/type must match a schema in anyOf
check_exit=2

Literal engine command:

CARGO_HOME="$HOME/.cargo" RUSTUP_HOME="$HOME/.rustup" CARGO_TARGET_DIR="$HOME/.relayflows-toolchain/target/gate-contract" "$HOME/.cargo/bin/cargo" test --locked --manifest-path kernel/Cargo.toml -p relayflowd --test invalid_schema_preflight -- --nocapture

Captured output:

   Compiling relayflowd v0.1.0 (/Users/khaliqgant/AgentWorkforce/flows-132-gates-wt/kernel/relayflowd)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 5.00s
     Running tests/invalid_schema_preflight.rs (/Users/khaliqgant/.relayflows-toolchain/target/gate-contract/debug/deps/invalid_schema_preflight-824fb09553015f68)

running 1 test
test invalid_json_schema_is_refused_before_journal_or_command ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s

Exact-head full verification

Literal SDK command:

cd sdk
set -o pipefail
./node_modules/.bin/vitest run --exclude tests/live-kernel.test.ts --silent --reporter=dot 2>&1 | tail -n 12
task_status=${pipestatus[1]}
printf 'vitest_exit=%s\n' "$task_status"
exit "$task_status"

Captured output:

   ✓ built flows binary > refuses through a symlink to the built artifact 1747ms
   ✓ built flows binary > refuses through a symlinked directory component 2619ms
   ✓ built flows binary > classifies a signal-terminated auth probe as probe_failed 1049ms
   ✓ built flows binary > classifies an unavailable PATH resolver as probe_failed 447ms
   ✓ built flows binary > does not describe a present non-executable CLI as missing 416ms
   ✓ built flows binary > runs one auth probe for three steps sharing a flow CLI 551ms

 Test Files  17 passed (17)
      Tests  227 passed (227)
   Start at  19:41:56
   Duration  12.83s (transform 3.86s, setup 0ms, collect 16.61s, tests 28.30s, environment 35ms, prepare 10.87s)

vitest_exit=0

Literal typecheck command:

cd sdk
./node_modules/.bin/tsc --noEmit
task_status=$?
printf 'tsc_exit=%s\n' "$task_status"
exit "$task_status"

Captured output:

tsc_exit=0

Literal full Rust workspace command:

set -o pipefail
CARGO_HOME="$HOME/.cargo" RUSTUP_HOME="$HOME/.rustup" CARGO_TARGET_DIR="$HOME/.relayflows-toolchain/target/gate-contract" "$HOME/.cargo/bin/cargo" test --locked --manifest-path kernel/Cargo.toml --workspace --quiet 2>&1 | tail -n 45
task_status=${pipestatus[1]}
printf 'cargo_exit=%s\n' "$task_status"
exit "$task_status"

Captured output:

running 1 test
.
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.40s

running 1 test
.
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.14s

running 3 tests
...
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.20s

running 27 tests
...........................
test result: ok. 27 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.26s

running 5 tests
.....
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.14s

running 17 tests
.................
test result: ok. 17 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.05s

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

cargo_exit=0

The first default-timeout live run was not green and is not counted as a pass. Literal command:

cd sdk
RELAYFLOWD_BIN="$HOME/.relayflows-toolchain/target/gate-contract/debug/relayflowd" ./node_modules/.bin/vitest run tests/live-kernel.test.ts --reporter=dot

Captured failure output:

 ❯ tests/live-kernel.test.ts (17 tests | 2 failed) 90429ms
   × built flows CLI against live relayflowd > runs rung (a), parks rung (b), and keeps JSON report-shaped 5153ms
     → Test timed out in 5000ms.
   × built flows CLI against live relayflowd > follows a live worker dispatch through flows run 2478ms
     → FAILED [protocol_error] relayflowd could not complete the run request: relayflowd returned status parked without a classifiable completion
   ✓ built flows CLI against live relayflowd > runs an agent CLI end to end through the SDK worker 375ms
   ✓ built flows CLI against live relayflowd > can always get a parked run to a late-attaching worker 6017ms
   ✓ built flows CLI against live relayflowd > reports a real manual-recovery NeedsHuman state as parked 1221ms
   ✓ built flows CLI against live relayflowd > runs hn-monitor analyze-story end-to-end via a stub agent CLI (gate 2 clause 2 demo) 727ms
   ✓ built flows CLI against live relayflowd > hn-monitor analyze-story FAILS verification when the CLI omits required schema fields 648ms
   ✓ built flows CLI against live relayflowd > AgentWorker exposes wake_context to the CLI via RELAYFLOW_WAKE_CONTEXT env var (real analyzer prerequisite) 1138ms
   ✓ built flows CLI against live relayflowd > AgentWorker leaves RELAYFLOW_WAKE_CONTEXT UNSET when the run has no wake_context (undefined-vs-null pin) 734ms
   ✓ built flows CLI against live relayflowd > AgentWorker passes a declared model to the CLI as RELAYFLOW_MODEL 660ms
   ✓ built flows CLI against live relayflowd > AgentWorker leaves RELAYFLOW_MODEL UNSET when the step declares no model 1748ms
   ✓ built flows CLI against live relayflowd > hn-monitor analyze-story reaches done through the real Claude analyzer CLI 30693ms
   ✓ built flows CLI against live relayflowd > preflights before journaling and names an unreachable socket 2207ms
   ✓ JournalClient wire conformance against live relayflowd > exercises every protocol-v0 verb with the real server 323ms
   ✓ surface resume after a real daemon kill > resumes a three-step run with each successful completion exactly once 3016ms

 Test Files  1 failed (1)
      Tests  2 failed | 15 passed (17)
   Duration  93.24s

Both cases passed individually, and the full suite then passed with an explicit 60-second per-test ceiling. Literal full rerun command:

cd sdk
RELAYFLOWD_BIN="$HOME/.relayflows-toolchain/target/gate-contract/debug/relayflowd" ./node_modules/.bin/vitest run tests/live-kernel.test.ts --reporter=dot --testTimeout=60000

Captured output:

 ✓ tests/live-kernel.test.ts (17 tests) 67731ms
   ✓ built flows CLI against live relayflowd > runs rung (a), parks rung (b), and keeps JSON report-shaped 4742ms
   ✓ built flows CLI against live relayflowd > allows a deterministic run to exceed the bounded request timeout 33059ms
   ✓ built flows CLI against live relayflowd > follows a live worker dispatch through flows run 1006ms
   ✓ built flows CLI against live relayflowd > can always get a parked run to a late-attaching worker 5748ms
   ✓ built flows CLI against live relayflowd > reports a real manual-recovery NeedsHuman state as parked 1756ms
   ✓ built flows CLI against live relayflowd > runs hn-monitor analyze-story end-to-end via a stub agent CLI (gate 2 clause 2 demo) 569ms
   ✓ built flows CLI against live relayflowd > hn-monitor analyze-story FAILS verification when the CLI omits required schema fields 476ms
   ✓ built flows CLI against live relayflowd > AgentWorker exposes wake_context to the CLI via RELAYFLOW_WAKE_CONTEXT env var (real analyzer prerequisite) 967ms
   ✓ built flows CLI against live relayflowd > AgentWorker leaves RELAYFLOW_WAKE_CONTEXT UNSET when the run has no wake_context (undefined-vs-null pin) 563ms
   ✓ built flows CLI against live relayflowd > AgentWorker passes a declared model to the CLI as RELAYFLOW_MODEL 435ms
   ✓ built flows CLI against live relayflowd > hn-monitor analyze-story reaches done through the real Claude analyzer CLI 14621ms
   ✓ built flows CLI against live relayflowd > preflights before journaling and names an unreachable socket 1242ms
   ✓ JournalClient wire conformance against live relayflowd > exercises every protocol-v0 verb with the real server 337ms
   ✓ surface resume after a real daemon kill > resumes a three-step run with each successful completion exactly once 1526ms

 Test Files  1 passed (1)
      Tests  17 passed (17)
   Start at  19:37:29
   Duration  71.11s (transform 742ms, setup 0ms, collect 1.65s, tests 67.73s, environment 2ms, prepare 418ms)

Literal diff command:

git diff --check HEAD~1..HEAD
task_status=$?
printf 'diff_check_exit=%s\n' "$task_status"
exit "$task_status"

Captured output:

diff_check_exit=0

@kjgbot

kjgbot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up artifact repair at 688d6a0.

The first pushed repair was not DONE because the hosted exact-artifact smoke failed. Literal inspection command:

gh run view 33662542959 --job 100356314891 --log-failed

Captured failing output:

linux-x64-artifact Smoke exact Linux artifact 2026-09-02T17:43:36.2965981Z error: Cannot find module 'ajv/dist/refs/json-schema-draft-06.json' from '/$bunfs/root/flows'
linux-x64-artifact Smoke exact Linux artifact 2026-09-02T17:43:36.2966997Z
linux-x64-artifact Smoke exact Linux artifact 2026-09-02T17:43:36.2967353Z Bun v1.4.0 (Linux x64)
linux-x64-artifact Smoke exact Linux artifact 2026-09-02T17:43:36.2998906Z ##[error]Process completed with exit code 1.

The dynamic createRequire path prevented Bun's standalone compiler from seeing the draft-06 metaschema. The repair makes it a static JSON import, which the standalone artifact bundles.

Literal local standalone-artifact command (same Bun 1.4.0 compiler, host target substituted for Linux):

cd sdk
./node_modules/.bin/tsc --noEmit
task_tmp_dir=$(mktemp -d)
bun build src/cli-executable.ts --compile --target=bun-darwin-arm64 --outfile="$task_tmp_dir/flows"
"$task_tmp_dir/flows" check --json ../testdata/hello-deterministic.flow.yaml
printf 'artifact_smoke_dir=%s\n' "$task_tmp_dir"

Captured output:

  [89ms]  bundle  196 modules
 [265ms] compile  /var/folders/yv/nbp9l2c55wlbj1x0gml37s7c0000gn/T/tmp.4t4tl9ZWFi/flows
WARNING [unprovable_effects] Step "greet" command "echo" resolves, but its effects cannot be proven before execution.
WARNING [unprovable_effects] Step "shout" command "echo" resolves, but its effects cannot be proven before execution.
{"ok":true,"path":"../testdata/hello-deterministic.flow.yaml","projectConfigPath":"/Users/khaliqgant/AgentWorkforce/flows-132-gates-wt/testdata/flows.json","gates":[{"stepId":"greet","kind":"data","checks":["exit_code","output_contains"],"evaluator":"kernel","preflightable":true,"replayable":true},{"stepId":"shout","kind":"data","checks":["exit_code","output_contains"],"evaluator":"kernel","preflightable":true,"replayable":true}],"resolutions":[],"diagnostics":[{"severity":"warning","kind":"unprovable_effects","stepId":"greet","message":"Step \"greet\" command \"echo\" resolves, but its effects cannot be proven before execution."},{"severity":"warning","kind":"unprovable_effects","stepId":"shout","message":"Step \"shout\" command \"echo\" resolves, but its effects cannot be proven before execution."}]}
artifact_smoke_dir=/var/folders/yv/nbp9l2c55wlbj1x0gml37s7c0000gn/T/tmp.4t4tl9ZWFi

Hosted Linux check is pending on 688d6a0; this comment does not claim it has passed.

@kjgbot

kjgbot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Hosted exact-head artifact is green after the static metaschema import repair.

Literal command:

gh pr checks 139 --watch --interval 10

Final captured output:

CodeRabbit          pass  0       Review rate limited
linux-x64-artifact  pass  3m20s   https://github.com/AgentWorkforce/flows/actions/runs/33663067781/job/100358045393
CodeRabbit          pass  0       Review rate limited
linux-x64-artifact  pass  3m20s   https://github.com/AgentWorkforce/flows/actions/runs/33663067781/job/100358045393

@kjgbot

kjgbot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Repair evidence for exact head 11ad3e2da7f473fefe1e9e0b44b599890b9de073.

The repair closes four public-boundary holes without adding a gate language: exit_code is deterministic-only, author input is copied into frozen behavior-free JSON before validation/lowering, boolean schemas match the kernel, and exported preflight() compiles before probing or inspecting. inspectStepGate is no longer a public raw-input API.

Red before implementation

$ cd sdk && ./node_modules/.bin/vitest run tests/gate-contract.test.ts tests/preflight.test.ts --reporter=verbose --maxWorkers=1 --minWorkers=1

 RUN  v2.1.9 /Users/khaliqgant/AgentWorkforce/flows-132-gates-wt/sdk

 × tests/preflight.test.ts > preflight: CLI resolution and refusal predicates > validates raw public input before any probe or gate inspection
   → expected [Function] to throw an error
 × tests/gate-contract.test.ts > data/code gate contract > rejects explicit exit_code gates the kernel cannot apply to llm or agent steps
   → expected [Function] to throw an error
 × tests/gate-contract.test.ts > data/code gate contract > snapshots and freezes schema data before returning a compiled or kernel spec
   → expected { type: 'number' } to deeply equal { type: 'string' }
 × tests/gate-contract.test.ts > data/code gate contract > rejects behavioral and non-JSON values inside schema declarations
   → expected [Function] to throw an error
 × tests/gate-contract.test.ts > data/code gate contract > accepts both boolean JSON Schemas exactly as the kernel does
   → spec compile failed:
  - spec.steps[0].verification.schema: expected a JSON Schema object

 Test Files  2 failed (2)
      Tests  5 failed | 21 passed (26)
   Duration  4.42s

Focused green

$ cd sdk && ./node_modules/.bin/vitest run tests/gate-contract.test.ts tests/preflight.test.ts --reporter=dot --maxWorkers=1 --minWorkers=1 && ./node_modules/.bin/tsc --noEmit

 RUN  v2.1.9 /Users/khaliqgant/AgentWorkforce/flows-132-gates-wt/sdk

 ✓ tests/preflight.test.ts (15 tests) 185ms
 ✓ tests/gate-contract.test.ts (11 tests) 490ms

 Test Files  2 passed (2)
      Tests  26 passed (26)
   Duration  6.73s

The schema behavior test also asserts accessorReads === 0 and toJsonCalls === 0; the tests reject nested functions, bigint, accessors, cycles, and toJSON at both compileSpec and direct toKernelSpec.

Full SDK and live protocol

$ cd sdk && ./node_modules/.bin/vitest run --exclude tests/live-kernel.test.ts --silent --reporter=dot --maxWorkers=1 --minWorkers=1

 Test Files  17 passed (17)
      Tests  232 passed (232)
   Duration  99.28s
$ cd sdk && RELAYFLOWD_BIN="$HOME/.relayflows-toolchain/target/gate-contract/debug/relayflowd" ./node_modules/.bin/vitest run tests/live-kernel.test.ts --reporter=dot --maxWorkers=1 --minWorkers=1 --testTimeout=60000

 ✓ tests/live-kernel.test.ts (17 tests) 89590ms
 Test Files  1 passed (1)
      Tests  17 passed (17)
   Duration  108.55s

Full kernel

$ CARGO_HOME="$HOME/.cargo" RUSTUP_HOME="$HOME/.rustup" CARGO_TARGET_DIR="$HOME/.relayflows-toolchain/target/gate-contract" "$HOME/.cargo/bin/cargo" test --locked --manifest-path kernel/Cargo.toml --workspace

running 22 tests
test result: ok. 22 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
running 19 tests
test result: ok. 19 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
running 1 test
test invalid_json_schema_is_refused_before_journal_or_command ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
running 27 tests
test schema::tests::shared_declarations_and_boolean_schemas_are_validated ... ok
test result: ok. 27 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
running 5 tests
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
running 17 tests
test result: ok. 17 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Built and packed public boundaries

$ cd sdk && ./node_modules/.bin/tsc --noEmit && ./node_modules/.bin/tsc && node scripts/make-cli-executable.mjs && node --input-type=module <public-boundary-probe>
{"refusal":true,"probes":0,"booleanSchema":false}
$ cd sdk && bun pm pack --ignore-scripts --destination "$consumer_root" --quiet && bun add "$tarball" --ignore-scripts && node --input-type=module -e <three-invalid-public-preflight-probes>
callback=refused
expression=refused
malformed_schema=refused
{"refused":3,"probes":0}
packed_consumer=/tmp/pr139-packed-consumer.JFCHYO/consumer
$ artifact_dir="$(mktemp -d /tmp/pr139-gates-artifact.XXXXXX)" && bun build sdk/src/cli-executable.ts --compile --target=bun-darwin-arm64 --outfile="$artifact_dir/flows" && "$artifact_dir/flows" check --json testdata/hello-deterministic.flow.yaml
 [108ms]  bundle  197 modules
 [448ms] compile  /tmp/pr139-gates-artifact.1gp9if/flows
{"ok":true,"path":"testdata/hello-deterministic.flow.yaml","projectConfigPath":"/Users/khaliqgant/AgentWorkforce/flows-132-gates-wt/testdata/flows.json","gates":[{"stepId":"greet","kind":"data","checks":["exit_code","output_contains"],"evaluator":"kernel","preflightable":true,"replayable":true},{"stepId":"shout","kind":"data","checks":["exit_code","output_contains"],"evaluator":"kernel","preflightable":true,"replayable":true}],"resolutions":[],"diagnostics":[{"severity":"warning","kind":"unprovable_effects","stepId":"greet","message":"Step \"greet\" command \"echo\" resolves, but its effects cannot be proven before execution."},{"severity":"warning","kind":"unprovable_effects","stepId":"shout","message":"Step \"shout\" command \"echo\" resolves, but its effects cannot be proven before execution."}]}

git diff --check emitted no output and exited 0. The six reviewer-owned ops/reviews/20260902-{1845,1945}-pr139-* files remain staged locally and were deliberately excluded from commit 11ad3e2; they were not edited.

@kjgbot

kjgbot commented Sep 2, 2026 •

Copy link
Copy Markdown
Contributor Author

Evidence correction: the two angle-bracket labels in my preceding comment abbreviated inline scripts, so those two command lines were not literal. The literal captured commands and outputs were:

$ cd sdk && ./node_modules/.bin/tsc --noEmit && ./node_modules/.bin/tsc && node scripts/make-cli-executable.mjs && node --input-type=module <<'NODE'
import { preflight, toKernelSpec } from './dist/index.js';
let probes = 0;
const probeSet = {
  command: () => { probes += 1; return true; },
  cli: () => { probes += 1; return { exists: true, authenticated: true }; },
  trigger: () => { probes += 1; return true; },
};
const invalid = {
  version: '0.1.0',
  steps: [{ id: 'raw', type: 'deterministic', command: 'printf ok', verification: { type: 'expression', expression: 'true' } }],
};
let refusal;
try { preflight(invalid, { probes: probeSet }); } catch (error) { refusal = error.message; }
const booleanSchema = toKernelSpec({
  version: '0.1.0',
  steps: [{ id: 'answer', type: 'llm', prompt: 'answer', verification: { type: 'json_schema', schema: false } }],
}).steps[0].verification.json_schema;
console.log(JSON.stringify({ refusal: /expected exit_code \| output_contains \| json_schema/.test(refusal), probes, booleanSchema }));
if (!refusal || probes !== 0 || booleanSchema !== false) process.exit(1);
NODE
{"refusal":true,"probes":0,"booleanSchema":false}
$ cd sdk && consumer_root="$(mktemp -d /tmp/pr139-packed-consumer.XXXXXX)" && bun pm pack --ignore-scripts --destination "$consumer_root" --quiet && tarball="$(find "$consumer_root" -maxdepth 1 -name '*.tgz' -type f -print -quit)" && mkdir "$consumer_root/consumer" && cd "$consumer_root/consumer" && bun init -y >/dev/null && bun add "$tarball" --ignore-scripts >/dev/null && node --input-type=module -e "import { preflight } from '@relayflows/sdk'; let probes=0; const p={command:()=>{probes++;return true},cli:()=>{probes++;return {exists:true,authenticated:true}},executor:()=>{probes++;return true}}; const checks={callback:v=>v,expression:{type:'expression',expression:'true'},malformed_schema:{type:'json_schema',schema:{type:'not-a-real-type'}}}; let refused=0; for(const [name,verification] of Object.entries(checks)){try{preflight({version:'0.1.0',steps:[{id:'s',type:'deterministic',command:'printf ok',verification}]},{probes:p})}catch{refused++;console.log(name+'=refused')}} console.log(JSON.stringify({refused,probes})); if(refused!==3||probes!==0)process.exit(1)" && printf 'packed_consumer=%s\n' "$consumer_root/consumer"
/tmp/pr139-packed-consumer.JFCHYO/relayflows-sdk-0.1.0.tgz
Resolving dependencies
Resolved, downloaded and extracted [4]
Saved lockfile
Resolving dependencies
Resolved, downloaded and extracted [11]
Saved lockfile
callback=refused
expression=refused
malformed_schema=refused
{"refused":3,"probes":0}
packed_consumer=/tmp/pr139-packed-consumer.JFCHYO/consumer

@kjgbot

kjgbot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Exact-head hosted artifact gate completed after the repair push:

$ gh run watch 33669268034 --interval 10 --exit-status
✓ feat/v2-gate-contract Relayflow v2 Cloud runtime artifact AgentWorkforce/flows#139 · 33669268034
Triggered via pull_request about 3 minutes ago

JOBS
✓ linux-x64-artifact in 2m58s (ID 100378534364)
  ✓ Set up job
  ✓ Run actions/checkout@v4
  ✓ Run actions/setup-node@v4
  ✓ Run oven-sh/setup-bun@v2
  ✓ Run dtolnay/rust-toolchain@stable
  ✓ Test artifact contract
  ✓ Build relayflowd
  ✓ Build standalone flows CLI
  ✓ Assemble artifact and smoke verifier path
  ✓ Smoke exact Linux artifact
  ✓ Run actions/upload-artifact@v4
  ✓ Complete job

Run: https://github.com/AgentWorkforce/flows/actions/runs/33669268034
Head: 11ad3e2da7f473fefe1e9e0b44b599890b9de073.

@kjgbot

kjgbot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Fresh REVIEW_FAILED repair evidence for exact pushed head f5b8437b41e32d7ba45bb96eecf9bf8eb2aee65a.

Product changes:

  • reject Proxy values through Node/Bun's trap-free proxy brand check before prototype/key/descriptor reflection;
  • omit explicitly undefined object properties, matching JSON, exactOptionalPropertyTypes: false, validateSpec, and v1 compilation while retaining strict array handling;
  • preserve the typed SpecError cause from Engine::start and map semantic run.start validation failures to protocol invalid_spec;
  • replace the in-process invalid-schema test with a real Unix-socket hello → run.start test asserting no command, registry, or runs directory.

Literal red evidence before product changes

$ cd sdk && ./node_modules/.bin/vitest run tests/gate-contract.test.ts --reporter=verbose --maxWorkers=1 --minWorkers=1

 × tests/gate-contract.test.ts > data/code gate contract > rejects proxy schemas without executing traps at any public data boundary
   → expected [Function] to throw an error
 × tests/gate-contract.test.ts > data/code gate contract > omits explicit undefined object properties accepted by the public types and validator
   → spec compile failed:
  - spec.description: expected JSON-compatible data; undefined values are not allowed

 Test Files  1 failed (1)
      Tests  2 failed | 11 passed (13)
   Duration  1.42s
$ CARGO_HOME="$HOME/.cargo" RUSTUP_HOME="$HOME/.rustup" CARGO_TARGET_DIR="$HOME/.relayflows-toolchain/target/gate-contract" "$HOME/.cargo/bin/cargo" test --locked --manifest-path kernel/Cargo.toml -p relayflowd --test invalid_schema_preflight -- --exact --nocapture

thread 'invalid_json_schema_is_refused_before_journal_or_command' panicked at relayflowd/tests/invalid_schema_preflight.rs:60:5:
assertion `left == right` failed
  left: String("internal")
 right: "invalid_spec"
test invalid_json_schema_is_refused_before_journal_or_command ... FAILED

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

Literal focused green and typecheck

$ cd sdk && ./node_modules/.bin/vitest run tests/gate-contract.test.ts tests/preflight.test.ts --reporter=dot --maxWorkers=1 --minWorkers=1 && ./node_modules/.bin/tsc --noEmit && cd .. && CARGO_HOME="$HOME/.cargo" RUSTUP_HOME="$HOME/.rustup" CARGO_TARGET_DIR="$HOME/.relayflows-toolchain/target/gate-contract" "$HOME/.cargo/bin/cargo" test --locked --manifest-path kernel/Cargo.toml -p relayflowd --test invalid_schema_preflight -- --exact --nocapture && printf 'focused_and_typecheck_exit=0\n'

 ✓ tests/preflight.test.ts (15 tests) 2137ms
 ✓ tests/gate-contract.test.ts (13 tests) 520ms

 Test Files  2 passed (2)
      Tests  28 passed (28)

running 1 test
test invalid_json_schema_is_refused_before_journal_or_command ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

focused_and_typecheck_exit=0
$ "$HOME/.rustup/toolchains/stable-aarch64-apple-darwin/bin/rustfmt" --edition 2024 --config skip_children=true --check kernel/relayflowd/src/server.rs kernel/relayflowd/tests/invalid_schema_preflight.rs; task_status=$?; printf 'targeted_rustfmt_exit=%s\n' "$task_status"; exit "$task_status"
targeted_rustfmt_exit=0

Literal full SDK, kernel, and live protocol results

$ cd sdk && ./node_modules/.bin/vitest run --exclude tests/live-kernel.test.ts --silent --reporter=dot --maxWorkers=1 --minWorkers=1

 Test Files  17 passed (17)
      Tests  234 passed (234)
   Duration  42.88s
$ CARGO_HOME="$HOME/.cargo" RUSTUP_HOME="$HOME/.rustup" CARGO_TARGET_DIR="$HOME/.relayflows-toolchain/target/gate-contract" "$HOME/.cargo/bin/cargo" test --locked --manifest-path kernel/Cargo.toml --workspace

running 22 tests
test result: ok. 22 passed; 0 failed
running 19 tests
test result: ok. 19 passed; 0 failed
running 1 test
test invalid_json_schema_is_refused_before_journal_or_command ... ok
test result: ok. 1 passed; 0 failed
running 27 tests
test result: ok. 27 passed; 0 failed
running 5 tests
test result: ok. 5 passed; 0 failed
running 17 tests
test result: ok. 17 passed; 0 failed
$ cd sdk && RELAYFLOWD_BIN="$HOME/.relayflows-toolchain/target/gate-contract/debug/relayflowd" ./node_modules/.bin/vitest run tests/live-kernel.test.ts --reporter=dot --maxWorkers=1 --minWorkers=1 --testTimeout=60000

 ✓ tests/live-kernel.test.ts (17 tests) 64946ms
 Test Files  1 passed (1)
      Tests  17 passed (17)
   Duration  68.16s

Literal real-socket result

The focused Rust test above is itself a spawned-daemon Unix-socket integration test. An independent SDK JournalClient probe against the rebuilt daemon also captured:

protocol_code=invalid_spec
marker_exists=no
registry_exists=no
runs_dir_exists=no
node_exit=0
data_dir=/tmp/pr139-invalid-spec-socket.cw0wrp

Literal installed-package and Bun runtime results

The installed consumer was created from /tmp/pr139-proxy-package.FpYJ6G/relayflows-sdk-0.1.0.tgz, produced after rebuilding dist at this tree.

$ cd /tmp/pr139-proxy-package.FpYJ6G/consumer && node --input-type=module -e "import {compileSpec,toKernelSpec,validateSpec} from '@relayflows/sdk';let traps=0;const schema=new Proxy({type:'string'},{getPrototypeOf(t){traps++;return Reflect.getPrototypeOf(t)}});let proxy='accepted';try{toKernelSpec({version:'0.1.0',steps:[{id:'s',type:'deterministic',command:'true',verification:{type:'json_schema',schema}}]})}catch(e){proxy=/proxy/i.test(e.message)?'refused':'wrong-error'}const flow={version:'0.1.0',description:undefined,steps:[{id:'s',type:'deterministic',command:'true',verification:undefined}]};const compiled=compileSpec(flow);console.log(JSON.stringify({proxy,traps,valid:validateSpec(flow).ok,undefinedOmitted:!Object.hasOwn(compiled,'description')}));if(proxy!=='refused'||traps!==0||!validateSpec(flow).ok||Object.hasOwn(compiled,'description'))process.exit(1)"
{"proxy":"refused","traps":0,"valid":true,"undefinedOmitted":true}
$ cd sdk && bun -e 'import { toKernelSpec } from "./dist/index.js"; let traps=0; const schema=new Proxy({type:"string"},{getPrototypeOf(t){traps++;return Reflect.getPrototypeOf(t)},ownKeys(t){traps++;return Reflect.ownKeys(t)},getOwnPropertyDescriptor(t,k){traps++;return Reflect.getOwnPropertyDescriptor(t,k)}}); let rejected=false; try{toKernelSpec({version:"0.1.0",steps:[{id:"s",type:"deterministic",command:"true",verification:{type:"json_schema",schema}}]})}catch(error){rejected=/proxy/i.test(error.message)} console.log(JSON.stringify({rejected,traps})); if(!rejected||traps!==0)process.exit(1)'
{"rejected":true,"traps":0}
$ artifact_dir="$(mktemp -d /tmp/pr139-proxy-artifact.XXXXXX)" && bun build src/cli-executable.ts --compile --target=bun-darwin-arm64 --outfile="$artifact_dir/flows" && "$artifact_dir/flows" check --json ../testdata/hello-deterministic.flow.yaml
  [61ms]  bundle  197 modules
 [294ms] compile  /tmp/pr139-proxy-artifact.SQlVAe/flows
{"ok":true,"path":"../testdata/hello-deterministic.flow.yaml","projectConfigPath":"/Users/khaliqgant/AgentWorkforce/flows-132-gates-wt/testdata/flows.json","gates":[{"stepId":"greet","kind":"data","checks":["exit_code","output_contains"],"evaluator":"kernel","preflightable":true,"replayable":true},{"stepId":"shout","kind":"data","checks":["exit_code","output_contains"],"evaluator":"kernel","preflightable":true,"replayable":true}],"resolutions":[],"diagnostics":[{"severity":"warning","kind":"unprovable_effects","stepId":"greet","message":"Step \"greet\" command \"echo\" resolves, but its effects cannot be proven before execution."},{"severity":"warning","kind":"unprovable_effects","stepId":"shout","message":"Step \"shout\" command \"echo\" resolves, but its effects cannot be proven before execution."}]}

git diff --check emitted no output and exited 0. Reviewer-owned ops/reviews files remain staged locally, untouched, and excluded from commit f5b8437.

@kjgbot

kjgbot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Exact-head hosted artifact result:

$ gh run watch 33673407803 --interval 10 --exit-status
✓ feat/v2-gate-contract Relayflow v2 Cloud runtime artifact AgentWorkforce/flows#139 · 33673407803
Triggered via pull_request about 3 minutes ago

JOBS
✓ linux-x64-artifact in 2m34s (ID 100392210141)
  ✓ Set up job
  ✓ Run actions/checkout@v4
  ✓ Run actions/setup-node@v4
  ✓ Run oven-sh/setup-bun@v2
  ✓ Run dtolnay/rust-toolchain@stable
  ✓ Test artifact contract
  ✓ Build relayflowd
  ✓ Build standalone flows CLI
  ✓ Assemble artifact and smoke verifier path
  ✓ Smoke exact Linux artifact
  ✓ Run actions/upload-artifact@v4
  ✓ Complete job

Run: https://github.com/AgentWorkforce/flows/actions/runs/33673407803
Head: f5b8437b41e32d7ba45bb96eecf9bf8eb2aee65a.

@kjgbot

kjgbot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Hostile-Proxy boundary repair evidence for exact head e6210a2fc666df6dc8c777c009712ddf99efa877 (parent f5b8437b41e32d7ba45bb96eecf9bf8eb2aee65a).

The exported unknown-input boundaries now snapshot/reject before reflection:

  • validateSpec catches snapshot refusal and returns { ok: false, errors } without throwing.
  • kernelToAuthoring snapshots before reverse lowering and translates snapshot refusal to CompileError.
  • Both tests import through the public src/index.js export; reverse lowering uses a nested Proxy-wrapped steps array.

RED at f5b8437:

$ cd sdk && ./node_modules/.bin/vitest run tests/validate.test.ts tests/spec-parity.test.ts --reporter=verbose --maxWorkers=1 --minWorkers=1

 × tests/validate.test.ts > validate: rejects malformed specs > returns a failure for proxy input without executing traps or throwing
   → expected true to be false // Object.is equality
 × tests/spec-parity.test.ts > spec parity: one dialect at the SDK<->kernel boundary > refuses nested proxy data before executing any trap
   → expected [Function] to throw an error

 Test Files  2 failed (2)
      Tests  2 failed | 51 passed (53)
   Duration  2.25s

Built-dist behavior before the repair:

validate={"ok":true,"errors":[]}
validate_traps=13
authoring={"version":"0.1.0","steps":[{"id":"s","type":"deterministic","maxIterations":1,"verification":{"type":"json_schema","schema":{"type":"string"}},"command":"true"}]}
authoring_traps=4

Focused GREEN:

$ cd sdk && ./node_modules/.bin/vitest run tests/validate.test.ts tests/spec-parity.test.ts tests/gate-contract.test.ts tests/preflight.test.ts --reporter=verbose --maxWorkers=1 --minWorkers=1 && ./node_modules/.bin/tsc --noEmit
...
 Test Files  4 passed (4)
      Tests  81 passed (81)
   Duration  7.11s

Final-tree public boundary rerun:

$ cd sdk && ./node_modules/.bin/vitest run tests/validate.test.ts tests/spec-parity.test.ts --reporter=dot --maxWorkers=1 --minWorkers=1 && ./node_modules/.bin/tsc --noEmit

 ✓ tests/validate.test.ts (37 tests) 56ms
 ✓ tests/spec-parity.test.ts (16 tests) 1317ms
 Test Files  2 passed (2)
      Tests  53 passed (53)
 Duration 3.98s

Full SDK:

$ cd sdk && ./node_modules/.bin/vitest run --exclude tests/live-kernel.test.ts --silent --reporter=dot --maxWorkers=1 --minWorkers=1
...
 Test Files  17 passed (17)
      Tests  236 passed (236)
   Duration  18.63s

Typecheck/build:

$ cd sdk && ./node_modules/.bin/tsc --noEmit && ./node_modules/.bin/tsc && node scripts/make-cli-executable.mjs && printf 'sdk_build_exit=0\n'
sdk_build_exit=0

Node built-dist GREEN:

validate={"ok":false,"errors":["spec: expected JSON-compatible data; Proxy objects are not allowed"]}
validate_traps=0
authoring=refused_proxy
authoring_traps=0

Bun built-dist GREEN:

$ cd sdk && bun -e 'import {compileSpec,kernelToAuthoring,toKernelSpec,validateSpec} from "./dist/index.js";let traps=0;const raw=new Proxy({version:"0.1.0",steps:[{id:"s",type:"deterministic",command:"true"}]},{get(t,k,r){traps++;return Reflect.get(t,k,r)},ownKeys(t){traps++;return Reflect.ownKeys(t)},getOwnPropertyDescriptor(t,k){traps++;return Reflect.getOwnPropertyDescriptor(t,k)}});const validation=validateSpec(raw);const kernel=toKernelSpec(compileSpec({version:"0.1.0",steps:[{id:"s",type:"deterministic",command:"true"}]}));kernel.steps[0].verification.json_schema=new Proxy({type:"string"},{ownKeys(t){traps++;return Reflect.ownKeys(t)}});let reverse="accepted";try{kernelToAuthoring(kernel)}catch(e){reverse=/proxy/i.test(e.message)?"refused_proxy":"wrong_error"}console.log(JSON.stringify({validation,traps,reverse}));if(validation.ok||traps!==0||reverse!=="refused_proxy")process.exit(1)'
{"validation":{"ok":false,"errors":["spec: expected JSON-compatible data; Proxy objects are not allowed"]},"traps":0,"reverse":"refused_proxy"}

Packed-package Node + Bun GREEN (bun pm pack --ignore-scripts, then consumer install):

/tmp/pr139-hostile-package.XF08m7/relayflows-sdk-0.1.0.tgz
Resolving dependencies
Resolved, downloaded and extracted [4]
Saved lockfile
Resolving dependencies
Resolved, downloaded and extracted [11]
Saved lockfile
{"validation":{"ok":false,"errors":["spec: expected JSON-compatible data; Proxy objects are not allowed"]},"traps":0,"reverse":"refused_proxy"}
{"validation":{"ok":false,"errors":["spec: expected JSON-compatible data; Proxy objects are not allowed"]},"traps":0,"reverse":"refused_proxy"}
packed_consumer=/tmp/pr139-hostile-package.XF08m7/consumer

Standalone Bun artifact:

$ cd sdk && artifact_dir="$(mktemp -d /tmp/pr139-hostile-artifact.XXXXXX)" && bun build src/cli-executable.ts --compile --target=bun-darwin-arm64 --outfile="$artifact_dir/flows" && "$artifact_dir/flows" check --json ../testdata/hello-deterministic.flow.yaml && printf 'artifact_path=%s\n' "$artifact_dir/flows"
  [99ms]  bundle  197 modules
 [521ms] compile  /tmp/pr139-hostile-artifact.Nq0422/flows
{"ok":true,"errors":[],"spec":{"version":"0.1.0","steps":[{"id":"hello","type":"deterministic","maxIterations":1,"verification":{"type":"exit_code","code":0},"command":"printf 'hello\\n'"}]},"kernelSpec":{"version":1,"steps":[{"id":"hello","type":"deterministic","max_iterations":1,"verification":{"exit_code":0},"command":"printf 'hello\\n'"}]},"gates":[{"stepId":"hello","kind":"exit_code","preflightable":true,"replayable":true,"runtimeCode":false}]}
artifact_path=/tmp/pr139-hostile-artifact.Nq0422/flows

Real-socket no-effect proof:

$ CARGO_HOME="$HOME/.cargo" RUSTUP_HOME="$HOME/.rustup" CARGO_TARGET_DIR="$HOME/.relayflows-toolchain/target/gate-contract" "$HOME/.cargo/bin/cargo" test --locked --manifest-path kernel/Cargo.toml -p relayflowd --test invalid_schema_preflight -- --exact --nocapture
...
running 1 test
test invalid_json_schema_is_refused_before_journal_or_command ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

The test launches the real relayflowd, connects over a Unix socket, performs hello + malformed-schema run.start, asserts typed invalid_spec, and proves no marker/registry/run effect.

Full Rust workspace:

$ CARGO_HOME="$HOME/.cargo" RUSTUP_HOME="$HOME/.rustup" CARGO_TARGET_DIR="$HOME/.relayflows-toolchain/target/gate-contract" "$HOME/.cargo/bin/cargo" test --locked --manifest-path kernel/Cargo.toml --workspace
...
relayflowd unit: 22 passed
crash_resume: 19 passed
event_wake: 1 passed
hn: 1 passed
invalid_schema_preflight: 1 passed
subscription: 3 passed
relayflow_core: 27 passed
spec parity: 5 passed
journal: 17 passed
doc-tests: passed

Live SDK/kernel:

$ cd sdk && RELAYFLOWD_BIN="$HOME/.relayflows-toolchain/target/gate-contract/debug/relayflowd" ./node_modules/.bin/vitest run tests/live-kernel.test.ts --reporter=dot --maxWorkers=1 --minWorkers=1 --testTimeout=60000
 ✓ tests/live-kernel.test.ts (17 tests) 65482ms
 Test Files  1 passed (1)
      Tests  17 passed (17)
   Duration  66.54s

Pre-push race check and push:

pre_push_remote_head=f5b8437b41e32d7ba45bb96eecf9bf8eb2aee65a
expected_parent=f5b8437b41e32d7ba45bb96eecf9bf8eb2aee65a
local_head=e6210a2fc666df6dc8c777c009712ddf99efa877
To https://github.com/AgentWorkforce/flows.git
   f5b8437..e6210a2  feat/v2-gate-contract -> feat/v2-gate-contract

Reviewer-owned reports under ops/reviews/ remain staged and are not part of this commit.

@kjgbot

kjgbot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Hosted exact-head follow-up:

$ printf 'local_head=%s\nremote_head=%s\n' "$(git rev-parse HEAD)" "$(git rev-parse origin/feat/v2-gate-contract)"
local_head=e6210a2fc666df6dc8c777c009712ddf99efa877
remote_head=e6210a2fc666df6dc8c777c009712ddf99efa877

$ gh run view 33678271936 --json status,conclusion,headSha,url
{"conclusion":"success","headSha":"e6210a2fc666df6dc8c777c009712ddf99efa877","status":"completed","url":"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/AgentWorkforce/flows/actions/runs/33678271936"}

Hosted linux-x64-artifact completed successfully at the PR head. Its Build relayflowd, Build standalone flows CLI, Assemble artifact and smoke verifier path, Smoke exact Linux artifact, and artifact-upload steps all passed.

PR remains open; no merge performed.

@kjgbot kjgbot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SIGNOFF A — FINDINGS (not an approval)

I reviewed exact head f5b8437b41e32d7ba45bb96eecf9bf8eb2aee65a against merge base a0d42ffbdc7fb60b42c0b5bea4f58408249b08a2.

P1 / F1: the claimed behavior-free SDK data boundary is incomplete. At that reviewed head, exported validateSpec accepts a valid Proxy after three ownKeys/descriptor traps and returns {ok:true}; exported kernelToAuthoring likewise executes three traps and returns normalized output. compileSpec correctly rejects the same Proxy at zero traps, which isolates the bypass.

File evidence: sdk/src/validate.ts:81-92 reflects input through Object.keys; sdk/src/compile.ts:187-324 has public kernelToAuthoring / requireKernelObject / assertKernelKeys reflecting an unknown; sdk/src/index.ts:31-40 exports both boundaries. Reproduced in Node, Bun, and a packed-and-installed @relayflows/sdk tarball.

Required repair: snapshot/reject before any reflection in both exported unknown-input boundaries (or make the reverse converter non-public and parsed-JSON-only), with Node, Bun, and packed-install regression coverage.

The PR now reports head e6210a2fc666df6dc8c777c009712ddf99efa877, which I have not reviewed; this verdict is pinned only to f5b8437b41e32d7ba45bb96eecf9bf8eb2aee65a and needs re-signoff after a fix. Full literal evidence is committed in ops/reviews/20260902-2140-pr139-signoff-structure.md.

@kjgbot
kjgbot force-pushed the feat/v2-gate-contract branch 2 times, most recently from 7702a2f to 0ea58b5 Compare September 3, 2026 12:28
kjgbot pushed a commit that referenced this pull request Sep 3, 2026
Supersedes an unmerged first pass of this report that targeted 990093b. Every
command is pinned to a literal SHA rather than the origin/main ref, which moved
twice during the task.

This rebase produced ZERO conflicts, which is the risk rather than the result:
on #139's rebase a line that reverted a lowering auto-merged silently. Every
hunk was therefore audited by reading.

512723c adds #138, which touches four files this branch also edits (spec.rs,
spec/tests.rs, validate.ts, validate.test.ts) and, critically, moves timeoutMs
to deterministic-only in TWO independent places: the step-fields allowlist and
compileStep's base spread. Getting one right and missing the other yields a spec
that validates but lowers wrong, and validateSpec cannot see it. Both halves are
byte-identical to 512723c and both were re-proved behaviourally through
compileYaml + toKernelSpec: a deterministic step lowers to timeout_ms, llm and
agent are refused at the allowlist. #136's `output` line survives in both verb
lists.

Artifact survival, both directions. All 15 of #138's blobs hashed before and
after: 11 identical including compile.ts and step-fields.ts; the 4 that moved
are the 4 this branch edits and each is a pure addition. Every line of #138
content absent afterwards was enumerated: a first pass with plain diff reported
14, of which 7 were false positives from re-indentation and one rustfmt
attribute rewrap; whitespace-insensitively 7 remain, all attributed and none
authored by #138. In the other direction, a whole-tree set-diff of the branch's
own change set before against after reports exactly three deltas across 41
files, the same three deliberate resolutions as the first pass and nothing else.

The branch's own gate is proved where it lives rather than where it is
convenient: a canonical spec compiled through the SDK, its lowered kernel spec
then mutated and submitted over a real socket with the SDK out of the path. The
kernel refuses all five non-canonical forms across both surface kinds, and
accepts the canonical control.

Gates: tsc --noEmit, tsc -p tsconfig.type-tests.json (a gate #138 added that the
brief's list predates), and tsc -p tsconfig.tests.json all pass; cargo test
--workspace is 130 passed, 0 failed; vitest is 410 passed with one failure, the
pre-existing wire-conformance one. Rust test names set-difference to exactly the
union of both parents, 130 executed against 130 expected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
@kjgbot
kjgbot force-pushed the feat/v2-gate-contract branch from 105411c to 40edd03 Compare September 3, 2026 13:07
kjgbot added a commit that referenced this pull request Sep 3, 2026
* kernel: dispatch runnable steps in parallel

Session-Id: 01a062cc-f525-7d01-932e-a634815114c1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* fix(kernel): drive complete parallel dispatch batches

Session-Id: 01a062cc-f525-7d01-932e-a634815114c1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* fix(kernel): preserve parallel assignment lifecycle

Session-Id: 01a062cc-f525-7d01-932e-a634815114c1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* fix(kernel): close parallel dispatch admission gaps

Session-Id: 01a062cc-f525-7d01-932e-a634815114c1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* fix(kernel): reject forged completion pins

Session-Id: 01a062cc-f525-7d01-932e-a634815114c1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* fix(kernel): canonicalize workspace surfaces across kernel/SDK/socket

Second P1 in PR #137 review: `/mount/repo` and `/mount/./repo` were
admitted concurrently. `parallel::SurfaceIdentity::Opaque("workspace:...")`
compared the raw string; workspace surfaces bypassed the canonical path
identity used for external surfaces, letting alias forms conflict-check
as disjoint.

Repair:

- `SurfaceIdentity::External` → `SurfaceIdentity::Path{kind, namespace,
  components}` with `PathSurfaceKind::{Workspace,External}`. Workspace and
  external surfaces now share the canonical path identity but do not
  cross-collide.
- Rename `spec::external_surface_identity` → `spec::path_surface_identity`;
  callers of the workspace surface use the same canonicalizer.
- SDK `isCanonicalExternalSurface` → `isCanonicalPathSurface`; workspace
  entries now reject empty/./.. components with the same error class as
  external surfaces.
- New `kernel/relayflowd/tests/crash_resume/workspace_identity.rs` covers
  alias refusal and canonical subtree serialization over a real socket.
- 33 new SDK validate cases pin workspace canonicalization; 9 kernel
  parallel_tests cases (ancestor/descendant/sibling for both surface
  kinds) still green.

Evidence:

  $ cargo test -p relayflowd-core --lib machine::parallel
  test result: ok. 9 passed; 0 failed; ...

  $ cargo test -p relayflowd --test crash_resume workspace_aliases
  test result: ok. 1 passed; 0 failed; ... finished in 0.89s

  $ ./node_modules/.bin/vitest run tests/validate.test.ts
  Test Files  1 passed (1)
  Tests  48 passed (48)

Session-Id: 6cae47a0-1263-4c8b-bfaa-bd5ffc72e08e

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* fix(kernel): preserve terminal-slash surface compatibility

Session-Id: 01a0667b-bd7e-73c1-8e14-e3e9d13d136e

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* fix(kernel): one spelling per surface, refusing the terminal slash

Reverts 83db98b's accept-and-normalize and restores 53bfee0's strict rule
for BOTH workspace and external surfaces. An independent signoff at 83db98b
found a P0: exactly-once effects can double-fire.

83db98b widened the *external* accept set the same way it widened workspace
-- its own test diff moved "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/provider/item/" from reject to accept -- but
added `workspace_surfaces_equal` only. The exactly-once ledger key is a raw
SQL string:

  PRIMARY KEY (step_id, idempotency_key, surface_path)
  relayflowd-journal/src/lib.rs:48, append.rs:162

`idempotency_key = sha256(run_id || step_id)` (machine.rs:396) and `step_id`
are both constant across attempts, so `surface_path` is the only variable in
that key -- and it had two legal spellings. Executed against the real
SqliteJournal at 83db98b:

  attempt1 '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/provider/item'   deduped = false
  attempt2 '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/provider/item/'  deduped = false
  effect_count = 2 ; confirmed_effect_count = 1

`deduped=false` means "you owe the provider call" (engine/effects.rs:17-24),
so one logical effect fires twice. At the parent commit it failed closed at
effects.rs:127.

Accept-and-normalize only holds if EVERY identity comparison routes through
the same normalization. 83db98b reached fifteen workspace comparison sites
and got all fifteen right; it missed the sixteenth, which happens to be the
one guarding exactly-once. Uniform reject needs no such completeness: one
surface has exactly one spelling, and a non-canonical one never enters the
system.

Two facts make the strict rule the house rule rather than a new constraint:
testdata/hello-agent.flow.yaml already authored `surface: repo`, so the
ladder fixture was the outlier; and 53bfee0's own contract test
`workspace_mounts_and_worktrees_must_have_one_canonical_spelling` already
asserted `/mount/repo/` is refused, which accept-and-normalize contradicted.

RED (before this change, with the tests restored to the strict contract):

  $ cargo test -p relayflowd-core --lib spec::tests::external_surface
  ---- spec::tests::external_surface_paths_must_have_one_canonical_spelling stdout ----
  panicked at relayflowd-core/src/spec/tests.rs:146:9:
  accepted non-canonical surface "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/provider/item/"
  test result: FAILED. 0 passed; 1 failed

GREEN:

  $ cargo test --workspace
  22 + 31 + 1 + 1 + 4 + 3 + 37 + 5 + 18 passed; 0 failed

  $ ./node_modules/.bin/vitest run
  259 passed (260); 1 pre-existing live-kernel failure, unchanged

Fixtures: testdata/hello-ladder.flow.yaml authors `surface: repo`, and its
canonical JSON and sha256 are regenerated through the SDK compiler rather
than hand-edited. The canonical diff is one character; the hash moves
ecccd7b2..de095a29 -> 57cac294..f6d57944, and spec_parity confirms kernel and
SDK still agree byte-for-byte.

`workspace_surfaces_equal` is kept across its 18 call sites. Under a single
spelling it is equivalent to string equality for valid surfaces, but it still
compares parsed identities and so fails closed when either side does not
parse -- defense in depth at the pin/declaration seam for exactly the bug
class above. Removing it would be an 18-site change for no safety gain.

A non-canonical spelling is no longer a surface at all, so
`external_surface_contains("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/provider/item/", "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/provider/item")` now fails
closed rather than resolving to the canonical form, and the two
ancestor/descendant conflict cases that exercised the terminal slash are
dropped as unreachable states rather than restated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* test(sdk): a late completion after cancel reports run_terminal, not lease_conflict

This edits a test that judges this branch's own behaviour, which AGENTS.md
rails against. It is therefore its own commit, touching nothing else, and the
taxonomy call was made by the lead (relayflow-lead-0903), not by this branch.
Flagging it for the independent signoff to re-derive rather than inherit.

The test is #142's own — "cancels over the real socket and rejects the lease
holder after closure", added by main in feat(kernel): add durable run
cancellation. It cancels a run over the socket, then has the lease holder
complete the step, and asserted the refusal carried `lease_conflict`.

What this branch changed is which of two refusals fires first, not whether the
completion is refused. `step.complete` now runs the `ensure_mutable` admission
gate before `completion_worker`:

  ensure_mutable(&engine, &params.run_id)?;              // -> run_terminal
  let worker_id = hub
      .completion_worker(connection_id, &key)
      .map_err(protocol_conflict)?;                      // -> lease_conflict

Each code has exactly one producer in the tree (server/protocol.rs:58 and :46),
so the ordering fully determines which is returned.

Unchanged by this commit, and still asserted by the same test: the completion is
refused, exactly one run.cancel.requested entry exists, and exactly one
run.completed entry exists carrying completionReason "canceled". Only the error
code moved.

The lead's reasoning for preferring run_terminal: lease_conflict tells a worker
"someone else holds your lease", which is false here -- nobody holds it, the run
is over -- and it invites a retry that terminality does not. Checking "can this
run accept mutations at all?" before "who holds this lease?" is also the correct
precedence: the cheaper, more general, fail-closed question first.

The rejected alternative was weakening ensure_mutable so lease_conflict still
won. That trades a correct guard for a stale expectation.

Mutation-verified, both directions, on the rebased tree at 512723c.

RED (before this commit):

  $ ./node_modules/.bin/vitest run
   FAIL  tests/live-kernel.test.ts > ... > cancels over the real socket and rejects the lease holder after closure
  AssertionError: expected JournalProtocolError: run_terminal: run 0... { code: '...' } to match object { code: 'lease_conflict' }
  - Object {
  -   "code": "lease_conflict",
  + JournalProtocolError {
  +   "code": "run_terminal",
        Tests  2 failed | 409 passed | 3 skipped (414)

GREEN (after):

  $ ./node_modules/.bin/vitest run
        Tests  1 failed | 410 passed | 3 skipped (414)

The one remaining failure is the pre-existing `JournalClient wire conformance`
failure, which is independent: it survives moving ensure_mutable after
completion_worker, whereas this test does not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* docs(reviews): record the PR #137 rebase onto 512723c

Supersedes an unmerged first pass of this report that targeted 990093b. Every
command is pinned to a literal SHA rather than the origin/main ref, which moved
twice during the task.

This rebase produced ZERO conflicts, which is the risk rather than the result:
on #139's rebase a line that reverted a lowering auto-merged silently. Every
hunk was therefore audited by reading.

512723c adds #138, which touches four files this branch also edits (spec.rs,
spec/tests.rs, validate.ts, validate.test.ts) and, critically, moves timeoutMs
to deterministic-only in TWO independent places: the step-fields allowlist and
compileStep's base spread. Getting one right and missing the other yields a spec
that validates but lowers wrong, and validateSpec cannot see it. Both halves are
byte-identical to 512723c and both were re-proved behaviourally through
compileYaml + toKernelSpec: a deterministic step lowers to timeout_ms, llm and
agent are refused at the allowlist. #136's `output` line survives in both verb
lists.

Artifact survival, both directions. All 15 of #138's blobs hashed before and
after: 11 identical including compile.ts and step-fields.ts; the 4 that moved
are the 4 this branch edits and each is a pure addition. Every line of #138
content absent afterwards was enumerated: a first pass with plain diff reported
14, of which 7 were false positives from re-indentation and one rustfmt
attribute rewrap; whitespace-insensitively 7 remain, all attributed and none
authored by #138. In the other direction, a whole-tree set-diff of the branch's
own change set before against after reports exactly three deltas across 41
files, the same three deliberate resolutions as the first pass and nothing else.

The branch's own gate is proved where it lives rather than where it is
convenient: a canonical spec compiled through the SDK, its lowered kernel spec
then mutated and submitted over a real socket with the SDK out of the path. The
kernel refuses all five non-canonical forms across both surface kinds, and
accepts the canonical control.

Gates: tsc --noEmit, tsc -p tsconfig.type-tests.json (a gate #138 added that the
brief's list predates), and tsc -p tsconfig.tests.json all pass; cargo test
--workspace is 130 passed, 0 failed; vitest is 410 passed with one failure, the
pre-existing wire-conformance one. Rust test names set-difference to exactly the
union of both parents, 130 executed against 130 expected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* docs(reviews): correct the wire-conformance attribution — it is this branch's

An earlier version of this report called the vitest wire-conformance failure
"pre-existing". That was wrong. It was observed at 83db98b, which is #137's own
head and already carries bdd598c; "pre-existing at the branch parent" is not
"pre-existing on main", and I repeated the stronger claim without testing it.

Determined empirically instead. A throwaway worktree at 512723c with its own
kernel build and RELAYFLOWD_BIN pinned to it runs tests/live-kernel.test.ts at
21 passed (21), wire conformance included. The same file fails on this branch.
The failure is the branch's.

It is also not an error-code taxonomy change, which is why no assertion was
touched. bdd598c gates six verbs with ensure_mutable, two of them event.emit and
stream.append. The wire-conformance test starts a deterministic run, which
completes immediately, then calls both on it and asserts they SUCCEED
(matched === 0, offset === 0). Main accepts them; this branch refuses them. The
observable protocol behaviour changed from accepted to refused.

The refusal is load-bearing rather than gratuitous, and the obvious fix is the
wrong one. On main, stream.append against a terminal run writes a
stream.appended entry after run.completed. The branch's state.rs fold guard
rejects exactly that, so a journal main produces is one this branch cannot load:
the same data directory resumes clean under the main binary and fails under the
branch binary with "journal entry 5 appears after terminal run.completed".
Relaxing ensure_mutable to turn the test green would let the daemon write
journals it then cannot resume, which is a durability bug and worse than a red
test.

The rebase also surfaced a direct contradiction: the branch's own
protocol_admission test asserts stream.append and event.emit return run_terminal
and leave the journal unchanged, while main's wire-conformance test asserts they
succeed. Both are in the tree, both were written deliberately, and they cannot
both pass. Because protocol_admission pins the error code, an accept-and-ignore
compromise would require editing that gate too.

Three options are laid out in the report. None is implemented; the branch is
unchanged and the push is on hold pending the lead's call.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* test(sdk): a terminal run refuses event.emit and stream.append

PROTOCOL CHANGE. This edits main's test to match a deliberate behaviour change
in this branch, so it is its own commit, touching nothing else. The call was the
lead's (relayflow-lead-0903), not this branch's. The signoff should re-derive it
rather than inherit it.

This is main's test -- "JournalClient wire conformance against live relayflowd".
It started a deterministic run, which completes immediately, then called
event.emit and stream.append on that terminal run and asserted both SUCCEED:

  expect((await client.eventEmit(run_id, 'unmatched', {ok:true})).matched).toBe(0);
  expect((await client.streamAppend(run_id, 'results', {answer:4})).offset).toBe(0);

bdd598c admits every mutating verb through `ensure_mutable`, so this branch
refuses both with run_terminal. Unlike the step.complete change in 269fcc6, this
is not an error-code taxonomy move: observable behaviour on a shipped verb pair
goes from accepted to refused.

It does not remove a working capability. It removes a way to corrupt a journal
that main reports as success. On main, stream.append against a terminal run
journals stream.appended AFTER run.completed, and this branch's state.rs fold
guard rejects exactly that -- so main produces journals the daemon cannot fold
on resume. Same data directory, both binaries:

  MAIN   resume -> exit=0  {"status":"completed","completion_reason":"success"}
  BRANCH resume -> exit=1  Error: fold run journal
                           Caused by: journal entry 5 appears after terminal run.completed

The realistic shape is worse than that synthetic one, and shows main is already
self-inconsistent. A worker holds an llm lease; the run is cancelled out from
under it; the worker then does what a live worker does:

  late step.complete  -> THROW [lease_conflict]      <- main already refuses this
  late stream.append  -> OK    {"offset":0}          <- and corrupts the journal
  late event.emit     -> OK    {"matched":0}
  >>> entries AFTER terminal run.completed: ["stream.appended"]

Main already holds "a terminal run accepts no step completions"
(engine/remote.rs:44, from #142). This branch extends the same rule to the other
mutating verbs, which is what the words already meant.

Checked before changing anything, rather than assuming: no product code calls
either verb (only the JournalClient method definitions); no doc sanctions a
post-terminal append -- kernel/DESIGN.md:384-385 says event.emit "satisfies
wait.event", which a terminal run has none of, and stream.append "journals
stream.appended", which is the corruption; and the one in-repo live use
(crash_resume/llm.rs) is against a parked run and is unaffected.

The test keeps full wire coverage of both verbs. Their success paths move to the
parked llm run, which is the only state in which appending to a run's journal is
meaningful, and the terminal case now asserts the refusal -- matching
crash_resume/protocol_admission.rs, which is left alone because it pins the
invariant correctly.

  $ ./node_modules/.bin/tsc -p tsconfig.tests.json
  (clean)
  $ RELAYFLOWD_BIN=<this worktree's own build> ./node_modules/.bin/vitest run
   Test Files  23 passed | 1 skipped (24)
        Tests  411 passed | 3 skipped (414)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* docs(reviews): record the option-1 resolution and the assumption test behind it

The lead chose option 1 and asked that the assumption behind it be tested rather
than inherited: is there any legitimate late stream.append or event.emit on a
terminal run, out-of-band async completion in particular. Checked four ways and
the assumption holds.

No product code calls either verb — only the JournalClient definitions, a
loopback mock, and tests. No doc sanctions a post-terminal append: DESIGN.md says
event.emit satisfies a wait, which a terminal run has none of, and stream.append
journals stream.appended, which is the corruption itself. The one live in-repo
use appends against a parked run and is unaffected.

The async shape was executed rather than reasoned about, and it cuts against main:
a worker holding an llm lease on a run cancelled out from under it has its late
step.complete refused with lease_conflict while its late stream.append is
accepted in the same moment, journalling after run.completed and producing a
journal the branch cannot fold. Main already holds the rule for step completions
at engine/remote.rs:44; this branch extends it to the remaining mutating verbs.

Adds the PR-body Protocol change section, and records a CI finding: the two
contradicting tests both passed because neither runs in flows CI. cargo test
never runs at all, and CI's vitest names four files, of which live-kernel.test.ts
is not one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* docs(reviews): lead the Protocol change section with main's self-inconsistency

The strongest argument for the change is not the resume demonstration, it is
that main already holds the rule and fails to apply it uniformly: a worker whose
run was cancelled has its late step.complete refused with lease_conflict and its
late stream.append accepted, in the same moment. This PR extends main's own rule
rather than imposing a new one. Reordered so a reviewer meets that first, with
the resume corruption as the reason it matters and DESIGN.md:384-385 as
corroboration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

---------

Co-authored-by: kjgbot <kjgbot@agentrelay.dev>
@kjgbot
kjgbot force-pushed the feat/v2-gate-contract branch from 40edd03 to 8b7148d Compare September 3, 2026 23:03
@kjgbot

kjgbot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto main @ 3725025 — now 8b7148d

The branch was CONFLICTING and sat unmoved for ~12h (the codex agent assigned the rebase never pushed). Rebased all 12 commits myself.

Conflicts and how they were resolved

1. kernel/relayflowd-core/src/spec.rs — both sides added validation to the same step loop. Main (#137) added the surface-identity check; this branch added the JSON Schema check. They are independent if let blocks that happened to share a trailing brace, which is why git could not merge them. Both kept, surface check first.

2. kernel/relayflowd/src/server.rs — trap 1 (a moved definition). This is the dangerous one. Main moved decode_params, to_value, protocol_conflict, internal_error and error_response out of server.rs into server/protocol.rs as pub(super). This branch's commit re-added all of them to server.rs. Taking "both sides" would have produced duplicate definitions.

Verified per-function against main before resolving:

decode_params:    in main server.rs=0 ; elsewhere in main kernel=1
to_value:         in main server.rs=0 ; elsewhere in main kernel=1
protocol_conflict:in main server.rs=0 ; elsewhere in main kernel=1
internal_error:   in main server.rs=0 ; elsewhere in main kernel=1
error_response:   in main server.rs=0 ; elsewhere in main kernel=1
run_start_error:  in main server.rs=0 ; elsewhere in main kernel=0   ← genuinely new

So: took main's side wholesale, and relocated only run_start_error — the one function that is this branch's own — into server/protocol.rs as pub(super), adding the relayflowd_core::SpecError import there. server.rs does use protocol::*, so the call site at run.start is unchanged and still maps a SpecError to invalid_spec rather than internal. Main's import line was taken as-is; the build produces no unused-import warning, which is the check that the resolution is actually consistent rather than merely compiling.

Trap audit

The other three known silent-revert traps, each checked textually because each auto-merges with no conflict marker:

  • timeoutMs deterministic-only — present in both the base spread and the allowlist (compile.ts:131, plus the timeout_ms lowering at 385/507).
  • .map(toKernelTrigger) — survives at compile.ts:252; without it camelCase reaches a deny_unknown_fields kernel.
  • step-fields.ts output — present on both llm and agent.

Verification at 8b7148d

kernel:  sh ops/cargo.sh test --workspace   → 142 passed, 0 failed
         (main is 130; this branch adds 12)
SDK:     vitest run                          → 561 passed, 3 skipped, 0 failed (26 files)

Diff against main in kernel/relayflowd-core/ is 750 insertions, 2 deletions — nothing of main's was silently dropped, which is the specific failure mode trap 1 causes.

One flake, reported not hidden

The first SDK run showed 1 failed, with:

Error: ENOENT: no such file or directory, open '.relayflow/backlog-picker-entry.json'

Two subsequent full runs were clean (561 passed). It looks like a test writing to a relative .relayflow/ path racing with another under vitest parallelism, rather than anything this rebase changed — but it is a real flake in the suite and now that #153 makes CI run all 26 files, it will surface there periodically. Worth a separate issue; I have not touched it here.

Note on the base

main moved under this work: #153 (ci: run the kernel and full SDK suites) merged mid-rebase, so I rebased again onto 3725025. This PR is the first to be judged by the full kernel + SDK suites rather than four named test files.

Still needs a fresh independent signoff at 8b7148d before merge — I did the rebase, so I am not the right party to clear it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

@kjgbot

kjgbot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Correction to my previous comment

I wrote that "the codex agent assigned the rebase never pushed" and that the branch "sat unmoved for ~12h." That framing is wrong and I want it corrected in the record.

flows-pr139-rebase-codex-0904 completed the rebase, at 22:59Z — about an hour before I started mine. It committed locally as df7cc1c1 and deliberately did not push, because its brief required my approval before pushing. It then reported and waited. That is exactly the behaviour I asked for. What I observed was an unmoved remote branch, and I read a correct hold as a failure without checking my inbox first.

So the duplicated work is mine, not its.

Its independently-produced result matches mine, which is a useful cross-check on both:

codex df7cc1c1 mine 8b7148d
kernel 142 passed (114 → 142, +28, 0 removed) 142 passed
SDK 561 passed / 3 skipped (538 → 561, +23, 0 removed) 561 passed / 3 skipped
tsc 3 × exit 0 clean

It also reached the same conclusion on the conflict I hit: server-side error mapping belongs in #137's extracted server/protocol.rs, with #137's canonical surfaces and #139's URI-aware termination gate both preserved.

The one substantive difference is the base: it pinned 27b7151 and, per its rails, did not chase main when 3725025 landed mid-run. Mine is rebased onto 3725025, so the pushed head stands — but it got there first and on the merits.

Its full evidence is at ops/reviews/20260903-pr139-repair-0903.md §14, including a one-sided-file enumeration and a P0 check I did not run: URI ladder with 0 effects and the daemon alive, seven URI attacks live=0, and child-applicator recursion accepted.

It also independently disclosed the intermittent capacity failure — see #155.

kjgbot pushed a commit that referenced this pull request Sep 3, 2026
…gnoff commissioned

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
@kjgbot

kjgbot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

VERDICT: PASSED

Independent signoff: PR #139 at 8b7148d

Scope: assessment only. I did not fix product code, push, merge, or resolve review
threads. The review worktree began at exactly
8b7148da9050a14996ecdcd80bb867c2813241c1; its merge-base with the requested
main baseline is exactly 372502560ffcc1de14cb3fe72c852e0490dbfb30.

The rebase preserved both sides of both conflict resolutions and all four
named silent-revert traps. Every semantic claim below has a source mutation,
a changed SHA-256, a failing witness, byte-for-byte restoration, and a passing
witness. The final full gates are 142/142 kernel tests and 561/561 SDK tests
with the expected 3 skips.

Conflict 1: protocol helper relocation plus run_start_error

Textual result

Command:

rg -n 'fn (decode_params|to_value|protocol_conflict|internal_error|error_response|run_start_error)\b' kernel --glob '*.rs' --glob '!target/**'

Literal output:

kernel/relayflowd/src/server/protocol.rs:38:pub(super) fn decode_params<T: DeserializeOwned>(params: Value) -> ProtocolResult<T> {
kernel/relayflowd/src/server/protocol.rs:42:pub(super) fn to_value(value: impl Serialize) -> ProtocolResult<Value> {
kernel/relayflowd/src/server/protocol.rs:46:pub(super) fn protocol_conflict(error: anyhow::Error) -> (&'static str, String) {
kernel/relayflowd/src/server/protocol.rs:65:pub(super) fn run_start_error(error: anyhow::Error) -> (&'static str, String) {
kernel/relayflowd/src/server/protocol.rs:73:pub(super) fn internal_error(error: anyhow::Error) -> (&'static str, String) {
kernel/relayflowd/src/server/protocol.rs:86:pub(super) fn error_response(id: Value, code: &str, message: String) -> Response {

Exact definition counts across kernel/**/*.rs:

decode_params=1
to_value=1
protocol_conflict=1
internal_error=1
error_response=1
run_start_error=1

All six are defined only in server/protocol.rs; none is duplicated in
server.rs. server/protocol.rs imports relayflowd_core::SpecError, and
server.rs calls .map_err(run_start_error) for engine.start.

Clean compiler output, with warnings visible:

$ cd kernel && PATH="$HOME/.cargo/bin:$PATH" RUSTUP_TOOLCHAIN=stable sh ../ops/cargo.sh check -p relayflowd
    Checking relayflowd v0.1.0 (/Users/khaliqgant/AgentWorkforce/flows-pr139-signoff-wt/kernel/relayflowd)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.47s

There are no unused-import or duplicate-definition warnings.

Mutation: the five upstream-moved helpers remain the live definitions

I renamed all five moved definitions in protocol.rs while leaving their
callers unchanged.

before  00338968d82a6f05c5655aff3d5138a9c3f08e9a80f102760b19bbadb3d26675  relayflowd/src/server/protocol.rs
mutated d951ca4eddf5045086ff4a6b8e00b48e1a33e4d39dac775c634d243fa1a42b97  relayflowd/src/server/protocol.rs

Command and literal failure excerpt:

$ PATH="$HOME/.cargo/bin:$PATH" RUSTUP_TOOLCHAIN=stable sh ../ops/cargo.sh check -p relayflowd
error[E0432]: unresolved imports `super::internal_error`, `super::to_value`
 --> relayflowd/src/server/cancel.rs:5:42
error[E0425]: cannot find value `protocol_conflict` in this scope
   --> relayflowd/src/server.rs:249:26
error[E0425]: cannot find function `decode_params` in this scope
   --> relayflowd/src/server.rs:132:39
error[E0425]: cannot find function `to_value` in this scope
   --> relayflowd/src/server.rs:147:13
error[E0425]: cannot find function `error_response` in this scope
  --> relayflowd/src/server.rs:73:35
error: could not compile `relayflowd` (lib) due to 48 previous errors

After restoring all five names byte-for-byte:

restored 00338968d82a6f05c5655aff3d5138a9c3f08e9a80f102760b19bbadb3d26675  relayflowd/src/server/protocol.rs
    Checking relayflowd v0.1.0 (/Users/khaliqgant/AgentWorkforce/flows-pr139-signoff-wt/kernel/relayflowd)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.47s

Mutation: SpecError is mapped to invalid_spec, not internal

I changed the SpecError arm in run_start_error from invalid_spec to
internal and temporarily added a direct unit witness. A direct witness is
needed here because handle_request performs an earlier explicit validation;
the contract under review is the error mapper itself.

protocol before  00338968d82a6f05c5655aff3d5138a9c3f08e9a80f102760b19bbadb3d26675
protocol mutated 81b5ac3ec33e22e649a74e4fd4305f14246de106a861e6d0a4cbebe3256e7fc8
test before      6a348c6b36464b30a3023f0231951f236aa550a7e83476504ac866efdf708fd7
test mutated     a0b79d2c39009d0badea2bccb2fbed25c590df334693de87f95a7bd1d6348c13

Command and literal failure:

$ PATH="$HOME/.cargo/bin:$PATH" RUSTUP_TOOLCHAIN=stable sh ../ops/cargo.sh test -p relayflowd signoff_run_start_maps_spec_errors_to_invalid_spec -- --nocapture
running 1 test
thread 'server::tests::signoff_run_start_maps_spec_errors_to_invalid_spec' panicked at relayflowd/src/server/tests.rs:24:5:
assertion `left == right` failed
  left: "internal"
 right: "invalid_spec"
test server::tests::signoff_run_start_maps_spec_errors_to_invalid_spec ... FAILED
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 22 filtered out
error: test failed, to rerun pass `-p relayflowd --lib`

After restoring the mapper, the same witness passed:

protocol restored 00338968d82a6f05c5655aff3d5138a9c3f08e9a80f102760b19bbadb3d26675
running 1 test
test server::tests::signoff_run_start_maps_spec_errors_to_invalid_spec ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 22 filtered out

The temporary test was then removed byte-for-byte:

test restored 6a348c6b36464b30a3023f0231951f236aa550a7e83476504ac866efdf708fd7

Conflict 2: both spec.rs validations survived in the same step loop

Textual excerpt:

143  if path_surface_identity(&workspace.surface).is_none() {
144      return Err(SpecError::InvalidWorkspaceSurface { ... });
151  if path_surface_identity(path).is_none() {
152      return Err(SpecError::InvalidExternalSurface { ... });
159  if let Some(schema) = &step.verification.json_schema {
160      crate::schema::validate_declaration(schema).map_err(|detail| {
161          SpecError::InvalidJsonSchema { ... }
165      })?;

Mutation: surface identity

I disabled the workspace path_surface_identity guard in the source loop.

before  f18e081157dac454abddba7ba5075c2e9518dfab7976ee92c91362a6b19057b2  relayflowd-core/src/spec.rs
mutated cce63758c481270c0bb710457d35cc70732c0f1030d5dfb36df14a3fdc7eca28  relayflowd-core/src/spec.rs

Command and literal failure:

$ PATH="$HOME/.cargo/bin:$PATH" RUSTUP_TOOLCHAIN=stable sh ../ops/cargo.sh test -p relayflowd-core spec::tests::workspace_mounts_and_worktrees_must_have_one_canonical_spelling -- --exact --nocapture
running 1 test
thread 'spec::tests::workspace_mounts_and_worktrees_must_have_one_canonical_spelling' panicked at relayflowd-core/src/spec/tests.rs:257:9:
accepted non-canonical workspace surface ""
test spec::tests::workspace_mounts_and_worktrees_must_have_one_canonical_spelling ... FAILED
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 50 filtered out

Restoration and pass:

restored f18e081157dac454abddba7ba5075c2e9518dfab7976ee92c91362a6b19057b2  relayflowd-core/src/spec.rs
running 1 test
test spec::tests::workspace_mounts_and_worktrees_must_have_one_canonical_spelling ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 50 filtered out

The full gate also executed and passed the real-socket witnesses
surface_identity::aliases_are_rejected_and_external_ancestors_serialize_over_real_sockets
and
workspace_identity::workspace_aliases_are_refused_and_canonical_subtrees_serialize_over_real_sockets.

Mutation: JSON Schema declaration validation

I replaced validate_declaration(schema) with a no-op use of schema.

before  f18e081157dac454abddba7ba5075c2e9518dfab7976ee92c91362a6b19057b2  relayflowd-core/src/spec.rs
mutated 7c48a208372a0a97ab452241ed0e30a502ad329494d5b9d7ecd0e657e0ec2f78  relayflowd-core/src/spec.rs

Command and literal failure:

$ PATH="$HOME/.cargo/bin:$PATH" RUSTUP_TOOLCHAIN=stable sh ../ops/cargo.sh test -p relayflowd --test invalid_schema_preflight invalid_json_schema_is_refused_before_journal_or_command -- --exact --nocapture
warning: function `validate_declaration` is never used
running 1 test
thread 'invalid_json_schema_is_refused_before_journal_or_command' panicked at relayflowd/tests/invalid_schema_preflight.rs:98:5:
assertion `left == right` failed
  left: Bool(true)
 right: false
test invalid_json_schema_is_refused_before_journal_or_command ... FAILED
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 2 filtered out

Restoration and pass:

restored f18e081157dac454abddba7ba5075c2e9518dfab7976ee92c91362a6b19057b2  relayflowd-core/src/spec.rs
running 1 test
test invalid_json_schema_is_refused_before_journal_or_command ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out

The final full gate additionally passed
unbounded_json_schema_is_refused_before_journal_or_command and the negative
control legitimately_recursive_json_schema_still_starts.

Silent-revert trap 1: upstream-moved definitions were not dropped

This is covered in two independent ways:

  1. The five server helpers have exactly one definition each, all in upstream's
    server/protocol.rs; renaming them caused 48 compiler errors, and restoring
    them produced a warning-free cargo check.
  2. The upstream-moved SDK descriptor is byte-identical to main:
$ sha256sum sdk/src/step-fields.ts
3d66671e4af82bf5f9e3940d7c2681882aa0a9f648351985f026763efbe1e8ca  sdk/src/step-fields.ts
$ git show 3725025:sdk/src/step-fields.ts | sha256sum
3d66671e4af82bf5f9e3940d7c2681882aa0a9f648351985f026763efbe1e8ca  -

The descriptor mutation under trap 3 below also proves its test witness is
live: changing the moved table failed both its exact descriptor assertion and
the validator behavior.

Silent-revert trap 2: compileStep base/per-verb output lowering

Textually, base contains only id, type, dependsOn, and
maxIterations. All three verb branches read the shared
verification = typedOutputVerification(step) binding; the llm and agent
branches pass that lowered value through outputGate.

I reproduced the silent revert by changing the llm and agent branches to
read raw s.verification.

before  9f1f9d761410561c7749d1f87b3af708e810b7adf079010165feac8a9dc7d0fe  src/compile.ts
mutated 5f14bb9a5c12ded566dce5f029bdb7419b418982e50b329a8379683806414314  src/compile.ts

Command and literal failure names:

$ ./node_modules/.bin/vitest run tests/verb-field-lint.test.ts tests/typed-output.test.ts -t "compile.*output"
× typed llm and agent outputs > compiles llm output sugar to the existing json_schema primitive
× typed llm and agent outputs > compiles agent output sugar to the existing json_schema primitive
× closed per-verb step fields > ... > compileYaml accepts output on llm and lowers it to the json_schema gate
× closed per-verb step fields > ... > compileYaml accepts output on agent and lowers it to the json_schema gate
AssertionError: expected undefined to deeply equal { type: 'json_schema', ... }
Test Files  2 failed (2)
Tests  4 failed | 88 skipped (92)

Restoration and pass:

restored 9f1f9d761410561c7749d1f87b3af708e810b7adf079010165feac8a9dc7d0fe  src/compile.ts
Test Files  2 passed (2)
Tests  4 passed | 88 skipped (92)

This is the required compileYaml/toKernelSpec witness rather than a
validator, flows check, or preflight-only witness.

Silent-revert trap 3: timeoutMs remains deterministic-only in both places

Textual result:

compileStep base: no timeoutMs spread
deterministic branch: ...(s.timeoutMs !== undefined ? { timeoutMs: s.timeoutMs } : {})
STEP_COMMON_FIELDS: no timeoutMs
STEP_FIELDS_BY_TYPE.deterministic: ['command', 'timeoutMs']
STEP_FIELDS_BY_TYPE.llm: ['prompt', 'model', 'cli', 'output']
STEP_FIELDS_BY_TYPE.agent: ['instruction', 'agent', 'cli', 'model', 'surfaces', 'recoveryMode', 'permissions', 'output']

Mutation A: remove the deterministic branch spread

before  9f1f9d761410561c7749d1f87b3af708e810b7adf079010165feac8a9dc7d0fe  src/compile.ts
mutated 9ca767c11c57f8b39013d90f477fce5d18dc4c79faab33e342f31c2181de3c94  src/compile.ts

Literal failure:

$ ./node_modules/.bin/vitest run tests/spec-parity.test.ts -t "hello-ladder"
× compiles hello-ladder to the pinned canonical JSON
  Expected: ... "timeout_ms":5000 ...
  Received: ... deterministic step with no timeout_ms ...
× hashes hello-ladder to the pinned spec_hash
  Expected: 57cac294f89be6a68ab1d8fbafc78f6fd75dc5998245695b6a28410ff6d57944
  Received: 618668f73dbf834f184f13cc519f19b45bcb718ec116031b935fff4423aefe3d
Test Files  1 failed (1)
Tests  2 failed | 1 passed | 22 skipped (25)

Restoration:

restored 9f1f9d761410561c7749d1f87b3af708e810b7adf079010165feac8a9dc7d0fe
Test Files  1 passed (1)
Tests  3 passed | 22 skipped (25)

Mutation B: add timeoutMs to the llm and agent allowlists

before  3d66671e4af82bf5f9e3940d7c2681882aa0a9f648351985f026763efbe1e8ca  src/step-fields.ts
mutated 4b4eb324fe50d85466ec88301532567af7be82e42b8a351875185bc13f34021d  src/step-fields.ts

Literal failure:

$ ./node_modules/.bin/vitest run tests/verb-field-lint.test.ts tests/validate.test.ts -t "per-verb descriptor|timeout on a non-deterministic"
× pins the per-verb descriptor and generates every foreign-field pair from it
  + "timeoutMs" in both llm and agent
× rejects a timeout on a non-deterministic step (no dialect surface in v0.1.0)
  expected true to be false
Test Files  2 failed (2)
Tests  2 failed | 138 skipped (140)

The mutated descriptor also reduced verb-field-lint.test.ts from 78 to 72
generated tests, independently exposing the lost cross-verb pairs.

Restoration:

restored 3d66671e4af82bf5f9e3940d7c2681882aa0a9f648351985f026763efbe1e8ca
Test Files  2 passed (2)
Tests  2 passed | 144 skipped (146)

Silent-revert trap 4: trigger lowering survives

Textually, the boundary is:

...(compiled.triggers?.length ? { triggers: compiled.triggers.map(toKernelTrigger) } : {}),

I reproduced the auto-merge trap by replacing the mapped value with raw
compiled.triggers.

before  9f1f9d761410561c7749d1f87b3af708e810b7adf079010165feac8a9dc7d0fe  src/compile.ts
mutated 72d0101b01e0f65ecc4a428a5133b41f1fec95ec24c1e5a4eb3cc6fa6f15fa3f  src/compile.ts

Literal failure names and boundary error:

$ ./node_modules/.bin/vitest run tests/spec-parity.test.ts -t "trigger"
× lowers event-triggered-flow.yaml's triggers to the kernel's snake_case dialect
× lowers hn-monitor.flow.yaml's triggers to the kernel's snake_case dialect
× lowers tick-heartbeat.flow.yaml's triggers to the kernel's snake_case dialect
× round-trips every trigger field across the kernel dialect
  spec.triggers[0]: unknown keys "eventType", "dedupeKeyTemplate", "staleAfterMs"
× refuses a kernel trigger key the authoring dialect cannot represent
Test Files  1 failed (1)
Tests  5 failed | 2 passed | 18 skipped (25)

Restoration:

restored 9f1f9d761410561c7749d1f87b3af708e810b7adf079010165feac8a9dc7d0fe
Test Files  1 passed (1)
Tests  7 passed | 18 skipped (25)

The expected/received canonical JSON in the failing run showed the precise
regression: expected event_type, dedupe_key_template, and
stale_after_ms; received eventType, dedupeKeyTemplate, and
staleAfterMs.

Full gates after all mutations were restored

Kernel

Command:

cd kernel && PATH="$HOME/.cargo/bin:$PATH" RUSTUP_TOOLCHAIN=stable sh ../ops/cargo.sh test --workspace

Literal harness summaries:

Finished `test` profile [unoptimized + debuginfo] target(s) in 3.04s
test result: ok. 22 passed; 0 failed; 0 ignored
test result: ok. 34 passed; 0 failed; 0 ignored
test result: ok. 1 passed; 0 failed; 0 ignored
test result: ok. 1 passed; 0 failed; 0 ignored
test result: ok. 3 passed; 0 failed; 0 ignored
test result: ok. 4 passed; 0 failed; 0 ignored
test result: ok. 3 passed; 0 failed; 0 ignored
test result: ok. 51 passed; 0 failed; 0 ignored
test result: ok. 5 passed; 0 failed; 0 ignored
test result: ok. 18 passed; 0 failed; 0 ignored
Doc-tests relayflowd: 0 passed; 0 failed
Doc-tests relayflowd_core: 0 passed; 0 failed
Doc-tests relayflowd_journal: 0 passed; 0 failed

Count: 22 + 34 + 1 + 1 + 3 + 4 + 3 + 51 + 5 + 18 = 142.
The total is exactly the expected 142. The named issue #155 kernel flake did
not occur: worker_capacity::default_capacity_one_reopens_only_after_durable_completion_or_crash ... ok.
No compiler warnings were emitted.

SDK build and suite

Commands:

cd sdk
cp -Rc /Users/khaliqgant/AgentWorkforce/flows-ci-gap-0904-wt/sdk/node_modules node_modules
./node_modules/.bin/tsc && node scripts/make-cli-executable.mjs
env -u RELAYFLOWD_BIN ./node_modules/.bin/vitest run

The build command exited 0 with no output. Final suite literal summary:

RUN  v2.1.9 /Users/khaliqgant/AgentWorkforce/flows-pr139-signoff-wt/sdk
LIVE_KERNEL relayflowd=/Users/khaliqgant/.relayflows-toolchain/target/1578293128/debug/relayflowd
LIVE_KERNEL flows=/Users/khaliqgant/AgentWorkforce/flows-pr139-signoff-wt/sdk/dist/cli.js
Test Files  26 passed | 1 skipped (27)
Tests  561 passed | 3 skipped (564)
Duration  52.41s (transform 603ms, setup 0ms, collect 2.70s, tests 81.84s, environment 3ms, prepare 1.15s)

The total is exactly the expected 561 passed / 3 skipped; no tests moved.

Flakes and reruns

I hit the documented issue #156 ENOENT on both full SDK invocations:

Error: ENOENT: no such file or directory, open '.relayflow/backlog-picker-entry.json'
  path: '.relayflow/backlog-picker-entry.json'

On the first full invocation, the suite also had one transient failure while
still enumerating the expected total:

FAIL tests/live-kernel.test.ts > built flows CLI against live relayflowd > follows a live worker dispatch through flows run
FAILED [protocol_error] relayflowd could not complete the run request: relayflowd returned status parked without a classifiable completion
Test Files  1 failed | 25 passed | 1 skipped (27)
Tests  1 failed | 560 passed | 3 skipped (564)

The exact test immediately passed:

Test Files  1 passed (1)
Tests  1 passed | 26 skipped (27)

It is a reproducible pre-existing default-capacity dispatch race, consistent
with issue #155 rather than a #139 regression. Ten isolated runs at this head
produced FAILURES=4/10. As a control, I created a detached worktree at the
exact main baseline 372502560ffcc1de14cb3fe72c852e0490dbfb30, built the SDK,
and ran the same test ten times; it produced FAILURES=1/10 with the identical
parked/protocol_error signature. No #139 change touches that dispatch path;
the only server.rs diff from main is internal_error to run_start_error on
the engine.start error mapper. The required final full SDK rerun passed all
561 tests.

Restoration ledger

Final SHA-256 values after every mutation was restored:

00338968d82a6f05c5655aff3d5138a9c3f08e9a80f102760b19bbadb3d26675  kernel/relayflowd/src/server/protocol.rs
6a348c6b36464b30a3023f0231951f236aa550a7e83476504ac866efdf708fd7  kernel/relayflowd/src/server/tests.rs
f18e081157dac454abddba7ba5075c2e9518dfab7976ee92c91362a6b19057b2  kernel/relayflowd-core/src/spec.rs
9f1f9d761410561c7749d1f87b3af708e810b7adf079010165feac8a9dc7d0fe  sdk/src/compile.ts
3d66671e4af82bf5f9e3940d7c2681882aa0a9f648351985f026763efbe1e8ca  sdk/src/step-fields.ts

No product or test mutation remains. The only intended worktree addition is
this assessment file.

kjgbot pushed a commit that referenced this pull request Sep 3, 2026
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
@kjgbot
kjgbot force-pushed the feat/v2-gate-contract branch from 8b7148d to 1ed9023 Compare September 4, 2026 06:12
@kjgbot

kjgbot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

CI at 1ed9023: two separate problems, one of them new and specific to this branch

Rebased onto the new main (ee28397) so this picks up #154's CI repair. The run then found two things.

1. worker_capacity — not this PR

06:16:41  test worker_capacity::default_capacity_one_reopens_only_after_durable_completion_or_crash ... FAILED

That is #155, pre-existing on main. #158 fixes it (27 runs/4 failures → 60 runs/0 failures). Nothing owed here.

2. A kernel test hangs on the runner — this one looks like ours

06:17:00  test agent::rung_c_sigkill_boundaries_resume_only_unfinished_steps_via_real_cli
            has been running for over 60 seconds
06:42:29  ##[error]The operation was canceled.

Test kernel ran for 30m15s and was cancelled; every later step was skipped, so the SDK suite never ran at all.

Why I think it is this branch and not the runner:

So: environment-sensitive, and only on this branch. This PR reworks gate and termination handling, which is a plausible place for a run that never terminates — rung_c_sigkill exercises kill/resume boundaries against a real CLI, so a resume path that waits on something that never arrives would look exactly like this.

Worth noting it was previously invisible: before #153 the kernel suite never ran in CI at all, so a hanging kernel test cost nothing and showed nothing.

What this needs

Not a rebase and not #158. Someone should reproduce agent::rung_c_sigkill_boundaries_resume_only_unfinished_steps_via_real_cli under runner-like conditions (Linux, cold target, no warm binaries) and find what it waits on. Until then this PR cannot go green even once #158 lands, because the hang is upstream of the SDK step.

The independent signoff at the previous head remains valid for the code — it covered both conflict resolutions and all four traps, with 7 hash-verified mutations. This is a runtime behaviour the signoff's local run did not surface.

kjgbot pushed a commit that referenced this pull request Sep 4, 2026
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
@kjgbot

kjgbot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Correcting my previous comment: the hang is NOT specific to this branch

I said the rung_c_sigkill hang "looks like ours" and gave three reasons. One of them has since been contradicted, so the claim does not stand.

#158 — which is main plus a single-expression kernel change, nothing to do with gates or termination — hangs in the same test, at the same place, for the same duration.

#139  Test kernel 30m15s   agent::rung_c_sigkill_... running >60s   → cancelled
#158  Test kernel 30m16s   agent::rung_c_sigkill_... running >60s   → cancelled

So "only this branch" was wrong. What the four runs actually show:

PR touches kernel? rung_c_sigkill
#157 (tick runner) no passed, 38s binary
#134 (surface) no passed, 38s binary
#139 (gates/termination) yes hung, cancelled at 30m
#158 (retry timing) yes hung, cancelled at 30m

That is a suggestive split but it is n=2 on each side, and it is equally consistent with a Linux-only flake that the two kernel PRs happened to hit. I have re-run #158's job to tell those apart; if it hangs a second time the split is real, if it passes the hang is a flake and this PR needs no work for it.

Either way the correction stands on its own: do not treat the hang as evidence against this PR's gate/termination changes until the re-run says something. I reached for the most specific explanation available and stated it more confidently than two data points support.

The worker_capacity failure in this PR's run is still #155 and still fixed by #158 — that part is unaffected, and #158's run confirms it on the runner:

test worker_capacity::default_capacity_one_reopens_only_after_durable_completion_or_crash ... ok

kjgbot added 12 commits September 4, 2026 10:13
Session-Id: 01a062df-0cdf-7f23-89dd-121aa9ecf743

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
Session-Id: 01a062df-0cdf-7f23-89dd-121aa9ecf743

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
Session-Id: 01a062df-0cdf-7f23-89dd-121aa9ecf743

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
Session-Id: 01a062df-0cdf-7f23-89dd-121aa9ecf743

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
Session-Id: 01a062df-0cdf-7f23-89dd-121aa9ecf743

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
Session-Id: 01a062df-0cdf-7f23-89dd-121aa9ecf743

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
A `json_schema` gate whose `$ref` graph cycles through only in-place
applicators compiles cleanly and then recurses without bound the first time
it validates an output. In Rust that aborts the process, so `run.start`
accepted the spec, created the journal, ran the step's command, and then took
relayflowd down with SIGABRT — leaving a run stuck `running` that re-executed
its effect on every resume (4 executions of one logical step, no
`completionReason`, no `step.completed`). RFC-0001 covenant 2 and gate 1.

A stack overflow cannot be caught, so the bound is structural and runs before
the declaration is accepted: reject a reference cycle that re-applies to the
same instance and therefore makes no progress. Cycles through a child
applicator (`properties`, `items`, `prefixItems`, ...) consume one level of
the instance per step and stay legal, so ordinary recursive schemas are
unaffected.

`kernel/relayflowd-core/src/schema.rs` and `sdk/src/json-schema-bound.ts`
implement the same rule and are pinned to a shared corpus in
`testdata/json-schema-bound-cases.json`, so the kernel and the SDK agree on
which schemas are legal by construction rather than by coincidence of Ajv's
catchable RangeError and Rust's uncatchable abort. That also closes the
reported SDK/kernel divergence on a self-recursive `$defs`. Every corpus
refusal compiles cleanly in `jsonschema`, which is what makes the tests test
the bound and not the mechanism. `verify` now compiles through the same gate.

Also from the same review:
- `canonicalize`/`specHash` are exported unknown-input helpers, so they carry
  the snapshot guard the rest of the exported surface already has, and each
  key is read exactly once instead of twice (a demonstrated getter TOCTOU).
- A `json_schema` gate that accepts every output (`{}`, `true`, annotations
  only) is still legal, but `flows check` marks the line and preflight emits a
  `vacuous_gate` warning: a gate that judges nothing must not read like one
  that judges something.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
Rebase onto main brought in #133's `output:` sugar, which lowers to a
`json_schema` gate at compile time but was only checked with `isObject`. So an
`output` schema the kernel refuses passed `flows check` and was reported as a
gate — an unbounded `$ref` cycle included. Route it through `jsonSchemaError`,
the same gate a hand-written `verification: {type: json_schema}` clears.

Also adapts one test to main's boundary contract rather than deleting it:
`preflight` now returns a named `invalid_spec` refusal where it used to throw,
so the proxy-boundary test accepts either refusal shape and treats "returned a
usable result" as a failure. Trap and getter counters are untouched.

Adds ops/reviews/20260903-pr139-repair-0903.md: the red four-execution ladder,
the structural fix, the shared kernel/SDK corpus, the three-path `output`
proof, the silently-merged-file enumeration, and every gate with literal
output. It also records that origin/main moved from 3da71e2 to 990093b (#136)
mid-work and that this branch is rebased onto 990093b, not the pinned SHA.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
… kernel exit-101

Two conflict resolutions on this rebase could have silently reverted freshly
merged behaviour, not one. The brief named step-fields.ts; compile.ts's
compileStep is the sharper one, because neither validateSpec nor `flows check`
can see it — both still report a healthy gate when the `output:` lowering has
been reverted to the raw authored gate. Only compileYaml + toKernelSpec, read
against the kernel verification object, tells "the key was accepted" apart from
"the key became a gate". Records that, and that one of the reverting lines
auto-merged without git flagging a conflict.

Also characterises the kernel gate's one exit-101-with-zero-failures rather
than leaving it as flake: the binary is named (relayflowd-core spec_parity, and
only that one), disk is ruled out at 30 GiB free, one clean reproduction
attempt came back green, and concurrent load is named as the untested
condition. Recorded as unexplained.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1


#138 moved `timeoutMs` out of BaseStepSpec/STEP_COMMON_FIELDS into
DeterministicStepSpec/STEP_FIELDS_BY_TYPE.deterministic, and out of
compileStep's shared `base` into the deterministic branch. Both conflicts this
rebase produced landed on that hunk and on the `output` lowering beside it.

compile.ts's deterministic branch now reads the SHARED `verification` binding
and carries #138's timeoutMs spread. The two sides of that conflict are equal
today — typedOutputVerification returns step.verification unchanged for a verb
that cannot declare `output` — so either would have passed every test; the
shared binding is kept because it is what holds the invariant that every branch
of the switch reads the lowered gate, not the raw authored one.

Adapts one assertion in #138's new dependency-validation suite: an exact
toEqual on PreflightResult, which this PR widens with `gates`. A refused spec
compiled nothing, so its gate plan is empty. No assertion weakened, no test
added or removed.

Report updates: the base moved twice and this rebase pinned the SHA; the
verification standard the traps expose — validateSpec, preflight and
`flows check` all answer "was the key accepted?", and only compileYaml +
toKernelSpec answers "did it reach the kernel?", so that path is the primary
assertion and the others corroborate; a four-path timeoutMs proof; and #138's
own blob-comparison method applied to all 15 files it touched, with every
deleted line attributed (two were widenings reading as deletions, the same
false-alarm shape #138's signoff found).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
Signoff 4 reproduced the original P0 verbatim on the repaired head: one logical
step executed four times, daemon SIGABRT on run.start and every resume, run
stuck "running". The route was a $ref written as a URI naming an $id declared
inside the same document -- the standard 2020-12 compound-schema-document form
that every bundler emits. Both resolvers keyed on a leading "#", so no edge was
added, no cycle was found, and jsonschema then resolved it from the document's
own resource map and overflowed.

The load-bearing error was the comment justifying that: "an unresolvable
reference is left opaque, validator_for refuses it outright". True for remote
resources, false for an in-document $id. The premise held for one case and was
generalised to both.

Reference resolution is now URI-aware and mirrored function for function across
schema.rs and json-schema-bound.ts: collect_scopes builds a resource map keyed
by resolved base URI AND by the raw $id (consistency between registration and
lookup matters more than exact RFC 3986 normalization, and a bundled document
writes the same literal in both places); anchors are keyed (base URI, name)
instead of document-wide first-match-wins, which closes the duplicate-anchor
crash; resolve splits <uri>#<fragment>, resolves the URI part against the base
in effect at that node, and applies the fragment inside that resource. The
checker stays iterative.

Also settles the divergence in the other direction: a RangeError out of Ajv's
compile is caught and discarded rather than reported as "invalid JSON Schema:
Maximum call stack size exceeded". The rule decides legality, the engine
decides only well-formedness, and a stack overflow is neither verdict -- by the
time Ajv runs the bound has already proved the declaration terminates and the
kernel accepts it. Narrow by construction: a schema the bound refuses never
reaches Ajv.

The corpus is extended by derivation from the specification's reference forms
rather than from the file: F1-F12, each with a refused instance and, where the
form can express one, an accepted instance. 12 -> 20 refused, 14 -> 22 accepted.
F12 gets its own engineRefused bucket that pins BOTH halves of the narrowed
premise -- the bound must not claim these, the engine must refuse them -- so a
future engine that accepts an unresolvable reference fails a test instead of
silently reopening the hole.

Also corrects three things signoff 4 caught in the report: a STEP_FIELDS_BY_TYPE
evidence block quoted from the pre-#138 base, an undisclosed fourth test
adaptation of the gates:[] class in cli.test.ts, and the anchor-scoping item in
"what I did not verify" -- which I had guessed would be a false refusal rather
than a crash, and the guess was wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
origin/main moved to 16860d2 (#151, trigger-key lowering) right after the last
push. Rebased onto the pinned SHA; two conflicts, and the first is trap 2's
shape a fourth time in the same function.

#151 added `triggers: flow.triggers.map(toKernelTrigger)` to toKernelSpec --
authoring keys lowered into the kernel's snake_case dialect, which is authoring
sugar becoming a different object at the boundary, exactly like `output:`. This
branch had changed the same lines from `flow.*` to `compiled.*` for the
snapshot guard. Taking either side wholesale reverts the other; the resolution
is `compiled.triggers.map(toKernelTrigger)`.

The prediction from section 10 held: validateSpec returns ok=true and
`flows check` returns CHECK PASSED exit=0 whether or not the lowering happened.
Only compileYaml + toKernelSpec, read against the kernel object, shows
eventType -> event_type. Unlike the first three traps this one also has
committed fixtures behind it -- #151 pinned a canonical form and a spec hash --
so a reverted lowering would go red in the suite too.

Blob-compared all 14 files #151 touched: 8 byte-identical including both pinned
fixtures, 6 changed by me with every deletion attributed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
@kjgbot
kjgbot force-pushed the feat/v2-gate-contract branch from 1ed9023 to 4da825b Compare September 4, 2026 08:13
kjgbot pushed a commit that referenced this pull request Sep 4, 2026
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
@kjgbot

kjgbot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Merging: green CI, independent signoff carried, zero unresolved threads

CI green at 4da825b — linux-x64-artifact pass 6m21s.

Getting there took three runs, and the two reds in between were not this PR:

I re-ran rather than attributing, which is the rule I wrote up on #156 after getting it wrong twice.

The signoff carries, proven rather than assumed. It returned PASSED at 8b7148d; this head is 4da825b after a rebase onto 4df6d94. Every one of the PR's own non-workflow files hashed at both heads:

PR touches 39 non-workflow files
differing from signed-off head 8b7148d: 0

Only .github/ differs. That signoff was a serious one — 7 mutations, each with before/mutated/restored SHA-256 plus a failing and a passing witness, covering both conflict resolutions and all four known silent-revert traps:

  • all six protocol helpers defined exactly once, in server/protocol.rs, none duplicated into server.rs
  • run.start maps SpecError → invalid_spec, not internal
  • both spec.rs validations (surface identity, JSON Schema) fire
  • timeoutMs deterministic-only in both the base spread and the allowlist
  • .map(toKernelTrigger) survives
  • compileStep's base/per-verb output lowering intact

Zero unresolved review threads.

For the record on the rebase itself: the conflict was trap 1 — main had moved five protocol helpers out of server.rs into server/protocol.rs, and this branch re-added all five. Taking "both sides" would have silently duplicated them. I verified each function's definition count against main before resolving, kept main's, and relocated only run_start_error — the one genuinely new. The signoff independently re-derived the same conclusion.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

@kjgbot
kjgbot merged commit f1314b1 into main Sep 4, 2026
3 of 5 checks passed
kjgbot pushed a commit that referenced this pull request Sep 4, 2026
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
kjgbot added a commit that referenced this pull request Sep 4, 2026
* feat(sdk): settle data and code gate contract

Session-Id: 01a062df-0cdf-7f23-89dd-121aa9ecf743

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* fix(sdk): fail closed on invalid gates

Session-Id: 01a062df-0cdf-7f23-89dd-121aa9ecf743

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* fix(sdk): bundle JSON Schema draft metadata

Session-Id: 01a062df-0cdf-7f23-89dd-121aa9ecf743

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* fix(sdk): fail closed at gate boundaries

Session-Id: 01a062df-0cdf-7f23-89dd-121aa9ecf743

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* fix(runtime): close gate boundary execution holes

Session-Id: 01a062df-0cdf-7f23-89dd-121aa9ecf743

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* fix(sdk): reject proxies at exported boundaries

Session-Id: 01a062df-0cdf-7f23-89dd-121aa9ecf743

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* fix(gates): bound JSON Schema declarations so validation terminates

A `json_schema` gate whose `$ref` graph cycles through only in-place
applicators compiles cleanly and then recurses without bound the first time
it validates an output. In Rust that aborts the process, so `run.start`
accepted the spec, created the journal, ran the step's command, and then took
relayflowd down with SIGABRT — leaving a run stuck `running` that re-executed
its effect on every resume (4 executions of one logical step, no
`completionReason`, no `step.completed`). RFC-0001 covenant 2 and gate 1.

A stack overflow cannot be caught, so the bound is structural and runs before
the declaration is accepted: reject a reference cycle that re-applies to the
same instance and therefore makes no progress. Cycles through a child
applicator (`properties`, `items`, `prefixItems`, ...) consume one level of
the instance per step and stay legal, so ordinary recursive schemas are
unaffected.

`kernel/relayflowd-core/src/schema.rs` and `sdk/src/json-schema-bound.ts`
implement the same rule and are pinned to a shared corpus in
`testdata/json-schema-bound-cases.json`, so the kernel and the SDK agree on
which schemas are legal by construction rather than by coincidence of Ajv's
catchable RangeError and Rust's uncatchable abort. That also closes the
reported SDK/kernel divergence on a self-recursive `$defs`. Every corpus
refusal compiles cleanly in `jsonschema`, which is what makes the tests test
the bound and not the mechanism. `verify` now compiles through the same gate.

Also from the same review:
- `canonicalize`/`specHash` are exported unknown-input helpers, so they carry
  the snapshot guard the rest of the exported surface already has, and each
  key is read exactly once instead of twice (a demonstrated getter TOCTOU).
- A `json_schema` gate that accepts every output (`{}`, `true`, annotations
  only) is still legal, but `flows check` marks the line and preflight emits a
  `vacuous_gate` warning: a gate that judges nothing must not read like one
  that judges something.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* fix(sdk): gate output declarations through the same schema bound

Rebase onto main brought in #133's `output:` sugar, which lowers to a
`json_schema` gate at compile time but was only checked with `isObject`. So an
`output` schema the kernel refuses passed `flows check` and was reported as a
gate — an unbounded `$ref` cycle included. Route it through `jsonSchemaError`,
the same gate a hand-written `verification: {type: json_schema}` clears.

Also adapts one test to main's boundary contract rather than deleting it:
`preflight` now returns a named `invalid_spec` refusal where it used to throw,
so the proxy-boundary test accepts either refusal shape and treats "returned a
usable result" as a failure. Trap and getter counters are untouched.

Adds ops/reviews/20260903-pr139-repair-0903.md: the red four-execution ladder,
the structural fix, the shared kernel/SDK corpus, the three-path `output`
proof, the silently-merged-file enumeration, and every gate with literal
output. It also records that origin/main moved from 3da71e2 to 990093b (#136)
mid-work and that this branch is rebased onto 990093b, not the pinned SHA.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* docs(review): name the second silent-revert trap and characterise the kernel exit-101

Two conflict resolutions on this rebase could have silently reverted freshly
merged behaviour, not one. The brief named step-fields.ts; compile.ts's
compileStep is the sharper one, because neither validateSpec nor `flows check`
can see it — both still report a healthy gate when the `output:` lowering has
been reverted to the raw authored gate. Only compileYaml + toKernelSpec, read
against the kernel verification object, tells "the key was accepted" apart from
"the key became a gate". Records that, and that one of the reverting lines
auto-merged without git flagging a conflict.

Also characterises the kernel gate's one exit-101-with-zero-failures rather
than leaving it as flake: the binary is named (relayflowd-core spec_parity, and
only that one), disk is ruled out at 30 GiB free, one clean reproduction
attempt came back green, and concurrent load is named as the untested
condition. Recorded as unexplained.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* fix(sdk): keep timeoutMs deterministic-only through the rebase onto #138

#138 moved `timeoutMs` out of BaseStepSpec/STEP_COMMON_FIELDS into
DeterministicStepSpec/STEP_FIELDS_BY_TYPE.deterministic, and out of
compileStep's shared `base` into the deterministic branch. Both conflicts this
rebase produced landed on that hunk and on the `output` lowering beside it.

compile.ts's deterministic branch now reads the SHARED `verification` binding
and carries #138's timeoutMs spread. The two sides of that conflict are equal
today — typedOutputVerification returns step.verification unchanged for a verb
that cannot declare `output` — so either would have passed every test; the
shared binding is kept because it is what holds the invariant that every branch
of the switch reads the lowered gate, not the raw authored one.

Adapts one assertion in #138's new dependency-validation suite: an exact
toEqual on PreflightResult, which this PR widens with `gates`. A refused spec
compiled nothing, so its gate plan is empty. No assertion weakened, no test
added or removed.

Report updates: the base moved twice and this rebase pinned the SHA; the
verification standard the traps expose — validateSpec, preflight and
`flows check` all answer "was the key accepted?", and only compileYaml +
toKernelSpec answers "did it reach the kernel?", so that path is the primary
assertion and the others corroborate; a four-path timeoutMs proof; and #138's
own blob-comparison method applied to all 15 files it touched, with every
deleted line attributed (two were widenings reading as deletions, the same
false-alarm shape #138's signoff found).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* fix(gates): resolve $ref as a URI, closing the compound-document bypass

Signoff 4 reproduced the original P0 verbatim on the repaired head: one logical
step executed four times, daemon SIGABRT on run.start and every resume, run
stuck "running". The route was a $ref written as a URI naming an $id declared
inside the same document -- the standard 2020-12 compound-schema-document form
that every bundler emits. Both resolvers keyed on a leading "#", so no edge was
added, no cycle was found, and jsonschema then resolved it from the document's
own resource map and overflowed.

The load-bearing error was the comment justifying that: "an unresolvable
reference is left opaque, validator_for refuses it outright". True for remote
resources, false for an in-document $id. The premise held for one case and was
generalised to both.

Reference resolution is now URI-aware and mirrored function for function across
schema.rs and json-schema-bound.ts: collect_scopes builds a resource map keyed
by resolved base URI AND by the raw $id (consistency between registration and
lookup matters more than exact RFC 3986 normalization, and a bundled document
writes the same literal in both places); anchors are keyed (base URI, name)
instead of document-wide first-match-wins, which closes the duplicate-anchor
crash; resolve splits <uri>#<fragment>, resolves the URI part against the base
in effect at that node, and applies the fragment inside that resource. The
checker stays iterative.

Also settles the divergence in the other direction: a RangeError out of Ajv's
compile is caught and discarded rather than reported as "invalid JSON Schema:
Maximum call stack size exceeded". The rule decides legality, the engine
decides only well-formedness, and a stack overflow is neither verdict -- by the
time Ajv runs the bound has already proved the declaration terminates and the
kernel accepts it. Narrow by construction: a schema the bound refuses never
reaches Ajv.

The corpus is extended by derivation from the specification's reference forms
rather than from the file: F1-F12, each with a refused instance and, where the
form can express one, an accepted instance. 12 -> 20 refused, 14 -> 22 accepted.
F12 gets its own engineRefused bucket that pins BOTH halves of the narrowed
premise -- the bound must not claim these, the engine must refuse them -- so a
future engine that accepts an unresolvable reference fails a test instead of
silently reopening the hole.

Also corrects three things signoff 4 caught in the report: a STEP_FIELDS_BY_TYPE
evidence block quoted from the pre-#138 base, an undisclosed fourth test
adaptation of the gates:[] class in cli.test.ts, and the anchor-scoping item in
"what I did not verify" -- which I had guessed would be a false refusal rather
than a crash, and the guess was wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

* docs(review): record the rebase onto #151 and the fourth trap

origin/main moved to 16860d2 (#151, trigger-key lowering) right after the last
push. Rebased onto the pinned SHA; two conflicts, and the first is trap 2's
shape a fourth time in the same function.

#151 added `triggers: flow.triggers.map(toKernelTrigger)` to toKernelSpec --
authoring keys lowered into the kernel's snake_case dialect, which is authoring
sugar becoming a different object at the boundary, exactly like `output:`. This
branch had changed the same lines from `flow.*` to `compiled.*` for the
snapshot guard. Taking either side wholesale reverts the other; the resolution
is `compiled.triggers.map(toKernelTrigger)`.

The prediction from section 10 held: validateSpec returns ok=true and
`flows check` returns CHECK PASSED exit=0 whether or not the lowering happened.
Only compileYaml + toKernelSpec, read against the kernel object, shows
eventType -> event_type. Unlike the first three traps this one also has
committed fixtures behind it -- #151 pinned a canonical form and a spec hash --
so a reverted lowering would go red in the suite too.

Blob-compared all 14 files #151 touched: 8 byte-identical including both pinned
fixtures, 6 changed by me with every deletion attributed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

---------

Co-authored-by: kjgbot <kjgbot@agentrelay.dev>
Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
kjgbot pushed a commit that referenced this pull request Sep 5, 2026
…aked test daemons

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
kjgbot pushed a commit that referenced this pull request Sep 6, 2026
…o merged

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
kjgbot pushed a commit that referenced this pull request Sep 8, 2026
…s stale

#134 and #139 both merged four days ago and the allSettled fix is on main in
refactored form — two of my checks gave false negatives (stale path, grep for
the old branch's identifiers) before I confirmed the behaviour. Verified the
docs disclosure the code comment claims rather than trusting it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR
kjgbot pushed a commit that referenced this pull request Sep 8, 2026
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR
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.

1 participant