Skip to content

unprovable_effects warns once per step and fires on nearly every real spec, so the warning channel carries no signal - #621

Merged
AgentRelayBot merged 4 commits into
mainfrom
relayflow/flows-software-garden-83d7850d
Oct 8, 2026
Merged

AgentRelayBot merged 4 commits into
mainfrom
relayflow/flows-software-garden-83d7850d

Conversation

@agent-relay-code

@agent-relay-code agent-relay-code Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

flows check now collapses repeated unprovable_effects warnings into one line,
so a suspicious command remains visible. --explain-warnings restores every
original per-step diagnostic, including in watch mode. JSON stdout retains the
original report; only its accompanying stderr is summarised.

The change follows reviewed-plan.md: folding is an explicit renderer opt-in,
starts at two affected steps, counts distinct step IDs, and preserves other
kinds in order before appending the summary. The denominator includes every step.
The ladder examples consequently read 2 of 2, 2 of 3, and 2 of 3.

Scope: fixes check on YAML/JSON specs. The external 58-step spec's format has
not been confirmed. Authored TypeScript checks do not currently produce these
step diagnostics. run/resume/deploy retain their current rendering, with a
follow-up recorded in ops/BACKLOG.md. Stdout still prints each GATE line.
No effect proving, spec-schema changes, kernel changes, or workflow edits.

Regression coverage includes opt-in isolation, single-step and duplicate-step
cases, missing step IDs, exact diagnostic ordering, full per-step explanation,
JSON serialization equality, invalid flags, and real watch-child forwarding.
The run-renderer regression uses a deliberate daemon-unreachable invocation
with --no-spawn; it verifies warning rendering, not live kernel execution.
The explicit per-step explanation assertion supplies coverage that the existing
ladder prefix assertions no longer provide after folding.

The relay-cli-surface.test.ts change only adds an INVOCATIONS sample for the
newly declared flag. Existing gate assertions remain intact; no covenant-2
assertion or preflight implementation was edited.

Verification commands and captured output follow. All final commands below
completed successfully. During implementation, the first build was mistakenly
launched at the repository root (no package.json); the first SDK build found an
import inserted ahead of the CLI shebang (TS18026/TS1005). The import was moved
below the shebang before the successful build and checks captured here.

$ cd packages/sdk && npm run build

> @relayflows/sdk@2.0.42 build
> tsc && node scripts/make-cli-executable.mjs

$ cd packages/sdk && npm run typecheck

> @relayflows/sdk@2.0.42 typecheck
> tsc --noEmit && tsc -p tsconfig.type-tests.json

$ cd packages/sdk && npm run typecheck:tests

> @relayflows/sdk@2.0.42 typecheck:tests
> tsc -p tsconfig.tests.json

$ cd packages/sdk && npx vitest run tests/check-warning-summary.test.ts

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

 ✓ tests/check-warning-summary.test.ts (12 tests) 1676ms
   ✓ flows check warning presentation > forwards explain=false to the watch child 822ms
   ✓ flows check warning presentation > forwards explain=true to the watch child 768ms

 Test Files  1 passed (1)
      Tests  12 passed (12)
   Start at  15:20:06
   Duration  3.75s (transform 1.24s, setup 13ms, collect 1.89s, tests 1.68s, environment 0ms, prepare 46ms)

$ cd packages/sdk && npx vitest run tests/cli.test.ts tests/preflight.test.ts tests/cli-watch.test.ts tests/relay-cli-surface.test.ts

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

 ✓ tests/relay-cli-surface.test.ts (88 tests) 105ms
 ✓ tests/preflight.test.ts (70 tests) 198ms
 ✓ tests/cli.test.ts (71 tests) 3586ms
   ✓ flows check CLI > binds a checked relative wrapper to the flow directory for worker execution 888ms
   ✓ flows check CLI > resolves a bare PATH-resolved claude with no declared model, in an isolated PATH 534ms
   ✓ flows run/resume CLI over the journal protocol > follows a worker wait past a locally expired lease until the daemon settles it 1040ms
 ✓ tests/cli-watch.test.ts (10 tests) 16133ms
   ✓ flows check --watch > rechecks syntax errors, clears once, and returns the last refusal on Ctrl-C 1590ms
   ✓ flows check --watch > streams JSON lines without ANSI, recovers after atomic saves, and exits zero after repair 1848ms
   ✓ flows check --watch > coalesces 20 concurrent saves into at most two rechecks 1809ms
   ✓ flows check --watch > watches transitive relative use imports, cycles, and nearest config changes 2287ms
   ✓ flows check --watch > refreshes the import graph and notices missing imports being created 2358ms
   ✓ flows check --watch > reloads authored TypeScript instead of reusing the first imported definition 1833ms
   ✓ flows check --watch > detects a nearer config appearing and falls back after it is deleted 1832ms
   ✓ flows check --watch > keeps watching after the target is deleted and recreated 1800ms
   ✓ flows check --watch > queues changes during a slow check without overlapping checks 773ms

 Test Files  4 passed (4)
      Tests  239 passed (239)
   Start at  15:20:21
   Duration  20.60s (transform 2.80s, setup 202ms, collect 10.08s, tests 20.02s, environment 1ms, prepare 323ms)

The ten-step demonstration fixture (outside the checkout) was:

version: '0.1.0'
name: many
steps:
  - id: s1
    type: deterministic
    command: 'node -e "0"'
  - id: s2
    type: deterministic
    command: 'node -e "0"'
  - id: s3
    type: deterministic
    command: 'node -e "0"'
  - id: s4
    type: deterministic
    command: 'node -e "0"'
  - id: s5
    type: deterministic
    command: 'node -e "0"'
  - id: s6
    type: deterministic
    command: 'node -e "0"'
  - id: s7
    type: deterministic
    command: 'node -e "0"'
  - id: s8
    type: deterministic
    command: 'node -e "0"'
  - id: s9
    type: deterministic
    command: 'if [ -f x ]; then printf found; fi'
  - id: s10
    type: deterministic
    command: 'definitely-not-a-real-binary'

Its sibling /tmp/foldcheck/flows.json contains {}.

$ node packages/sdk/dist/cli.js check /tmp/foldcheck/many.flow.yaml
WARNING [command_unresolved] Step "s10" command "definitely-not-a-real-binary" does not resolve as an executable; it runs only if the shell supplies it.
WARNING [editor_schema_missing] For editor validation, add this first line: # yaml-language-server: $schema=https://schema.relayflows.dev/v0.1/flows.schema.json
WARNING [unprovable_effects] 9 of 10 steps have effects that cannot be proven before execution (rerun with --explain-warnings to list them).
GATE step "s1" exit_code from data (kernel, journal-replayable)
GATE step "s2" exit_code from data (kernel, journal-replayable)
GATE step "s3" exit_code from data (kernel, journal-replayable)
GATE step "s4" exit_code from data (kernel, journal-replayable)
GATE step "s5" exit_code from data (kernel, journal-replayable)
GATE step "s6" exit_code from data (kernel, journal-replayable)
GATE step "s7" exit_code from data (kernel, journal-replayable)
GATE step "s8" exit_code from data (kernel, journal-replayable)
GATE step "s9" exit_code from data (kernel, journal-replayable)
GATE step "s10" exit_code from data (kernel, journal-replayable)
CHECK PASSED /tmp/foldcheck/many.flow.yaml
$ node packages/sdk/dist/cli.js check --explain-warnings /tmp/foldcheck/many.flow.yaml
WARNING [unprovable_effects] Step "s1" command "node" resolves, but its effects cannot be proven before execution.
WARNING [unprovable_effects] Step "s2" command "node" resolves, but its effects cannot be proven before execution.
WARNING [unprovable_effects] Step "s3" command "node" resolves, but its effects cannot be proven before execution.
WARNING [unprovable_effects] Step "s4" command "node" resolves, but its effects cannot be proven before execution.
WARNING [unprovable_effects] Step "s5" command "node" resolves, but its effects cannot be proven before execution.
WARNING [unprovable_effects] Step "s6" command "node" resolves, but its effects cannot be proven before execution.
WARNING [unprovable_effects] Step "s7" command "node" resolves, but its effects cannot be proven before execution.
WARNING [unprovable_effects] Step "s8" command "node" resolves, but its effects cannot be proven before execution.
WARNING [unprovable_effects] Step "s9" starts with the shell reserved word "if", whose effects cannot be proven before execution.
WARNING [command_unresolved] Step "s10" command "definitely-not-a-real-binary" does not resolve as an executable; it runs only if the shell supplies it.
WARNING [editor_schema_missing] For editor validation, add this first line: # yaml-language-server: $schema=https://schema.relayflows.dev/v0.1/flows.schema.json
GATE step "s1" exit_code from data (kernel, journal-replayable)
GATE step "s2" exit_code from data (kernel, journal-replayable)
GATE step "s3" exit_code from data (kernel, journal-replayable)
GATE step "s4" exit_code from data (kernel, journal-replayable)
GATE step "s5" exit_code from data (kernel, journal-replayable)
GATE step "s6" exit_code from data (kernel, journal-replayable)
GATE step "s7" exit_code from data (kernel, journal-replayable)
GATE step "s8" exit_code from data (kernel, journal-replayable)
GATE step "s9" exit_code from data (kernel, journal-replayable)
GATE step "s10" exit_code from data (kernel, journal-replayable)
CHECK PASSED /tmp/foldcheck/many.flow.yaml
$ node packages/sdk/dist/cli.js check --json /tmp/foldcheck/many.flow.yaml 2>/tmp/warning-json-stderr.log | jq '[.diagnostics[].kind] | group_by(.) | map({(.[0]): length}) | add'
{
  "command_unresolved": 1,
  "editor_schema_missing": 1,
  "unprovable_effects": 9
}
$ node packages/sdk/dist/cli.js check testdata/hello-deterministic.flow.yaml
WARNING [editor_schema_missing] For editor validation, add this first line: # yaml-language-server: $schema=https://schema.relayflows.dev/v0.1/flows.schema.json
WARNING [unprovable_effects] 2 of 2 steps have effects that cannot be proven before execution (rerun with --explain-warnings to list them).
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
$ node packages/sdk/dist/cli.js check testdata/hello-llm.flow.yaml
WARNING [editor_schema_missing] For editor validation, add this first line: # yaml-language-server: $schema=https://schema.relayflows.dev/v0.1/flows.schema.json
WARNING [unprovable_effects] 2 of 3 steps have effects that cannot be proven before execution (rerun with --explain-warnings to list them).
GATE step "greet" exit_code+output_contains from data (kernel, journal-replayable)
GATE step "answer" json_schema from data (kernel, journal-replayable)
GATE step "finish" exit_code from data (kernel, journal-replayable)
RESOLVED step "answer" cli "./preflight/authenticated-cli" from project (/home/daytona/.relayflow-v2-supervisor/durable/repository/testdata/flows.json) model "deterministic-test-stub" from step
REQUIRES claude (step "answer")
CHECK PASSED testdata/hello-llm.flow.yaml
$ node packages/sdk/dist/cli.js check testdata/hello-agent.flow.yaml
WARNING [permissions_unenforced] Step "edit" declares permissions (fileGlobs, accessPreset). This declaration is validated and recorded with the step spec; it is not currently enforced (gate 8 / #442). These permissions do not restrict file or network access.
WARNING [editor_schema_missing] For editor validation, add this first line: # yaml-language-server: $schema=https://schema.relayflows.dev/v0.1/flows.schema.json
WARNING [unprovable_effects] 2 of 3 steps have effects that cannot be proven before execution (rerun with --explain-warnings to list them).
GATE step "greet" exit_code+output_contains from data (kernel, journal-replayable)
GATE step "edit" output_contains from data (kernel, journal-replayable)
GATE step "finish" exit_code from data (kernel, journal-replayable)
RESOLVED step "edit" cli "./preflight/authenticated-cli" from project (/home/daytona/.relayflow-v2-supervisor/durable/repository/testdata/flows.json) model "test-model-v1" from step
REQUIRES claude (step "edit")
WARNING [agent_worker_unresolved] 1 agent step ("edit") requires an attached worker. For a local run, use `flows run --local-agent <flow>`, unless you already attach an agent worker for this daemon. `flows check` does not verify worker attachment: with no worker attached the run parks at the first agent step.
CHECK PASSED testdata/hello-agent.flow.yaml

JSON stderr captured by the command above:

WARNING [command_unresolved] Step "s10" command "definitely-not-a-real-binary" does not resolve as an executable; it runs only if the shell supplies it.
WARNING [editor_schema_missing] For editor validation, add this first line: # yaml-language-server: $schema=https://schema.relayflows.dev/v0.1/flows.schema.json
WARNING [unprovable_effects] 9 of 10 steps have effects that cannot be proven before execution (rerun with --explain-warnings to list them).

Checks

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

What ran (.relayflow/check.sh)
#!/bin/sh
# What CI checks, on a fresh machine.
#
# Mirrors .github/workflows/cloud-runtime-artifact.yml (the gate that runs on
# every PR touching the kernel or the SDK) plus the non-publishing halves of
# schema-publish.yml and surface-package.yml. Run it from anywhere:
#
#   sh .relayflow/check.sh
#
# Deliberately NOT here, because each needs a secret, a deployment target or a
# service this machine does not have:
#   * publish.yml's npm publish / OIDC steps and softprops/action-gh-release
#     (NPM_TOKEN, id-token, a release tag). Its one test step -- `node --test
#     scripts/publish.test.mjs` -- does run below.
#   * schema-publish.yml's npm + pages jobs (NPM_TOKEN, github-pages
#     environment, a vX.Y.Z tag). Its validate job runs below.
#   * actions/upload-artifact steps: there is no CI artifact store here.
#   * review-swarm.yml: it launches a cloud agent swarm and posts to a PR
#     (RELAY_API_KEY, cloud login, a GitHub token, an open PR).
#   * `sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0` and the
#     `bwrap --unshare-all` smoke that CI runs after it. CI does that on an
#     ephemeral VM it owns; this machine's kernel is not ours to retune. The
#     tests that need bubblewrap either self-skip when /usr/bin/bwrap is absent
#     (`it.skipIf`) or fail loudly -- which is the honest outcome, not a pass.
#     On a container the sysctl is not merely impolite but impossible: it is not
#     namespaced, so even with passwordless sudo it answers
#     `sysctl: permission denied on key "kernel.apparmor_restrict_unprivileged_userns"`
#     and every `bwrap --unshare-*` still dies with
#     `loopback: Failed RTM_NEWADDR: Operation not permitted`. See
#     .relayflow/repair-notes.md for the captured probes.
set -e

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

# CI gets its toolchain from dtolnay/rust-toolchain@stable and then calls plain
# `cargo`. A fresh machine may ship no Rust at all, so go through the repo's own
# wrapper: it uses `cargo` from PATH when there is one, and otherwise bootstraps
# rustup into $HOME/.relayflows-toolchain. CARGO_TARGET_DIR is pinned to
# kernel/target (gitignored) so the release binary lands where CI puts it and
# the SDK suite can be pointed at it below.
CARGO_TARGET_DIR="$repo_root/kernel/target"
export CARGO_TARGET_DIR
cargo() { ( cd "$repo_root/kernel" && sh ../ops/cargo.sh "$@" ); }

# ---------------------------------------------------------------- dependencies

# Hosted-extension isolation runs the real /usr/bin/bwrap. CI apt-installs it;
# do the same when it is missing and this machine grants non-interactive root.
if ! command -v bwrap >/dev/null 2>&1; then
  if command -v sudo >/dev/null 2>&1 && sudo -n true 2>/dev/null; then
    sudo -n apt-get update
    sudo -n apt-get install --yes --no-install-recommends bubblewrap
  else
    echo "check.sh: no bubblewrap and no passwordless sudo to install one;" >&2
    echo "check.sh: hosted-extension sandbox tests will skip or fail." >&2
  fi
fi

# CI pins bun with oven-sh/setup-bun@v2 `bun-version: 1.4.0` in all four
# workflows this script mirrors, and
# packages/sdk/tests/authored-node-runtime.test.ts asserts that exact version
# before it builds a standalone CLI -- the standalone target is bun's own
# single-file executable, so the version IS the artifact. A machine whose PATH
# bun is anything else fails that suite at beforeAll ("expected '1.3.6' to be
# '1.4.0'"), which is the pin working, not a bug. Fetch the pinned release into
# the same private toolchain home ops/cargo.sh uses and put it first on PATH, so
# `bun` here and FLOWS_BUILD_BUN inside the build script are both 1.4.0.
bun_pin=1.4.0
if [ "$(command -v bun >/dev/null 2>&1 && bun --version 2>/dev/null)" != "$bun_pin" ]; then
  bun_home="${RELAYFLOWS_TOOLCHAIN_HOME:-$HOME/.relayflows-toolchain}/bun-$bun_pin"
  if [ ! -x "$bun_home/bin/bun" ]; then
    case "$(uname -s)/$(uname -m)" in
      Linux/x86_64) bun_asset=bun-linux-x64 ;;
      Darwin/arm64) bun_asset=bun-darwin-aarch64 ;;
      *) echo "check.sh: no pinned bun build for $(uname -s)/$(uname -m)." >&2; exit 127 ;;
    esac
    echo "check.sh: PATH bun is not $bun_pin; installing the pinned one into $bun_home" >&2
    bun_zip="${TMPDIR:-/tmp}/$bun_asset-$bun_pin.zip"
    curl -fsSL --max-time 300 -o "$bun_zip" \
      "https://github.com/oven-sh/bun/releases/download/bun-v$bun_pin/$bun_asset.zip"
    rm -rf "$bun_home" "$bun_zip.d"
    mkdir -p "$bun_home/bin" "$bun_zip.d"
    # Debian images ship no unzip; python3 is present wherever node is.
    if command -v unzip >/dev/null 2>&1; then
      unzip -oq "$bun_zip" -d "$bun_zip.d"
    else
      python3 -c "import sys,zipfile; zipfile.ZipFile(sys.argv[1]).extractall(sys.argv[2])" "$bun_zip" "$bun_zip.d"
    fi
    install -m 0755 "$bun_zip.d/$bun_asset/bun" "$bun_home/bin/bun"
    rm -rf "$bun_zip" "$bun_zip.d"
  fi
  PATH="$bun_home/bin:$PATH"
  export PATH
fi
FLOWS_BUILD_BUN="$(command -v bun)"
export FLOWS_BUILD_BUN
[ "$(bun --version)" = "$bun_pin" ] || {
  echo "check.sh: bun is $(bun --version), not the pinned $bun_pin." >&2; exit 127; }

# testdata/preflight's agent-CLI fixtures are extensionless executables whose
# bodies are ESM (`#!/usr/bin/env node` + `import ... from
# './wrapper-session.mjs'`). Node decides an extensionless file's module kind
# from the nearest ancestor package.json, and a checkout on this machine lives
# under a $HOME that has an unrelated package.json declaring
# `"type": "commonjs"` -- at which point node runs those fixtures as CommonJS,
# which with ESM syntax exits 0 having produced nothing at all. Every agent step
# driven by one then completes with verification `{gate: execution, verdict:
# fail}` and output null, failing seven cases in tests/live-kernel.test.ts. A
# CI checkout has no ancestor package.json, so CI never needs this; it is a
# property of where this machine puts the tree. Declare the fixture directory's
# module kind locally. Uncommitted and untracked on purpose: the repo has no
# root package.json by design, and this is environment repair, not a change to
# the branch under test.
[ -e testdata/preflight/package.json ] \
  || echo '{"type":"module"}' > testdata/preflight/package.json

( cd packages/surface && bun install --frozen-lockfile --ignore-scripts && bun run build )

# --ignore-scripts because the surface is already built above; without it npm
# runs the file: dependency's prepare before its own devDependencies exist. The
# SDK's own build is a step below, so nothing is skipped.
npm ci --prefix packages/sdk --ignore-scripts

# `npm ci` installs @relayflows/surface from the REGISTRY, not the local build.
# Any SDK change importing a symbol that has not been published yet then fails
# typecheck. --no-save keeps the committed manifest unchanged; the ./ prefix is
# load-bearing (without it npm reads the path as a GitHub org/repo shorthand).
npm install ./packages/surface --prefix packages/sdk --no-save --ignore-scripts

# workflows/*.flow.ts and regressions/ import @relayflows/surface from the repo
# root, where a checkout has no node_modules at all.
mkdir -p node_modules/@relayflows
ln -sfn ../../packages/sdk/node_modules/@relayflows/surface node_modules/@relayflows/surface
node -e "console.log(require.resolve('@relayflows/surface'))"

# ------------------------------------------------------------ code generation

# schema-publish.yml: the committed schema must be what the generator emits, and
# the generator must be deterministic across two runs.
node scripts/generate-json-schema.mjs
git diff --exit-code -- packages/schema/flows.schema.json
cp packages/schema/flows.schema.json "${TMPDIR:-/tmp}/flows.schema.first.json"
node scripts/generate-json-schema.mjs
diff -q "${TMPDIR:-/tmp}/flows.schema.first.json" packages/schema/flows.schema.json

# ------------------------------------------------------------------- builds

cargo build --locked --release -p relayflowd

# test:prep's other half: re-assert the executable bit on the preflight CLI
# fixtures. They are committed 100755, but the failure this prevents is an
# opaque EACCES deep inside a preflight test.
[ ! -d testdata/preflight ] || find testdata/preflight -name '*-cli' -type f -exec chmod +x {} +

npm run typecheck --prefix packages/sdk
npm run build --prefix packages/sdk
npm run typecheck:tests --prefix packages/sdk

# -------------------------------------------------------------------- tests

node --test scripts/cloud-artifact.test.mjs
node --test scripts/publish.test.mjs

cargo test --workspace

( cd packages/schema && bun run test )

# The SDK suite. RELAYFLOWD_BIN points at the release binary built above rather
# than compiling a second debug copy through `npm test`'s test:prep.
#
# RELAYFLOWS_ALLOW_ANALYZER_SKIP=1 for the same reason CI sets it: one case in
# tests/live-kernel.test.ts runs against the real Claude analyzer and fails by
# default when it cannot, deliberately. Setting the flag is an explicit
# statement that **this script is not gate-2 acceptance evidence** -- that has
# to come from a machine with model access, and the skipped case says so in its
# own output. Everything else in the suite still runs and still gates.
( cd packages/sdk \
  && RELAYFLOWS_ALLOW_ANALYZER_SKIP=1 \
     RELAYFLOWD_BIN="$repo_root/kernel/target/release/relayflowd" \
     ./node_modules/.bin/vitest run )

# ------------------------------------------- artifact assembly and CLI smoke

# cloud-runtime-artifact.yml's remaining steps, minus the upload. The standalone
# target is per-platform, so take it from this machine; CI only ever builds the
# two that packaging ships.
case "$(uname -s)/$(uname -m)" in
  Linux/x86_64) standalone_target=bun-linux-x64 ;;
  Darwin/arm64) standalone_target=bun-darwin-arm64 ;;
  *) standalone_target='' ;;
esac

if [ -z "$standalone_target" ]; then
  echo "check.sh: no standalone bun target for $(uname -s)/$(uname -m); skipping the artifact smoke." >&2
else
  mkdir -p dist/cloud-artifact-input
  node scripts/build-standalone-cli.mjs "$standalone_target" dist/cloud-artifact-input/flows

  node scripts/cloud-artifact.mjs build \
    --relayflowd kernel/target/release/relayflowd \
    --flows-executable dist/cloud-artifact-input/flows \
    --output-dir dist/cloud-artifact \
    --source-commit "$(git rev-parse HEAD)"
  archive="$(find dist/cloud-artifact -name '*.tar.gz' -type f -print -quit)"
  node scripts/cloud-artifact.mjs verify \
    --archive "$archive" \
    --sha256 "$(awk '{print $1}' "$archive.sha256")"

  rm -rf dist/cloud-artifact-smoke
  mkdir -p dist/cloud-artifact-smoke
  tar -xzf "$archive" -C dist/cloud-artifact-smoke
  dist/cloud-artifact-smoke/bin/relayflowd --help
  flows_output="$(dist/cloud-artifact-smoke/bin/flows check --json testdata/hello-deterministic.flow.yaml)"
  printf '%s\n' "$flows_output"
  node -e '
    const report = JSON.parse(process.argv[1]);
    if (report.ok !== true) throw new Error("flows smoke report was not ok");
    if (report.path !== "testdata/hello-deterministic.flow.yaml") {
      throw new Error(`flows smoke reported unexpected path: ${report.path}`);
    }
  ' "$flows_output"
fi

# ----------------------------------------- surface packed-consumer gate (last)

# surface-package.yml in full: surface tests, regression/example typechecks, and
# a consumer built against a packed tarball. It runs last because it reinstalls
# packages/sdk's @relayflows/surface from that tarball, replacing the local
# directory install above. Needs bash, which is what CI invokes it with.
bash scripts/surface-package-gate.sh

echo "check.sh: OK"
Output on this branch (last 80 lines)

 ❯ tests/hosted-extension-protocol.test.ts:447:5
    445|   ])('rejects an import-time %s frame with zero adapter calls', async …
    446|     let calls = 0;
    447|     await expect(runVerifiedNativeExtensionSandbox({
       |     ^
    448|       artifact: await artifact(hostileImport([frame, { type: 'error', …
    449|       manifest: validateFlowExtensionManifest(manifest()), dispatch: d…

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[14/24]⎯

 FAIL  tests/hosted-extension-protocol.test.ts > hosted extension hostile protocol > rejects two forged calls after the authoritative first outcome settles
 FAIL  tests/hosted-extension-protocol.test.ts > hosted extension hostile protocol > waits for a pending adapter to reject after a forged child error
 FAIL  tests/hosted-extension-protocol.test.ts > hosted extension hostile protocol > waits for a pending adapter to resolve after a forged child error
 FAIL  tests/hosted-extension-protocol.test.ts > hosted extension hostile protocol > returns a typed adapter rejection even when the hostile child hangs
Error: hostile child did not invoke the adapter
 ❯ Timeout._onTimeout tests/hosted-extension-protocol.test.ts:135:45
    133| async function waitForInvocation(invoked: Promise<void>): Promise<void…
    134|   await new Promise<void>((resolve, reject) => {
    135|     const timeout = setTimeout(() => reject(new Error('hostile child d…
       |                                             ^
    136|     void invoked.then(() => { clearTimeout(timeout); resolve(); }, rej…
    137|   });

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[15/24]⎯

 FAIL  tests/software-garden-babysitter-composition.test.ts > canonical Software Garden + Babysitter composition > propagates capability denial without a retry or fallback
AssertionError: expected Error: Hosted extension sandbox exited wi… { code: '…' } to be Error: live babysit label is absent // Object.is equality

- Expected
+ Received

- [Error: live babysit label is absent]
+ [Error: Hosted extension sandbox exited without a valid completion (exit 1): bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted
+ ]

 ❯ tests/software-garden-babysitter-composition.test.ts:139:7
    137|       const refusal = new Error('live babysit label is absent');
    138|       let calls = 0;
    139|       await expect(runHostedSoftwareGardenBabysitter({
       |       ^
    140|         flowPath: installed.flowPath,
    141|         dispatch: dispatch(),

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[16/24]⎯

⎯⎯⎯⎯⎯⎯ Unhandled Errors ⎯⎯⎯⎯⎯⎯

Vitest caught 1 unhandled error during the test run.
This might cause false positive tests. Resolve unhandled errors to make sure your tests are not affected.

⎯⎯⎯⎯ Unhandled Rejection ⎯⎯⎯⎯⎯
Error: Hosted extension sandbox exited without a valid completion (exit 1): bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted

 ❯ refuse src/hosted-extension-protocol.ts:135:21
    133|       : new PluginError('plugin_unsupported', 'Hosted capability rejec…
    134|     const refuse = (message: string) => {
    135|       const error = new PluginError('plugin_unsupported', message);
       |                     ^
    136|       CHILD_PROCESS_KILL(child, 'SIGKILL');
    137|       if (capabilityState === 'pending') {
 ❯ ChildProcess.<anonymous> src/hosted-extension-protocol.ts:234:21
 ❯ ChildProcess.emit node:events:520:22
 ❯ maybeClose node:internal/child_process:1084:16
 ❯ Process.ChildProcess._handle.onexit node:internal/child_process:304:5

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
Serialized Error: { code: 'plugin_unsupported' }
This error originated in "tests/hosted-extension-protocol.test.ts" test file. It doesn't mean the error was thrown inside the file itself, but while it was running.
The latest test that might've caused the error is "rejects two forged calls after the authoritative first outcome settles". It might mean one of the following:
- The error was thrown, while Vitest was running this test.
- If the error occurred after the test had been completed, this was the last documented test before it was thrown.
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯

 Test Files  4 failed | 246 passed | 1 skipped (251)
      Tests  24 failed | 3832 passed | 4 skipped (3860)
     Errors  1 error
   Start at  15:58:02
   Duration  599.06s (transform 6.95s, setup 1.15s, collect 70.22s, tests 1658.18s, environment 29ms, prepare 9.40s)

Output on the base commit (last 80 lines)

 ❯ tests/hosted-extension-protocol.test.ts:447:5
    445|   ])('rejects an import-time %s frame with zero adapter calls', async …
    446|     let calls = 0;
    447|     await expect(runVerifiedNativeExtensionSandbox({
       |     ^
    448|       artifact: await artifact(hostileImport([frame, { type: 'error', …
    449|       manifest: validateFlowExtensionManifest(manifest()), dispatch: d…

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[14/24]⎯

 FAIL  tests/hosted-extension-protocol.test.ts > hosted extension hostile protocol > rejects two forged calls after the authoritative first outcome settles
 FAIL  tests/hosted-extension-protocol.test.ts > hosted extension hostile protocol > waits for a pending adapter to reject after a forged child error
 FAIL  tests/hosted-extension-protocol.test.ts > hosted extension hostile protocol > waits for a pending adapter to resolve after a forged child error
 FAIL  tests/hosted-extension-protocol.test.ts > hosted extension hostile protocol > returns a typed adapter rejection even when the hostile child hangs
Error: hostile child did not invoke the adapter
 ❯ Timeout._onTimeout tests/hosted-extension-protocol.test.ts:135:45
    133| async function waitForInvocation(invoked: Promise<void>): Promise<void…
    134|   await new Promise<void>((resolve, reject) => {
    135|     const timeout = setTimeout(() => reject(new Error('hostile child d…
       |                                             ^
    136|     void invoked.then(() => { clearTimeout(timeout); resolve(); }, rej…
    137|   });

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[15/24]⎯

 FAIL  tests/software-garden-babysitter-composition.test.ts > canonical Software Garden + Babysitter composition > propagates capability denial without a retry or fallback
AssertionError: expected Error: Hosted extension sandbox exited wi… { code: '…' } to be Error: live babysit label is absent // Object.is equality

- Expected
+ Received

- [Error: live babysit label is absent]
+ [Error: Hosted extension sandbox exited without a valid completion (exit 1): bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted
+ ]

 ❯ tests/software-garden-babysitter-composition.test.ts:139:7
    137|       const refusal = new Error('live babysit label is absent');
    138|       let calls = 0;
    139|       await expect(runHostedSoftwareGardenBabysitter({
       |       ^
    140|         flowPath: installed.flowPath,
    141|         dispatch: dispatch(),

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[16/24]⎯

⎯⎯⎯⎯⎯⎯ Unhandled Errors ⎯⎯⎯⎯⎯⎯

Vitest caught 1 unhandled error during the test run.
This might cause false positive tests. Resolve unhandled errors to make sure your tests are not affected.

⎯⎯⎯⎯ Unhandled Rejection ⎯⎯⎯⎯⎯
Error: Hosted extension sandbox exited without a valid completion (exit 1): bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted

 ❯ refuse src/hosted-extension-protocol.ts:135:21
    133|       : new PluginError('plugin_unsupported', 'Hosted capability rejec…
    134|     const refuse = (message: string) => {
    135|       const error = new PluginError('plugin_unsupported', message);
       |                     ^
    136|       CHILD_PROCESS_KILL(child, 'SIGKILL');
    137|       if (capabilityState === 'pending') {
 ❯ ChildProcess.<anonymous> src/hosted-extension-protocol.ts:234:21
 ❯ ChildProcess.emit node:events:520:22
 ❯ maybeClose node:internal/child_process:1084:16
 ❯ Process.ChildProcess._handle.onexit node:internal/child_process:304:5

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
Serialized Error: { code: 'plugin_unsupported' }
This error originated in "tests/hosted-extension-protocol.test.ts" test file. It doesn't mean the error was thrown inside the file itself, but while it was running.
The latest test that might've caused the error is "rejects two forged calls after the authoritative first outcome settles". It might mean one of the following:
- The error was thrown, while Vitest was running this test.
- If the error occurred after the test had been completed, this was the last documented test before it was thrown.
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯

 Test Files  4 failed | 246 passed | 1 skipped (251)
      Tests  24 failed | 3832 passed | 4 skipped (3860)
     Errors  1 error
   Start at  16:09:25
   Duration  612.55s (transform 6.45s, setup 1.07s, collect 69.73s, tests 1676.79s, environment 29ms, prepare 9.32s)

What the repair agent found

Repair notes — relayflow/flows-software-garden-83d7850d

.relayflow/check.sh failed on this branch with 31 failed tests + 1 failed
suite
. None of them trace to the branch's change (flows check warning
folding). Three distinct root causes, all in the machine rather than in the
code:

Cause Failures Status
PATH bun is 1.3.6; CI pins 1.4.0 1 suite (tests/authored-node-runtime.test.ts) fixed in check.sh (not committed)
$HOME/package.json declares "type": "commonjs" 7 tests (tests/live-kernel.test.ts) fixed in check.sh (not committed)
kernel.apparmor_restrict_unprivileged_userns=1 and the sysctl is not writable here 24 tests (hosted-extension isolation/protocol, babysitter native extension, software-garden composition) outside this run's control

1. bun version (fixed as setup)

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

packages/sdk/tests/authored-node-runtime.test.ts:18 asserts the bun version
before building a standalone CLI, because the standalone target is a bun
single-file executable. All four workflows check.sh mirrors pin it:

$ grep -rn "bun-version" .github/workflows/
.github/workflows/schema-publish.yml:43:          bun-version: '1.4.0'
.github/workflows/publish.yml:78:          bun-version: '1.4.0'
.github/workflows/publish.yml:196:          bun-version: '1.4.0'
.github/workflows/cloud-runtime-artifact.yml:58:          bun-version: "1.4.0"
.github/workflows/surface-package.yml:40:          bun-version: "1.4.0"

check.sh had no bun step at all — it used whatever bun PATH offered, which on
this machine is nvm's 1.3.6. It now fetches the pinned release into
$HOME/.relayflows-toolchain/bun-1.4.0 (the same private toolchain home
ops/cargo.sh uses), prepends it to PATH, exports FLOWS_BUILD_BUN, and fails
closed if the result is not 1.4.0.

2. $HOME/package.json with "type": "commonjs" (fixed as setup)

Seven tests/live-kernel.test.ts cases failed with the agent step's
verification reading {gate: 'execution', verdict: 'fail'} and output: null,
e.g.

 FAIL  tests/live-kernel.test.ts > built flows CLI against live relayflowd > runs hn-monitor analyze-story end-to-end via a stub agent CLI (gate 2 clause 2 demo)
-   "output": Object { "reasoning": "stub agent runtime …", "relevance_score": 5, "story_title": "stub" },
+   "output": null,
    "verification": Object {
-     "gate": "json_schema", "verdict": "pass",
+     "gate": "execution",  "verdict": "fail",

The fixtures those steps execute (testdata/preflight/analyze-story-stub-cli
and friends) are extensionless executables whose bodies are ESM:

$ head -3 testdata/preflight/analyze-story-stub-cli
#!/usr/bin/env node
// Deterministic gate-2 wrapper fixture.
import { receiveWrapperRequest } from './wrapper-session.mjs';

Node decides an extensionless file's module kind from the nearest ancestor
package.json. This checkout lives at
/home/daytona/.relayflow-v2-supervisor/durable/repository, and the harness
image ships an unrelated /home/daytona/package.json containing
"type": "commonjs". That turns module detection off, the fixture is loaded as
CommonJS, and node then exits 0 having produced nothing — no output, no
error:

$ printf '%s' '{"protocol":"relayflows-agent-cli-v1","instruction":"hi"}' \
    | testdata/preflight/analyze-story-stub-cli --relayflows-adapter-v1; echo "EXIT=$?"
EXIT=0

Reduced to first principles, outside the repo entirely:

$ mkdir -p /tmp/pkgt && cd /tmp/pkgt
$ echo '{"type":"commonjs"}' > package.json
$ printf 'export const z=1;\n' > m.mjs
$ printf 'import {z} from "./m.mjs";\nconsole.log("ran2");\n' > cli2
$ node cli2; echo "EXIT=$?"
EXIT=0
$ printf 'console.log("plain");\n' > cli3 && node cli3; echo "EXIT=$?"
plain
EXIT=0

Not a node-version artifact — node 22 (what CI pins) behaves identically:

$ ~/.relayflows-toolchain/node22/bin/node --version
v22.23.3
$ ~/.relayflows-toolchain/node22/bin/node testdata/preflight/analyze-story-stub-cli; echo "EXIT=$?"
EXIT=0

A CI checkout has no ancestor package.json, so CI never sees this. check.sh
now writes testdata/preflight/package.json = {"type":"module"} during setup
if it is absent. That is deliberately not committed and not tracked: the
repo has no root package.json by design, and this is environment repair, not
a change to the branch under test. With it in place all 32 live-kernel cases
pass (see §4).

Same cause, visible outside the test suite — flows check on an authored .ts
flow also breaks here, which is worth knowing when hand-verifying anything:

$ node packages/sdk/dist/cli.js check examples/stale-issues/stale-issues.flow.ts
(node:56319) Warning: Failed to load the ES module: …/stale-issues.flow.ts. Make sure to set "type": "module" in the nearest package.json file or use the .mjs extension.
REFUSED [invalid_spec] Flow "…/stale-issues.flow.ts" could not be imported: Cannot use import statement outside a module

check.sh does not paper over that one, because no test in the suite depends on
it. If a future run needs it, the fix is the same one line in examples/.

Durable fix worth a human's call (not done here, out of this task's scope):
commit {"type":"module"} package.json files next to the extensionless ESM
fixtures, or a root one. That makes the fixtures independent of wherever the
tree happens to be checked out, instead of relying on no ancestor declaring a
type.

3. bubblewrap cannot create a network namespace (outside this run's control)

24 failures, all one error:

Error: Hosted extension sandbox exited without a valid completion (exit 1): bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted
 ❯ refuse src/hosted-extension-protocol.ts:135:21

bubblewrap is installed and new enough; it simply cannot unshare a namespace
here:

$ bwrap --version
bubblewrap 0.12.0
$ bwrap --unshare-all --ro-bind / / /bin/true; echo "exit=$?"
bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted
exit=1
$ bwrap --unshare-net --ro-bind / / /bin/true; echo "exit=$?"
bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted
exit=1
$ bwrap --unshare-pid --ro-bind / / /bin/true; echo "exit=$?"
bwrap: setting up uid map: Permission denied
exit=1

CI's remedy is sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0.
This machine has passwordless sudo, and the sysctl is still refused — it is
not namespaced, so a container cannot write it:

$ cat /proc/sys/kernel/apparmor_restrict_unprivileged_userns
1
$ sudo -n true; echo "sudo_exit=$?"
sudo_exit=0
$ sudo -n sysctl -w kernel.apparmor_restrict_unprivileged_userns=0; echo "exit=$?"
sysctl: permission denied on key "kernel.apparmor_restrict_unprivileged_userns"
exit=1
$ bwrap --unshare-all --ro-bind / / /bin/true; echo "exit=$?"
bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted
exit=1

So the hosted-extension sandbox suites cannot run on this machine at all. This
is exactly the case check.sh's own header already anticipates ("CI does that on
an ephemeral VM it owns; this machine's kernel is not ours to retune … fail
loudly — which is the honest outcome, not a pass"). Left failing; no check.sh
change, no code change. These 24 cases are unverified by this run and need
a machine that can unshare a user namespace.

Affected files: tests/hosted-extension-isolation.test.ts (13),
tests/hosted-extension-protocol.test.ts (8),
tests/software-garden-babysitter-composition.test.ts (2),
tests/babysitter-native-extension.test.ts (1).

Unrelated note: node version

CI pins actions/setup-node@v4 node-version: "22"; this machine runs node
v25.6.0 and no other version is installed. No failure in this run traces to
that (§2 reproduces identically under node 22), so check.sh was left using the
PATH node rather than growing a second toolchain bootstrap.

Fixes #510


Summary by cubic

flows check now folds repeated unprovable_effects warnings into a single trailing stderr line, so rarer diagnostics like command_unresolved stay visible. Use the new --explain-warnings flag (also with --watch) to list each warning per step; --json stdout always retains the full original report, and only the stderr rendering is summarized.

  • Folding kicks in when two or more distinct steps warn; other diagnostic kinds keep their original order and stay individual.
  • The denominator counts every step in the spec, including llm and agent steps.
  • Scope is limited to check on YAML/JSON specs: run, resume, and deploy keep their current rendering, with a follow-up noted in ops/BACKLOG.md.
  • The CI suite failures come from the machine (bun version, a $HOME/package.json, bubblewrap without a writable userns sysctl) and reproduce identically on the base commit.

Written for commit 0fa9174. Summary will update on new commits.

View guided diff Turn on auto-fix


Note

Low Risk
CLI presentation and flag parsing only; preflight diagnostics and JSON reports are unchanged aside from stderr folding on check.

Overview
flows check now collapses repeated unprovable_effects warnings on YAML/JSON specs into a single trailing line when two or more distinct steps are affected (denominator is total step count). Other warning kinds stay separate and in order, so warnings like command_unresolved remain visible.

Adds --explain-warnings (including with --watch) to print every per-step warning again. --json stdout still carries the full diagnostic list; only human stderr is summarized. run, resume, and deploy are unchanged.

Implementation is presentation-only via foldUnprovableEffects in diagnostic-fold.ts, wired through emitCheckReport / emitDiagnostics. Docs and CLI surface metadata are updated; a backlog item notes extending the same pattern to other verbs later.

Reviewed by Cursor Bugbot for commit 0fa9174. Bugbot is set up for automated code reviews on this repo. Configure here.

@coderabbitai

coderabbitai Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Bot user detected.

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

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 593338ab-aa46-4a3e-a35f-6c8844b638a2

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

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

❤️ Share

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

agentrelaybot added 2 commits October 7, 2026 23:36
…are-garden-83d7850d

Session-Id: 0e078a4c-f89d-4e74-9919-00e1e9a08879
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Session-Id: 0e078a4c-f89d-4e74-9919-00e1e9a08879
@AgentRelayBot

Copy link
Copy Markdown
Contributor

Finishing pass. No Garden adversarial review or review threads were recorded for this PR, so I reviewed it myself:

  • Merged origin/main (no conflicts).
  • Removed the leaked relayflow working file summary.md at the repo root, which duplicated this description (5a-style working file; same fix as the Garden's own 'Keep relayflow working files out of the change' commits).
  • Checked the fold: every unprovable_effects site in preflight.ts is severity: 'warning', so folding cannot hide a refusal. Other kinds keep their order, and --json stdout keeps the original report.
  • Local results: build, typecheck and typecheck:tests pass. check-warning-summary, preflight, cli-watch and relay-cli-surface pass. In cli.test.ts, two 'flows check CLI' wrapper-path cases fail identically on unchanged main on this Mac and pass in CI.

bugbot run

@AgentRelayBot
AgentRelayBot marked this pull request as ready for review October 8, 2026 06:38
@AgentRelayBot

Copy link
Copy Markdown
Contributor

@coderabbitai review

@AgentRelayBot

Copy link
Copy Markdown
Contributor

bugbot run

@coderabbitai

coderabbitai Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review skipped.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit e08cc12. Configure here.

…are-garden-83d7850d

# Conflicts:
#	packages/sdk/src/cli.ts

Session-Id: 0e078a4c-f89d-4e74-9919-00e1e9a08879
@AgentRelayBot

Copy link
Copy Markdown
Contributor

Merged origin/main (with #622 --detach) in 0fa9174. I resolved the cli.ts import, ParsedArgs and USAGE conflicts by keeping both --detach and --explain-warnings. Build, typecheck, typecheck:tests, and check-warning-summary, relay-cli-surface, cli-detach, detached-start and preflight (188/188) pass.

@AgentRelayBot
AgentRelayBot merged commit dc15d20 into main Oct 8, 2026
9 checks passed
@AgentRelayBot
AgentRelayBot deleted the relayflow/flows-software-garden-83d7850d branch October 8, 2026 08:46
AgentRelayBot pushed a commit that referenced this pull request Oct 8, 2026
Combine both flows check options in cli.ts, cli-commands.ts, cli-watch.ts
(the watch child forwards the daemon mode and --explain-warnings), and
the CLI surface samples. #621's JSON byte-identity test now builds its
expected report in the CLI's daemon validation mode, since check --json
carries the validation field.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Session-Id: 0e078a4c-f89d-4e74-9919-00e1e9a08879
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.

unprovable_effects warns once per step and fires on nearly every real spec, so the warning channel carries no signal

1 participant