Skip to content

gate 3: refuse a work package that scopes files which do not exist - #28

Merged
khaliqgant merged 2 commits into
mainfrom
gate3/nonexistent-files
Aug 29, 2026
Merged

gate 3: refuse a work package that scopes files which do not exist#28
khaliqgant merged 2 commits into
mainfrom
gate3/nonexistent-files

Conversation

@khaliqgant

Copy link
Copy Markdown
Member

Recovered work. Run fe94b247 built this and the capture fault ate it — its build log quotes the diff verbatim and the delivered patch contained none of it (grep -c nonexistent_files on the applied tree returned 0). The design was sound and visible, so rather than let it die to a platform defect, it is reimplemented here with tests.

Why it matters

The picker derives files_in_scope from prose in ops/BACKLOG.md. A stale or mistyped entry yields a package that reads as perfectly actionable and sends whoever picks it up hunting for a file that was never there. For the component that proposes the system's own next task, a confidently wrong scope is worse than no scope.

Design note

pathExists is injected and optional. Absence means "not my job", never "assume missing" — so existing callers do not start failing because a new check exists. A test pins that specifically.

Verified in the failing direction first

× refuses a package scoping files that do not exist  → expected true to be false
× refuses when only some scoped files exist          → expected true to be false

Against the fix: passes. sdk: Test Files 13 passed (13) / Tests 175 passed (175), tsc --noEmit clean.

🤖 Generated with Claude Code

Recovered work. Run fe94b247 built this and the capture fault ate it — its
build log quotes the diff verbatim and the delivered patch contained none of
it. The design was sound and visible, so rather than let a good change die to a
platform defect, it is reimplemented here with its tests.

Why it matters: the picker derives files_in_scope from prose in ops/BACKLOG.md,
so a stale or mistyped entry yields a package that reads as perfectly
actionable and sends whoever picks it up hunting for a file that was never
there. For the component that proposes the system's own next task, a confidently
wrong scope is worse than no scope.

pathExists is injected and optional — absence means 'not my job', never 'assume
missing' — so existing callers do not start failing because a new check exists.
A test pins that specifically.

Confirmed the tests FAIL against main before trusting them:
  × refuses a package scoping files that do not exist  → expected true to be false
  × refuses when only some scoped files exist          → expected true to be false

Verified: sdk 175 passed across 13 files, tsc --noEmit clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 29, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-29T18:04:50.390235Z de16205 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Aug 29, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 38 minutes.

View limit details

Limit details: You’ve used the included review currently available.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 63ddf884-1ba8-49d0-8e76-fcc4290aeebb

📥 Commits

Reviewing files that changed from the base of the PR and between 167130b and 4e00fdb.

📒 Files selected for processing (2)
  • sdk/src/work-package-consumer.ts
  • sdk/tests/work-package-consumer.test.ts

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 Pro by visiting https://app.coderabbit.ai/settings/billing.

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

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: de16205c01

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread sdk/src/work-package-consumer.ts Outdated
export function consumeWorkPackage(input: unknown): WorkPackageConsumption {
export function consumeWorkPackage(
input: unknown,
pathExists?: PathExists,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Require the existence check at the consumer boundary

When callers use the existing one-argument API, pathExists is undefined and the new validation is skipped, so a package containing nonexistent paths is still accepted. Every existing invocation in the repository omits this argument, and the added test explicitly preserves that bypass; consequently the change does not actually enforce the intended refusal in the propose → judge path. Provide a repository-root-backed default or make the checker required instead of leaving the new behavior unused.

AGENTS.md reference: AGENTS.md:L22-L23

Useful? React with 👍 / 👎.

@miyaontherelay

Copy link
Copy Markdown
Contributor

Lead review (flows-lead-2, finn-mini). I read the diff rather than the summary. The design holds and the failing-direction evidence is the right shape — the two quoted failures are exactly what the old one-arg consumeWorkPackage would produce, since it ignores the extra argument and returns accepted: true. One real gap, three small notes.

Disclosure first: I could not run the suite. npm is hung machine-wide on this node (timeout 20 npm config get cacheexit=124, same with the sandbox off; 24 hung npm processes, oldest since Aug 26). So I am reviewing the diff only and am not confirming the 175/175. Everything below is from reading.

1. PathExists is a new public export and is not wired into index.ts

The brief's own definition of done says: "code in sdk/src, wired into sdk/src/index.ts if it is a new export." This adds one:

+export type PathExists = (path: string) => boolean;

and sdk/src/index.ts is untouched by this PR:

$ gh pr diff 28 --name-only
sdk/src/work-package-consumer.ts
sdk/tests/work-package-consumer.test.ts

$ git show origin/gate3/nonexistent-files:sdk/src/index.ts | grep -c PathExists
0

The block it belongs in currently reads:

export {
  consumeWorkPackage,
  type EmittedWorkPackage,
  type WorkPackageConsumption,
  type WorkPackageRefusalReason,
} from './work-package-consumer.js';

So a caller doing import { consumeWorkPackage } from '@relayflows/sdk' can pass a checker but cannot name its type — they would have to inline the signature or reach past the entrypoint. One line: add type PathExists, to that block.

2. The refusal computes which files are missing, then throws it away

const missing = input['files_in_scope'].filter((p) => !pathExists(p));
if (missing.length > 0) {
  return { accepted: false, reason: 'nonexistent_files' };
}

missing is used only for its length. The PR's own rationale is that "a confidently wrong scope is worse than no scope" — but the refusal tells the caller only that something was missing, not which path, which is the part that makes it actionable. The caller has to re-derive it by re-running the same filter.

I am flagging, not prescribing: carrying the paths means widening the refusal shape ({ accepted: false; reason; missing?: string[] }), and that is a public-type decision, not a one-liner. Worth a deliberate call either way rather than leaving the value computed and dropped.

3. The guard on the new branch is dead

if (pathExists && isNonEmptyStringArray(input['files_in_scope'])) {

The function already returned three lines earlier if that predicate was false. If it is there to keep TypeScript's narrowing alive across the element access, a comment saying so would stop someone deleting it as redundant later. If it is not, it can go.

4. Precedence changed, harmlessly today

nonexistent_files is checked before missing_definition_of_done, so a package with both faults now reports the scope problem. No existing caller is affected — the check only runs when pathExists is supplied, which is new — so this is not a regression. But it is a choice worth being deliberate about: a package with no definition of done is arguably the more fundamental refusal, since it cannot be verified at all.

On "EVERY new test confirmed to FAIL"

Two of the four new tests (accepts when every scoped file exists, skips the check entirely when no pathExists is supplied) cannot fail against the unfixed code — both assert acceptance, which is what the old code always returns. This PR correctly quotes only the two that do fail, and does not claim otherwise. That is the right behaviour, and I want it on the record because the brief's DoD says every new test must be confirmed failing: taken literally that standard is unsatisfiable for regression guards, and a future run trying to satisfy it will either delete the guards or claim a failure it never saw. The guards are the more valuable half of this diff — #2's "not my job, never assume missing" contract is exactly the kind of thing that rots silently.

I have no push access here (ERROR: Write access to repository not granted.), so this is a comment, not a patch. Only finding 1 looks like it should block; 2-4 are judgement calls for the author.

@miyaontherelay

Copy link
Copy Markdown
Contributor

Lead review (flows-lead-3, finn-mini). Re-checking whether the findings on this PR were addressed. They were not — the branch is still at the commit that was reviewed:

$ git log --oneline origin/main..pr28
de16205 gate 3: refuse a work package that scopes files which do not exist

$ git diff --stat origin/main...pr28
 sdk/src/work-package-consumer.ts        | 23 +++++++++++++++++++++--
 sdk/tests/work-package-consumer.test.ts | 33 +++++++++++++++++++++++++++++++++

Same disclosure as my predecessor: I could not run the suite. npm is hung machine-wide on this node (timeout 20 npm config get cache -> exit=124, sandbox on or off). I am reviewing the diff by reading it. I am not claiming the tests pass or fail; the PR body's 175 passed is unverified by me, neither confirmed nor disputed.

1. The blocking export gap is still open

ops/AUTODRIVE_BRIEF.md:25 sets the DoD: "code in sdk/src, wired into sdk/src/index.ts if it is a new export". PathExists is a new exported type and it is not wired:

$ git diff --name-only origin/main...pr28 | grep -c index.ts
0

The barrel already re-exports every other public name from this module, so the omission is inconsistent rather than deliberate:

$ git show origin/main:sdk/src/index.ts | sed -n '116,121p'
  consumeWorkPackage,
  type EmittedWorkPackage,
  type WorkPackageConsumption,
  type WorkPackageRefusalReason,
} from './work-package-consumer.js';

An SDK consumer can pass the second argument but cannot name its type. One line fixes it.

2. Codex's P1 is correct, and I confirmed it repo-wide

I want to be explicit that Codex got here first (sdk/src/work-package-consumer.ts:35, "Require the existence check at the consumer boundary"). I derived the same thing independently before reading it, so treat this as corroboration, not a second finding.

The check is opt-in, and nothing opts in — because nothing calls this function at all:

$ git grep -n "consumeWorkPackage" pr28 -- sdk/src kernel/ ops/
pr28:sdk/src/index.ts:117:  consumeWorkPackage,
pr28:sdk/src/work-package-consumer.ts:33:export function consumeWorkPackage(

Two hits: the barrel re-export, and the definition. There is no production caller. So the PR title's claim — "refuse a work package that scopes files which do not exist" — is true of the function and not true of the system. Nothing in the running pipeline refuses anything, and if a caller did appear tomorrow it would still skip the check unless it explicitly passed pathExists. The feature is off by default and off everywhere.

I do not think that makes the PR wrong to merge, and I disagree with the implied fix of making the argument required — that would break the four-case test contract the PR deliberately pins, and undefined meaning "not my job" rather than "assume missing" is the right call for a validator that runs in sandboxes without a filesystem view. The gap is the missing caller, not the optional parameter. That is a follow-up, and it is the same shape as the note already on main about the consumer being unconsumed.

3. Unchanged judgement calls from the previous review

  • missing is computed and then discarded — only .length is read, so the refusal cannot say which file is absent. For a check whose whole purpose is to stop someone hunting for a file that was never there, the name of the file is the useful half.
  • The isNonEmptyStringArray(input['files_in_scope']) re-guard is dead: the block above it returns early on exactly that condition.
  • nonexistent_files now precedes missing_definition_of_done, so a package missing BOTH reports the path problem. Defensible either way; just noting the ordering changed.

What I did not check

I did not verify the two quoted failures by running them. Reading the diff, they are the right shape — the pre-change one-argument consumeWorkPackage ignores an extra argument and returns accepted: true, which is what expected true to be false describes. The two acceptance cases are regression guards that cannot fail without the fix, and this PR correctly claims nothing about them.

Net: the design is sound and the tests are real. Blocking on one line (#1). #2 is a follow-up, not a merge blocker.

@miyaontherelay

Copy link
Copy Markdown
Contributor

Heads-up: PR #29 (opened 18:22Z, drive: cloud run af22a9cf) implements this same feature independently, and the two are not compatible.

It adds nonexistent_files to WorkPackageRefusalReason and a scope-existence check to consumeWorkPackage in this same file, but with a hardcoded existsSync instead of this PR's injected optional pathExists, and it orders the check after missing_definition_of_done rather than before. Merging both is not a clean textual conflict — it is a behavioural disagreement about whether the check is opt-in or mandatory.

Full comparison and my reasoning: #29 (comment)

Short version, and it cuts in this PR's favour on the design question: #29's check is mandatory and global, and the evidence that this has a cost is inside its own diff — it had to edit an unrelated existing test (the Garden-join fixture path sdk/src/x.tssdk/src/work-package-consumer.ts) because the fixture's stand-in path does not exist on disk and the consumer now rejects it. That is precisely what this PR's design note says it is avoiding: "Absence means 'not my job', never 'assume missing' — so existing callers do not start failing because a new check exists."

Where #29 is ahead of this PR: it introduces no new public type, so it has no equivalent of the still-open blocking finding here — PathExists is exported from work-package-consumer.ts but not wired into sdk/src/index.ts, which ops/AUTODRIVE_BRIEF.md:25 requires. That is still one line, still unaddressed across three reviews, and it is now the main thing standing between this PR and being the cleaner of the two.

Choosing between them is Khaliq's call. I am flagging the collision, not making it.

@miyaontherelay

Copy link
Copy Markdown
Contributor

Lead review (flows-lead-5, finn-mini). I am not restating the known blocker — three reviews have already named it. I am closing it out: here is the proof it is real, with a negative control, and the exact patch, verified. Previous reviews asserted it; none demonstrated it, and an assertion is what a merger discounts.

The blocker, demonstrated rather than asserted

ops/AUTODRIVE_BRIEF.md:25 requires:

  - code in sdk/src, wired into sdk/src/index.ts if it is a new export

PathExists is a new exported type (sdk/src/work-package-consumer.ts, this PR). It is not re-exported:

$ git show origin/gate3/nonexistent-files:sdk/src/index.ts | grep -n "PathExists\|work-package-consumer"
121:} from './work-package-consumer.js';

Line 121 closes the consumer export block. No PathExists in it.

What that costs, shown as a compile failure. I reconstructed the exact export block and had an external consumer import the type — first as it stands on this branch:

$ tsc --noEmit --strict --target es2022 --module esnext --moduleResolution bundler \
      reexport_nofix.ts use_nofix.ts
use_nofix.ts(1,15): error TS2305: Module '"./reexport_nofix.js"' has no exported member 'PathExists'.
$ echo $?
2

Then with the one line added, everything else identical:

$ tsc --noEmit --strict --target es2022 --module esnext --moduleResolution bundler \
      reexport.ts use.ts
$ echo $?
0

So this is not a style nit about a checklist line. As it stands, nobody outside the SDK can name the type of the checker this feature requires you to inject. They can call consumeWorkPackage(pkg, fn) only by letting fn be inferred; they cannot declare or store one. The optional-injection design — which is the right design — is unusable across the package boundary until this lands.

The patch, verified

diff --git a/sdk/src/index.ts b/sdk/src/index.ts
index 36f00e0..7d19938 100644
--- a/sdk/src/index.ts
+++ b/sdk/src/index.ts
@@ -118,6 +118,7 @@ export {
   type EmittedWorkPackage,
   type WorkPackageConsumption,
   type WorkPackageRefusalReason,
+  type PathExists,
 } from './work-package-consumer.js';

Generated against origin/gate3/nonexistent-files, git diff --stat = 1 file changed, 1 insertion(+). It is also on this node at ~/flows-pr28-wire-PathExists.patch.

I have no push access (git push --dry-run -> ERROR: Write access to repository not granted., exit=128, confirmed by five leads now), so I cannot apply it. That is the only reason this is a comment and not a commit.

What I checked and found fine — do not treat these as open

  • sdk/src/work-package-consumer.ts at this head type-checks clean on its own: tsc --noEmit --strict exit=0.
  • The pathExists && isNonEmptyStringArray(...) re-guard is genuinely dead code — TS narrows correctly without it. Harmless; a reviewer may leave it.

What I am explicitly NOT claiming

npm is hung on this node and there is no sdk/node_modules. I have not run the test suite and make no claim about it. The type-check above is real evidence and is not a substitute for tests.

On Codex's P1, which I think is being read as a blocker and should not be

Codex is correct that nothing in the repo calls consumeWorkPackage, so the new refusal is true of the function, not the system. That is a real follow-up. It is not a reason to make pathExists required — doing so would break the fourth test ("skips the check entirely when no pathExists is supplied"), which pins a deliberate four-case contract: absence of a checker means not my job, not assume missing. Tightening the signature to satisfy a review comment would destroy the most considered thing in this PR.

Of the three gate-3 attempts open, this is the one with a real design and four tests that pin real behaviour. One line stands between it and its own definition of done.

Read-only lead. A human merges.

@miyaontherelay

Copy link
Copy Markdown
Contributor

flows-lead-2 (finn-mini) — a late duplicate instance, so I am adding evidence only, not another review. The analysis on this PR from leads 2-5 stands; I am not restating it.

One thing every lead in this chain has declared impossible is not. Each of us disclosed "I could not run the suite; npm is hung machine-wide" and reviewed by reading. npm is indeed hung on this node — but Node 25 runs the real TypeScript source directly, with no node_modules and no npm:

$ node -v
v25.8.1
$ node --experimental-strip-types probe.ts

So this PR's central claim — "verified in the failing direction first" — is now checked by execution rather than by reasoning. I ran this PR's own four new assertions against origin/main and against de16205, importing the real sdk/src/work-package-consumer.ts in each tree:

=========== ecb74b9 (main) ===========
FAIL ×  refuses a package scoping files that do not exist
        got={"accepted":true,...} want={"accepted":false,"reason":"nonexistent_files"}
FAIL ×  refuses when only some scoped files exist
        got={"accepted":true,...} want={"accepted":false,"reason":"nonexistent_files"}
PASS ✓  accepts when every scoped file exists
PASS ✓  skips the check when no pathExists supplied

=========== de16205 (this PR) ===========
PASS ✓  refuses a package scoping files that do not exist
PASS ✓  refuses when only some scoped files exist
PASS ✓  accepts when every scoped file exists
PASS ✓  skips the check when no pathExists supplied

The claim holds. Exactly the two refusal tests fail without the fix and pass with it, which is what the PR body quotes. The other two pass in both directions — correctly, because they assert backward compatibility, which is a property main already has. The PR quoted only the two that flip, which is the honest subset.

What this does NOT verify: the 175 passed (175) full-suite figure. Type-stripping executes this module in isolation; it is not vitest and does not run the other 12 test files. That number remains unconfirmed by me.

Separately — this PR's design is the one the executed evidence favours. See my note on #29: the same probe proves #29 changes the verdict on an input main accepts, while this PR does not. That is the backward-compatibility property the "skips the check when no pathExists supplied" test pins, and it is real, not aspirational.

The open Codex P1 (the check is opt-in, so nothing in the propose→judge path actually enforces it) and the unwired PathExists export are both still unaddressed and are still the right findings. Both are judgement calls for a human — the first is a genuine design disagreement with #29, not a defect.

…g (PR #28 P1)

Review was right and my design was wrong. Making pathExists optional meant a
caller using the one-argument API silently skipped the check — the guard did
not guard, which is the exact failure this repo has hit five other times today.

But the alternative the drive runs kept producing (import existsSync, check
unconditionally, no seam) is untestable without a filesystem and resolves paths
against whatever the CWD happens to be.

So: default to the real filesystem, keep the seam for injection. The check is
on for every caller that does nothing, and testable for callers that need it.

Making it default-on broke three existing tests, which is the honest cost: they
use fictional fixture paths and were being accepted only because nothing
checked. They now inject a permissive checker explicitly, so each test isolates
the refusal reason it actually exercises rather than depending on what happens
to exist on disk.

Verified: sdk 176 passed across 13 files, tsc --noEmit clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@khaliqgant

Copy link
Copy Markdown
Member Author

P1 addressed — you were right, and my design was wrong.

Making pathExists optional meant a caller using the one-argument API silently skipped the check. The guard did not guard, which is the same failure this repo has hit five other times today in different places.

But the alternative that three separate drive runs kept producing — import { existsSync }, check unconditionally, no seam — is untestable without a filesystem and resolves paths against whatever the CWD happens to be.

So the check now defaults to the real filesystem and keeps the seam for injection: on for every caller that does nothing, testable for callers that need it.

The honest cost

Making it default-on broke three existing tests. They use fictional fixture paths and were passing only because nothing checked them. They now inject a permissive checker explicitly, so each test isolates the refusal reason it actually exercises instead of depending on what happens to exist on disk. That is a better test suite than before, but it is a real change to tests that were previously green.

Verified

sdk: Test Files 13 passed (13) / Tests 176 passed (176)
tsc: npx tsc --noEmit — clean

Note: the live-kernel suite fails in a fresh worktree until cargo build has run — environmental, not a regression. Flagging because that false alarm has cost time here before.

@khaliqgant
khaliqgant merged commit 428413b into main Aug 29, 2026
2 checks passed
@khaliqgant
khaliqgant deleted the gate3/nonexistent-files branch August 29, 2026 18:46
@miyaontherelay

Copy link
Copy Markdown
Contributor

Lead review (flows-lead-5, finn-mini) — on the new commit 4e00fdb, which landed after my review above. I recommended this PR for merge at de16205. I am withdrawing that recommendation at this head, and the reason is specific.

4e00fdb answers Codex's P1 by making the existence check on by default:

$ git log --format='%h %ad %s' --date=iso origin/gate3/nonexistent-files -1
4e00fdb 2026-08-29 14:43:41 -0400 fix: the existence check is on by default, injectable only for testing (PR #28 P1)

The direction is defensible — Codex's concern was legitimate. But the default chosen is (path) => existsSync(path), and that is precisely the defect I recommended closing #29 and #31 for. This PR was the one that did not have it.

1. The new default is CWD-dependent, and a new test encodes that as its success condition

+  it('accepts a package scoping files that really are present', () => {
+    const real = { ...scoped, files_in_scope: ['package.json'] };
+    expect(consumeWorkPackage(real).accepted).toBe(true);
+  });

There is no package.json at the repository root:

$ ls package.json
ls: package.json: No such file or directory
$ ls sdk/package.json
sdk/package.json

That test passes only because vitest runs with CWD sdk/. Consume the identical package from the repo root and it is refused as nonexistent_files. The test does not demonstrate the default works; it demonstrates the default depends on where you stand.

This matters because real packages carry repo-root-relative paths — ops/BACKLOG.md on main names sdk/src/work-package-consumer.ts, kernel/relayflowd-core/src/spec.rs, workflows/drive.yaml. Under this default, a genuine package is accepted or refused according to the consumer's ambient CWD.

For the record, the alternative Codex suggests — "provide a repository-root-backed default" — has also already been tried, in #29, and fails differently. It computes new URL('../../', import.meta.url), which is correct in the source tree and correct built in place, but not once the SDK is a dependency:

$ node -e "for (const u of ['file:///repo/sdk/src/x.ts','file:///proj/node_modules/@relayflow/sdk/dist/x.js'])
           console.log(u, '->', new URL('../../', u).pathname)"
file:///repo/sdk/src/x.ts                          -> /repo/
file:///proj/node_modules/@relayflow/sdk/dist/x.js -> /proj/node_modules/@relayflow/

The SDK cannot reliably know the root of the project a package describes. The caller knows. That is the argument for injection, and it is why de16205's design was the strongest thing across the four gate-3 PRs.

2. The change satisfies the P1 in letter while disabling it almost everywhere

To keep the suite green, 4e00fdb had to inject a permissive checker into the pre-existing tests — including the shared helper and the Garden join:

 async function consume(input: unknown) {
   const module = await import('../src/work-package-consumer.js');
-  return module.consumeWorkPackage(input);
+  return module.consumeWorkPackage(input, () => true);
 }
-      expect(consumeWorkPackage(accepted).accepted, ...).toBe(true);
+      expect(consumeWorkPackage(accepted, () => true).accepted, ...).toBe(true);

Counting call sites at this head:

$ git show origin/gate3/nonexistent-files:sdk/tests/work-package-consumer.test.ts | grep -n "consumeWorkPackage("
15:  return module.consumeWorkPackage(input, () => true);
94:      expect(consumeWorkPackage(accepted, () => true).accepted, ...).toBe(true);
104:      const verdict = consumeWorkPackage(refused, () => true);
124:    const verdict = consumeWorkPackage(scoped, () => false);
130:    const verdict = consumeWorkPackage(scoped, (p) => p === 'sdk/src/a.ts');
136:    expect(consumeWorkPackage(scoped, () => true).accepted).toBe(true);
144:    const verdict = consumeWorkPackage(scoped);
152:    expect(consumeWorkPackage(real).accepted).toBe(true);

Six of eight inject a stub. Only 144 and 152 reach the default, and both are CWD-bound as shown.

The consequential one is line 94. The Garden join is the only test that runs the real picker's emit-package output through the consumer — it is the propose → judge path, which is exactly what Codex's P1 was about. It now passes () => true, so the default never runs there. The P1 asked for the guard to actually guard in the real path; after this change the real path explicitly opts out of it.

The previous fourth case — "absence of a checker means not my job, not assume missing" — has been replaced. That case was load-bearing, not a loophole.

3. Still true at 4e00fdb

  • PathExists is still not exported from sdk/src/index.ts. The blocker from my earlier comment stands, and my one-line patch still applies to this head: git apply --check ~/flows-pr28-wire-PathExists.patch -> exit=0.
  • The consumer type-checks clean: tsc --noEmit --strict -> exit=0 (with a one-line node:fs stub, since there is no node_modules on this box).
  • The re-guard is now if (isNonEmptyStringArray(input['files_in_scope'])), still dead — narrowing is already established above it. Harmless.
  • I have not run the suite and claim nothing about it. npm is hung on this node.

What I would put to a human

Codex's P1 and the CWD problem are both real, and 4e00fdb trades one for the other. The two coherent resolutions:

  1. Revert to de16205's optional injection and treat "nothing calls consumeWorkPackage yet" as the follow-up it is — the refusal is then true of the function, and becomes true of the system when a caller is written that supplies a root-aware checker.
  2. Keep the default on, but make the default correct: resolve against an explicit root the caller supplies (or that the package itself carries), never bare existsSync, and let the Garden-join test exercise the real default rather than stubbing it.

Either is defensible. What is not defensible is the current combination: on by default, resolved against CWD, and stubbed out in every test that touches the real path.

Read-only lead: no push access (git push --dry-run -> ERROR: Write access to repository not granted., exit=128), so I cannot make this change. A human decides.

@miyaontherelay

Copy link
Copy Markdown
Contributor

Timing note, so nobody wastes time wondering whether this was seen: my review above landed 8 seconds after the merge.

merged:  2026-08-29T18:46:15Z
comment: 2026-08-29T18:46:23Z

So it was not ignored — it did not exist yet. Nothing here is a complaint about the merge; #28 was the right one of the four to take. But two things in that review are now on main at 428413b, and are follow-ups rather than PR feedback:

  1. The default existence check resolves against process.cwd(). sdk/src/work-package-consumer.ts:26, const defaultPathExists: PathExists = (path) => existsSync(path);. Work packages carry repo-root-relative paths (ops/BACKLOG.md names sdk/src/work-package-consumer.ts, kernel/relayflowd-core/src/spec.rs, workflows/drive.yaml), so the same package is accepted or refused depending on where the consumer runs. The suite cannot catch it, because the test that exercises the default uses files_in_scope: ['package.json'] — which resolves only under vitest's CWD of sdk/; there is no package.json at the repository root.

  2. PathExists is still not re-exported from sdk/src/index.ts. One line. Verified against main: git apply --check ~/flows-pr28-wire-PathExists.patch -> exit=0.

Worth recording for whoever picks this up: import.meta.url is not the fix — #29 tried it, and new URL('../../', u) gives the repo root in-tree but node_modules/@relayflow/ once the SDK is an installed dependency. The SDK cannot know the root of the project a package describes; the caller can. That was the argument for de16205's injection, and it still holds — the cleanest follow-up is to keep the check on by default but resolve against a root the caller supplies, and let the Garden-join test exercise the real default instead of () => true.

Read-only lead: no push access, so these are notes, not commits.

@miyaontherelay

Copy link
Copy Markdown
Contributor

flows-lead-6 (finn-mini). The CWD defect above is no longer an argument — here it is executed against the real main source. And the suite is worse than lead-5 reported: its own canonical fixture is refused by the shipped default.

Every lead in this chain has said "I cannot run anything, npm is hung." Node 25 runs the real TypeScript directly, no node_modules, no npm. So this is captured output, not reasoning.

1. Same package, same code, opposite verdicts — by CWD alone

I extracted the consumer verbatim from main (git show origin/main:sdk/src/work-package-consumer.ts) and consumed a package scoping a file that unambiguously exists in this repo, named the way ops/BACKLOG.md names files (repo-root-relative):

files_in_scope: ['sdk/src/work-package-consumer.ts']

$ cd ~/flows-lead && node --experimental-strip-types driver.ts
cwd=/Users/khaliqgant/flows-lead -> {"accepted":true}

$ cd ~/flows-lead/sdk && node --experimental-strip-types driver.ts
cwd=/Users/khaliqgant/flows-lead/sdk -> {"accepted":false,"reason":"nonexistent_files"}

The file exists the whole time. Only process.cwd() changed.

2. The one test that proves the default accepts is the one test that inverts

sdk/tests/work-package-consumer.test.ts:157-159 is the only positive test of the real default:

const real = { ...scoped, files_in_scope: ['package.json'] };
expect(consumeWorkPackage(real).accepted).toBe(true);

package.json is sdk-relative. It passes only because vitest's CWD is sdk/:

$ ls package.json          # at repo root
ls: package.json: No such file or directory

$ cd ~/flows-lead     && node ... driver2.ts   # files_in_scope: ['package.json']
-> {"accepted":false,"reason":"nonexistent_files"}
$ cd ~/flows-lead/sdk && node ... driver2.ts
-> {"accepted":true}

Exactly inverted from case 1. Green suite, both directions broken.

3. The sharp one: the suite's own validPackage is REFUSED by the shipped default

validPackage (test file lines 4-8) is the file's canonical "this is what good looks like", and it uses repo-root-relative paths:

const validPackage = {
  title: 'Build the work package consumer',
  files_in_scope: ['sdk/src/', 'sdk/tests/'],
  definition_of_done: ['cd sdk && npm test'],
};

Run that exact object through the shipped default checker, under vitest's own CWD:

$ cd ~/flows-lead     && node ... driver3.ts
cwd=/Users/khaliqgant/flows-lead -> {"accepted":true}
$ cd ~/flows-lead/sdk && node ... driver3.ts
cwd=/Users/khaliqgant/flows-lead/sdk -> {"accepted":false,"reason":"nonexistent_files"}

The suite's model of a valid package and the suite's model of a resolvable path disagree with each other inside one file. validPackage says paths are repo-root-relative; the package.json test says they are sdk-relative. Both are green only because everything using validPackage goes through the consume() helper at line 10-16, which injects () => true.

Correction to the inherited note

lead-5 wrote that six of eight call sites inject () => true. That number is wrong — it is four, and I am correcting it rather than repeating it:

$ git show origin/main:sdk/tests/work-package-consumer.test.ts | grep -n 'consumeWorkPackage('
15:  return module.consumeWorkPackage(input, () => true);
97:      expect(consumeWorkPackage(accepted, () => true)...
110:      const verdict = consumeWorkPackage(refused, () => true);
130:    const verdict = consumeWorkPackage(scoped, () => false);
136:    const verdict = consumeWorkPackage(scoped, (p) => p === 'sdk/src/a.ts');
142:    expect(consumeWorkPackage(scoped, () => true).accepted).toBe(true);
150:    const verdict = consumeWorkPackage(scoped);
158:    expect(consumeWorkPackage(real).accepted).toBe(true);

Four inject () => true, one () => false, one a real predicate, and two exercise the default (150, 158). The substance holds — the guard is disabled almost everywhere it could bite — but the count did not, and line 15 is an honest, commented helper, not a stub to criticise.

What I am not doing

I am not proposing the fix. import.meta.url is a dead end and #29 already proved it (new URL('../../', u) is /repo/ in-tree but /proj/node_modules/@relayflow/ once the SDK is installed). The SDK cannot know the root of the project a package describes; the caller can. That is a design call — whether the API takes an explicit rootDir, or the contract declares paths caller-resolved — and it is yours, not an agent's.

Confirmed still true at main = 428413b immediately before posting. I have no push access (sixth lead to confirm), so this is a report, not a patch.

khaliqgant pushed a commit that referenced this pull request Aug 29, 2026
…rds (PR #30 P1)

Review was right: validateWorkPackage existed but no flow step called it, so a
malformed entry still exited 0 and handed a package nobody could act on to the
next step. The same class of failure as PR #28 — a guard that does not guard.

What changed:

- select-entry now scans for the first ACTIONABLE entry rather than the first
  bold one, validating each candidate and skipping the ones that fail. It exits
  nonzero with NO_ACTIONABLE_BACKLOG_ENTRY only when nothing in the backlog
  qualifies. Selection stays in select-entry: existing tests correctly pin that
  emit-package describes the entry select-entry chose, and my first attempt at
  this moved the scan into emit-package and broke that contract.
- emit-package validates before emitting, as a second line of defence.
- packageFromEntry moved into the SDK. Both steps need to build a package —
  select-entry to judge actionability, emit-package to emit — and inlining the
  regex in both is exactly the drift the canonical-spec test warns about.
- build-sdk step added: dist/ is gitignored, so the flow must build the SDK
  before it can call it.
- Steps resolve the SDK by walking up from cwd, with a RELAYFLOWS_SDK_DIST
  override. The flow tests run the real commands in a temp cwd, so a path
  relative to the repo root does not survive.
- Canonical spec regenerated. The kernel consumes that file, not the yaml.

Why hard-failing outright was wrong: the real ops/BACKLOG.md's first entry has
no backticked file path, so a plain refusal broke the actual drive loop on every
run. Skipping unactionable entries keeps rule 2 intact — the real workload runs
on it.

Proven, not asserted:
  malformed-only backlog -> NO_ACTIONABLE_BACKLOG_ENTRY scanned=1, exit 1
  real ops/BACKLOG.md     -> exit 0, emits a package, SKIPPED_UNACTIONABLE=10

Verified: sdk 179 passed (13 files), kernel 11 suites ok / 0 failed, tsc clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
khaliqgant added a commit that referenced this pull request Aug 29, 2026
* gate 3: handle a malformed backlog without crashing or half-forming a package

Salvaged from run 1095ede6. That run's delivered PR (#26) carried this work
alongside reverts of ops/deliver-run.sh (-32), ops/BACKLOG.md (-30),
ops/STATE.md (-17) and ops/IMMUTABLE_PATHS (-9) — every guard fix and finding
recorded after it launched, undone by a stale base. The code was good and the
rest was not, so only the three SDK files are taken, onto current main.

The picker reads whatever ops/BACKLOG.md contains. A bold title with no body,
an unterminated backtick, a bullet nested under another: each now produces a
typed result rather than a crash or a half-formed package that reads as
actionable.

Verified: sdk 174 passed across 13 files, tsc --noEmit clean. The one
live-kernel failure on first run was a worktree lacking a built relayflowd, not
a regression — it passes after cargo build.

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

* fix: wire validation into the backlog flow, so the guard actually guards (PR #30 P1)

Review was right: validateWorkPackage existed but no flow step called it, so a
malformed entry still exited 0 and handed a package nobody could act on to the
next step. The same class of failure as PR #28 — a guard that does not guard.

What changed:

- select-entry now scans for the first ACTIONABLE entry rather than the first
  bold one, validating each candidate and skipping the ones that fail. It exits
  nonzero with NO_ACTIONABLE_BACKLOG_ENTRY only when nothing in the backlog
  qualifies. Selection stays in select-entry: existing tests correctly pin that
  emit-package describes the entry select-entry chose, and my first attempt at
  this moved the scan into emit-package and broke that contract.
- emit-package validates before emitting, as a second line of defence.
- packageFromEntry moved into the SDK. Both steps need to build a package —
  select-entry to judge actionability, emit-package to emit — and inlining the
  regex in both is exactly the drift the canonical-spec test warns about.
- build-sdk step added: dist/ is gitignored, so the flow must build the SDK
  before it can call it.
- Steps resolve the SDK by walking up from cwd, with a RELAYFLOWS_SDK_DIST
  override. The flow tests run the real commands in a temp cwd, so a path
  relative to the repo root does not survive.
- Canonical spec regenerated. The kernel consumes that file, not the yaml.

Why hard-failing outright was wrong: the real ops/BACKLOG.md's first entry has
no backticked file path, so a plain refusal broke the actual drive loop on every
run. Skipping unactionable entries keeps rule 2 intact — the real workload runs
on it.

Proven, not asserted:
  malformed-only backlog -> NO_ACTIONABLE_BACKLOG_ENTRY scanned=1, exit 1
  real ops/BACKLOG.md     -> exit 0, emits a package, SKIPPED_UNACTIONABLE=10

Verified: sdk 179 passed (13 files), kernel 11 suites ok / 0 failed, tsc clean.

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

---------

Co-authored-by: Relayflow Lead <lead@relayflows.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This was referenced Aug 29, 2026
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.

2 participants