Skip to content

drive: cloud run fb9528bb - #20

Merged
khaliqgant merged 2 commits into
mainfrom
cloud/run-fb9528bb
Aug 29, 2026
Merged

khaliqgant merged 2 commits into
mainfrom
cloud/run-fb9528bb

Conversation

@khaliqgant

Copy link
Copy Markdown
Member

Automated drive work from cloud run fb9528bb-ae04-4c26-8cc1-04eb212c19fb.

The sandbox cannot open PRs (no remote, no GitHub token), so this was delivered
from a host that can. Verification and adversarial review ran in-run — see
ops/reviews/ in the diff. A human merges.

Work produced by cloud run fb9528bb-ae04-4c26-8cc1-04eb212c19fb in a workflow sandbox and delivered from
this host, because a sandbox has no remote and no GitHub token.

Verification and adversarial review ran in-run; see ops/reviews/ in the diff.
@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-29T09:55:29.109616Z f90100a 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

Review Change Stack

Warning

Review limit reached

Next included review available in 19 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: f441c1ec-4938-419f-8ac1-047c0fbb2566

📥 Commits

Reviewing files that changed from the base of the PR and between f90100a and e3e3db9.

📒 Files selected for processing (2)
  • sdk/src/backlog-picker.ts
  • sdk/tests/backlog-picker.test.ts
ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 14235185-b916-4559-86b4-2817008140c2

📥 Commits

Reviewing files that changed from the base of the PR and between 6c47266 and f90100a.

📒 Files selected for processing (3)
  • ops/NEXT.md
  • testdata/backlog-picker.flow.yaml
  • testdata/backlog-picker.spec.canonical.json

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


📝 Walkthrough

Walkthrough

The change replaces the Gate 2 Hacker News poller plan with a Gate 3 backlog-picker workflow. The workflow reads ops/BACKLOG.md, selects the first matching entry, and emits structured JSON with normalized metadata and fixed scope.

Changes

Gate 3 backlog picker

Layer / File(s) Summary
Gate 3 work package contract
ops/NEXT.md
Defines the deterministic read-backlog, select-entry, and emit-package flow, output requirements, validation steps, and Gate 3 scope exclusions.
Backlog-picker workflow definition
testdata/backlog-picker.flow.yaml, testdata/backlog-picker.spec.canonical.json
Adds the workflow and canonical specification for selecting the first bold top-level backlog entry and emitting its normalized title, description, file scope, and gate value.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to f9010

This PR adds a deterministic backlog-picker workflow specification and updates planning documentation. No actionable merge-blocking risk remains, so it is merge-ready after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant Backlog as ops/BACKLOG.md
  participant Selector as select-entry
  participant Emitter as emit-package
  Backlog->>Selector: Read backlog content
  Selector->>Emitter: First bold top-level entry
  Emitter-->>Selector: Structured JSON work package
Loading

Poem

A rabbit reads the backlog bright

Picks one bold task in morning light
Three small steps guide the package through
With normalized words and scope in view
Gate Three hops into the queue


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: f90100af4b

ℹ️ 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 ops/NEXT.md
**New files to create:**
- `testdata/backlog-picker.flow.yaml` — the flow spec that reads ops/BACKLOG.md and selects one entry
- `testdata/backlog-picker.spec.canonical.json` — canonical compiled spec (via `flows check`)
- `sdk/tests/backlog-picker.test.ts` — test proving deterministic selection given the same input

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 Add the required deterministic-selection test

The diff declares sdk/tests/backlog-picker.test.ts as required, but does not add that file or register this fixture in an existing test. Consequently npm test can pass without executing the new selection regex or asserting its structured output, leaving the deterministic behavior introduced here entirely unpinned despite this definition of done.

AGENTS.md reference: AGENTS.md:L19-L21

Useful? React with 👍 / 👎.

type: deterministic
dependsOn: [select-entry]
command: >-
node -e 'const fs=require("node:fs");const text=fs.readFileSync("ops/BACKLOG.md","utf8");const match=text.match(/^- \*\*(.+?)\*\*\s*(.*(?:\n .*)*)/m);if(!match)process.exit(1);process.stdout.write(JSON.stringify({title:match[1],description:match[2].replace(/\s+/g," ").trim(),files_in_scope:["sdk/src/preflight.ts","sdk/tests/preflight.test.ts"],gate:3}))'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reuse the selected backlog snapshot when emitting

When ops/BACKLOG.md changes after select-entry completes—including across a crash and resume—this step rereads the mutable file instead of consuming the completed selection, so the journal can record item A as selected while the final package describes item B. Emit from the exact selected snapshot, or combine selection and emission into one deterministic step, so resume cannot change the work package.

AGENTS.md reference: AGENTS.md:L3-L5

Useful? React with 👍 / 👎.

type: deterministic
dependsOn: [select-entry]
command: >-
node -e 'const fs=require("node:fs");const text=fs.readFileSync("ops/BACKLOG.md","utf8");const match=text.match(/^- \*\*(.+?)\*\*\s*(.*(?:\n .*)*)/m);if(!match)process.exit(1);process.stdout.write(JSON.stringify({title:match[1],description:match[2].replace(/\s+/g," ").trim(),files_in_scope:["sdk/src/preflight.ts","sdk/tests/preflight.test.ts"],gate:3}))'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Derive package metadata from the selected backlog item

The emitted files_in_scope and gate are constants rather than properties of the selected entry. The current first item is the deterministic-command preflight gap, which RFC-0001 assigns to Gate 1, yet this emits gate: 3; once that item is completed or reordered, every other existing backlog item will also inherit unrelated preflight file paths. This produces a structurally valid but incorrectly routed work package.

AGENTS.md reference: AGENTS.md:L3-L5

Useful? React with 👍 / 👎.

khaliqgant pushed a commit that referenced this pull request Aug 29, 2026
…ation

Refreshing ground truth BEFORE it goes stale again. A stale STATE.md has cost
two runs already: one escalated on a contradiction it could not resolve,
another was told two completed items were still missing. An assessor in a
sandbox has no git history — this file is its history.

Three PRs are open and named so no run duplicates their work. Gate 2's entry
now records that a real external event HAS woken the flow, with exactly-once
holding across repeated live polls, and that what remains is Khaliq's
judgement on rule 2 rather than a missing part.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The work package's definition of done called for a test proving selection is
deterministic given the same input. The run reported the DoD met and shipped no
such test — the selection rule existed only as a regex inside a shell one-liner
in the flow yaml, where it could not be asserted at all.

The rule now lives in sdk/src/backlog-picker.ts and the flow can call it.
Five tests cover: the rule itself, determinism across 25 repeated selections,
stable rendering, returning null rather than guessing when nothing is
actionable, and ignoring bold prose that is not a bullet title.

Determinism is the property that matters here, not a nicety. A Garden that
proposes its own work must be predictable before it is clever: if two runs over
identical input can disagree, nothing downstream can reason about what the
system decided or why.

Verified: sdk 158 passed across 11 files, tsc --noEmit clean.

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

Copy link
Copy Markdown
Member Author

P1 addressed — the deterministic-selection test now exists.

You were right that it was missing. The DoD called for it, the run reported the DoD met, and no such test shipped. The reason it was missing is worth naming: the selection rule lived only as a regex inside a shell one-liner in testdata/backlog-picker.flow.yaml, where nothing could assert it. A rule that cannot be tested is a rule nobody can rely on.

The rule now lives in sdk/src/backlog-picker.ts. Five tests cover it: the rule itself, determinism across 25 repeated selections, stable rendering of the work package, returning null rather than guessing when nothing is actionable, and ignoring bold prose that is not a bullet title.

Determinism is the load-bearing property, not a nicety — a Garden that proposes its own work must be predictable before it is clever. If two runs over identical input can disagree, nothing downstream can reason about what the system decided or why.

sdk: Test Files 11 passed (11) / Tests 158 passed (158)
tsc: npx tsc --noEmit — clean

The two P2s — reusing the selected snapshot rather than re-reading the file, and deriving package metadata from the entry — are not addressed. Both are real and both are refinements of a rule that now has a test around it, so they are safe to take next rather than urgent.

@khaliqgant
khaliqgant merged commit 5ed2c2f into main Aug 29, 2026
2 checks passed
@khaliqgant
khaliqgant deleted the cloud/run-fb9528bb branch August 29, 2026 10:52
khaliqgant pushed a commit that referenced this pull request Aug 29, 2026
Recording immediately after the merge rather than letting ground truth drift;
a stale STATE.md has cost two runs already and an assessor in a sandbox has no
git history to fall back on.

Gate 3 moves RED -> AMBER: the backlog picker is on main with its selection
rule extracted from a shell one-liner into testable code, determinism asserted
across 25 repeated selections. That is the seed of flows proposing their own
work, not the Garden.

Two P2 refinements remain open in review and are recorded as deliberately
non-blocking under the advisory/blocking split.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
khaliqgant pushed a commit that referenced this pull request Aug 29, 2026
…red test

Twice in a row: PR #20's determinism test and its follow-up's cross-step
agreement test were both specified in the DoD, both reported done, and neither
shipped. The code was right both times; the claim was not. BUILD_DONE and a
green suite prove nothing here, because a missing test cannot fail.

Recorded with why the easy fix is wrong: a build-gate grepping for a new test
file is trivially satisfied by an empty one, and this program has already
shipped four guards that could not fail. The real fix is to make the DoD itself
executable — assess emits it as commands, verify runs them — so a missing test
fails because the command naming it does not exist. That is a change to the
assess/verify contract and wants Khaliq's view first.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
khaliqgant added a commit that referenced this pull request Aug 29, 2026
* gate 3: snapshot the backlog once, and prove the steps agree (PR #20 P2s)

Two review refinements from PR #20, plus the test its DoD asked for.

The flow re-read ops/BACKLOG.md in every step, so a backlog edit between
select-entry and emit-package produced a package describing an entry that was
never selected — a Garden reporting work it did not choose. read-backlog now
snapshots the file once and the later steps read the snapshot and the selected
entry, so the steps cannot disagree. Package metadata is derived from the
selected entry rather than hardcoded.

The test runs the flow's ACTUAL shell commands, not a reimplementation — a test
of a paraphrase would pass while the flow stayed broken — and mutates the
backlog mid-run to force the condition.

Confirmed it FAILS against the unfixed flow before trusting it:
  × expected '{"title":"Swapped entry"...}' to contain 'Original entry'
and passes against the fixed one. A regression test never seen to fail proves
nothing; PR #18 is still carrying exactly that gap.

Verified: sdk 159 passed across 12 files, tsc --noEmit clean.

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

* fix: clear the selected entry before selecting, so no run inherits the last one (PR #21 P1)

My own fix created this. Snapshotting the backlog stopped the two steps
disagreeing within a run, but it did so with shared persistent state — and
shared state leaks across runs. If select-entry finds nothing actionable it
exits before writing, so emit-package read the PREVIOUS run's entry and
presented it as this run's choice. A Garden confidently proposing yesterday's
work as today's.

select-entry now removes .relayflow/backlog-picker-entry.json before it
attempts selection, so a failed selection leaves nothing behind to inherit.

Confirmed the test FAILS without the fix before trusting it:
  × expected '{"title":"Yesterday entry"...}' not to contain 'Yesterday entry'

Verified: sdk 160 passed across 12 files.

The P2 (the files_in_scope regex matching backticked prose that contains a
slash) is not addressed — real, cosmetic, and safe to take next.

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>
khaliqgant pushed a commit that referenced this pull request Aug 29, 2026
Recording straight after the merge, as with #20. Both of the picker's
properties now have tests confirmed to fail without their fixes: the two steps
cannot disagree within a run, and no run inherits the previous run's selection.

Noting in the same breath that #18 still does not meet that standard — its
regression test has never been observed to fail — so the difference is visible
to whoever reads this next rather than buried in a PR comment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
kjgbot added a commit that referenced this pull request Sep 10, 2026
* feat(drive-local): pick any backlog item, not the one it was written for

The local drive flow could only ever execute BACKLOG F8b. Its selector
hardcoded one file, one old identifier and one new one, and asserted that
BACKLOG still contained that exact entry. It proved a relayflow can drive a
real change on this checkout with no Cloud admission, no Daytona and no
Relaycast workspace — but every later tick needed a human to rewrite the script
first. A loop that needs editing between iterations is not a loop.

Two changes make it general.

Selection now comes from the SDK's backlog picker (gate 3, PR #20) — the same
rule the cloud drive uses: first top-level bullet with a bold title, validated
for a title, files in scope and a definition of done. Using it rather than a
second implementation means the local and cloud loops cannot drift about what
"next" means. The script refuses an underspecified package instead of handing
an agent something it cannot tell it has finished.

Implementation is now an agent step. A deterministic step can only make
mechanical changes, and most backlog entries are not mechanical; that limit,
not the selector, is what really pinned the old flow to a rename. The agent is
told to stay inside the declared scope, to change nothing if the package is
already done or its premise is false, and that reporting "already done" is a
good tick while inventing an edit to look busy is not.

Verified end to end: the flow compiles under the 0.1.0 SDK (5 steps, one of
type agent), and `select` run against the real ops/BACKLOG.md picks
"`timeoutMs` is enforced LATE, not never", writes the package with its files,
definition of done and a pinned HEAD.

Two things running it taught me, both now encoded: an agent step cannot declare
`timeoutMs` (0.1.0 bounds deterministic steps only), and `selectBacklogEntry`
is not re-exported from the SDK index — only `dist/backlog-picker.js` has all
four functions.

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

* fix(drive-local): skip work this loop cannot bound, do not stall on it

The picker emits ['.'] for files_in_scope when an entry references code but
names no path. That is deliberate on its side — its own comment calls it
"honest breadth" — and it is a fair description of the entry. It is not usable
as scope for an agent: "." is the whole repository, and an agent told its scope
is everything has been told nothing.

First attempt refused the tick outright when the selected entry was unbounded.
That failed closed, which was right, but the current BACKLOG's first selectable
entry is unbounded — so the loop would have refused on every run forever. A loop
that never runs is not safer than one that runs on bounded work.

Selection now walks past entries it cannot bound and reports each skip with its
reason. "Next" is still the picker's definition: rather than write a second
parser that could disagree with it about what an entry is, the rejected entry's
title is cut from the markdown and the picker is asked again.

Verified against the real ops/BACKLOG.md: skips the unbounded `timeoutMs` entry
and selects "GATES 2 AND 3 ARE BLOCKED ON A MISSING COMPONENT: there is no agent
worker" with four concrete files in scope.

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

* fix(drive-local): a path that no longer exists is not scope

Backlog entries outlive the tree they were written against. This repo moved
`sdk/` to `packages/sdk/`, so an entry naming `sdk/src/protocol.ts` still reads
as precise while pointing at nothing. An agent handed four missing files will
either invent work or widen scope until it finds something, and the flow's own
instruction forbids both.

`select` now checks that every declared path exists and skips entries whose
scope has rotted, naming the missing files in the skip line. A rotted entry can
no longer silently become an agent's instruction.

This is deliberately the guard rather than a backlog cleanup. Repairing the
entries by hand is a one-time fix that rots again at the next reorg — the
sdk/ move already proves that. With the guard in place the skip output IS the
worklist, with the exact missing paths named, so the cleanup becomes generated
rather than audited.

What it reports against the current BACKLOG: 12 entries skipped — 5 unbounded,
5 with no scope at all, 1 with no definition of done, and 1 stale
(sdk/tests/live-kernel.test.ts, sdk/src/protocol.ts, sdk/src/journal-client.ts,
sdk/src/cli/run.ts). Two of the skipped entries are titled "DONE (PR #45,
merged)" and "DONE (PR #42, merged)" and are still sitting in the backlog.

It then selects real bounded work: "Regression suite (`regressions/`, dormant)"
scoped to regressions/MANIFEST.json, which exists.

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

* fix(drive-local): the flow could never run — pin a stream, drop a dead step

Three review findings, all confirmed by running the launcher rather than
reading it. I had only ever compiled this flow, and compiling proves the spec
is legal, not that the runner will accept it.

LOCAL_AGENT_PINS_REQUIRED. The launcher refuses any agent step that declares no
stream — "the kernel refuses workers with no pins" — and the refusal happens
before a run is created. So every invocation of this flow failed immediately,
and I had described it as safe to run. The agent step now pins a stream.

The build-sdk step was dead code. The launcher asserts packages/sdk/dist/cli.js
exists during preflight, before it submits anything, so a build step inside the
flow can never run on the cold checkout it was meant to serve. Removed, with the
prerequisite documented where an operator will see it.

The output gate demanded a marker the instruction never requested: verification
gates on `DONE` and nothing told the agent to emit it, so a correct
implementation would have been recorded as a failure. The instruction now states
the contract.

Verified by re-running: LOCAL_AGENT_PINS_REQUIRED is gone. Execution then stops
on environment rather than on the flow — a built relayflowd, and a working
directory short enough for a unix socket path (LOCAL_SOCKET_PATH_TOO_LONG from
this scratchpad).

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

* fix(drive-local): enforce package scope and executable acceptance checks

Choose explicit Verify JSON argv declarations in backlog entries instead of translating prose or inferring correctness from the SDK suite. Selection skips packages without executable checks; verification executes every selected check before the regression suite. Add an acceptance assertion to the existing F8b entry.

Capture a scope step in the submitted flow that refuses changed verifier code before loading helpers, then checks staged, unstaged, and untracked paths against the selected scope. Reconstruct package metadata from the unchanged backlog and reject tampering, symlinks, and file-to-directory scope widening. Advance skipped entries past their matched bullet line rather than searching for a title mention.

Evidence: ops/runtime-evidence/drive-hardening-0908.txt contains literal commands and output for 26 local/launcher tests, 803 SDK passes (3 skipped), the pre-existing obsolete test failure, and the F3 cursor mutation failure/pass.

* docs(drive-local): capture real backlog selection and acceptance output

* fix(drive-local): restore the work-branch guard dropped in the generalization

cubic's P1 on ops/local-work-package.mjs:149 is correct and is a regression.

main carries the guard:

  const branch = git('branch', '--show-current');
  assert(branch && branch !== 'main', 'LOCAL_DRIVE_REFUSED: use a work branch');

When select() was generalized it kept RECORDING the branch in the package and
stopped asserting it, so the loop would select work while sitting on main and let
the agent edit the protected branch. `--show-current` prints nothing on a
detached HEAD, which is equally not a work branch and was equally unguarded.

The guard now runs before anything is read, and names which case it refused so
the operator does not have to guess.

Verified: detached HEAD refuses live with
"LOCAL_DRIVE_REFUSED: detached HEAD is not a work branch"; the branch condition
refuses 'main' and accepts a work branch.

Note for the next person testing this: `git checkout main` swaps this script for
main's own copy, so a naive "check out main and run it" tests the wrong code and
looks like the guard did not fire.

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

* fix(drive-local): rebuild the SDK before running its suite

cubic's P1 on workflows/drive-local.yaml:94 is correct. The verify step ran
`vitest` straight after `local-work-package.mjs verify` with no build in
between. Tests and probes import `packages/sdk/dist/*` (see
ops/probes/pr134-repair-0903/harness.mjs), so when the agent changes SDK
TypeScript the suite exercises the PRE-agent artifact and can pass code that was
just changed. That is the stale-tree gate failure: a green check against an
artifact the change never reached.

Now runs the package's own build (`tsc` then `make-cli-executable.mjs`) between
verify and the suite.

Invoked directly rather than through `npm run build`, matching the existing
`node node_modules/vitest/vitest.mjs run` idiom in this same step, because npm
hangs on this host: ~/.npmrc is a Dropbox symlink and any bare npm blocks with
no output.

NOT VERIFIED ON DISK: no local checkout currently has packages/sdk/node_modules,
so I could not confirm `node_modules/typescript/bin/tsc` resolves here. It is
TypeScript's shipped entry point and mirrors the vitest invocation two lines
below it, but the first real loop run is what proves it. If it is wrong the step
fails loudly rather than silently testing stale code, which is still an
improvement on the current behaviour.

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

* fix(drive-local): run the acceptance gate from an immutable pre-agent snapshot

Khaliq approved option A. Addresses #244 H1: "the acceptance gate again trusts
code writable by the agent it judges."

The hole was specific. The scope guard protects
packages/sdk/src/backlog-picker.ts, but local-work-package.mjs imports the BUILT
packages/sdk/dist/backlog-picker.js during scope and acceptance. An agent could
leave the guarded source untouched, rebuild dist, and the guard would still pass
while the gate executed the agent's own code. The rebuild step added earlier in
this PR makes dist routinely regenerated, so this was not theoretical.

New `gate-snapshot` step runs BEFORE the agent: builds the SDK, copies the
picker and both acceptance scripts into .drive-gate/, and records SHA256SUMS.
`scope` and `verify` now verify those sums and execute from the snapshot, with
DRIVE_GATE_PICKER pinning the snapshotted picker so the acceptance path cannot
reach the live dist.

The diff guards additionally cover packages/sdk/dist/backlog-picker.js, and
.drive-gate/ is gitignored so the snapshot never enters a commit.

Ordering is the load-bearing part and it is asserted, not assumed: `implement`
now dependsOn [gate-snapshot], not [select]. My first edit missed that and left
the agent able to run CONCURRENTLY with the snapshot, which would have voided
the whole change while looking correct.

Not yet exercised end to end -- the loop needs a real run to prove the snapshot
path works. If a copied file or the shasum check is wrong the gate fails loudly
rather than silently trusting agent-written code.

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

* fix(drive-local): validate descendants of directory scopes for escaping symlinks

Khaliq approved option B. Addresses cubic's P1 on
ops/local-work-verification.mjs: "A pre-existing symlink inside a declared scope
is never inspected when it is unchanged, so a Verify command can write outside
the checkout without creating a touched path."

The existing symlink walk runs per TOUCHED path, so it only sees paths the agent
already modified. A symlink that was there before the run is never touched, never
inspected, and a declared directory scope is an authorization to write anywhere
beneath it -- including straight through that link.

checkScope now walks the descendants of every directory scope before any check
runs, and refuses a symlink whose realpath leaves the checkout root.

Symlinks that stay inside the root are deliberately allowed: workspace layouts
use them legitimately, and the threat here is escape, not indirection. Dangling
links are skipped -- a write cannot escape through a link with no target.

Verified both directions in a scratch repo with `src` as the declared scope:

  pre-existing src/escape -> ../../outside   SYMLINK_ESCAPES_SCOPE (refused)
  benign src/inside -> a.txt only            SCOPE_OK: 0 changed path(s)

so it catches the escape without rejecting internal links.

.git is skipped during the walk.

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

* fix(drive-local): give selection and the gate one baseline

cubic's P2 on ops/local-work-verification.mjs:35: "When a backlog scope names an
existing untracked path, `select` accepts it, but this `git cat-file` lookup
aborts `scope` because the path is absent from `pkg.head`."

Two different existence rules were in play:

  select()           pathExists = existsSync                    working tree
  verifiedPackage()  git cat-file -e ${pkg.head}:path           committed at HEAD
  checkScope()       git cat-file -t ${pkg.head}:path           committed at HEAD

So selection blessed a path that exists only on disk, persisted it into the work
package, and the gate then refused the very package selection had produced. The
loop aborted on its own decision.

select() now uses the same committed-at-HEAD rule. An untracked path is refused
during selection, with the existing stale_scope reason, instead of passing and
detonating two steps later.

Committed-at-HEAD is the right rule for both rather than relaxing the gate:
scope is a claim about reviewable content, and an untracked path is not yet
that.

Demonstrated in a scratch repo:

  ops/tracked.txt   existsSync=true  inHEAD=true
  untracked-dir     existsSync=true  inHEAD=false   <- accepted by select, refused by scope

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

* fix(drive-local): address report and scope review findings

* Capture local drive gates and baseline in submitted commands

* Clarify historical drive snapshot and launcher documentation

* Load local drive gate inputs from a pinned Git commit

---------

Co-authored-by: kjgbot <kjgbot@agentrelay.dev>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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