Skip to content

D: inspect task changes and make runner commits (needed by F2) #66

Closed

Description

@mchwang

Lane D follow-up needed by F2 (per-item execution). Related: #51 (other D prerequisites), #22.

Why

The design says codeboost makes every commit. After an execute or fix invocation, the runner must inspect what the agent changed, audit it against the plan item's declared files, undo any commits the agent made, and commit the changes itself with Plan-Item/Plan-Revision trailers (docs/plan-format.md, "After each run"; design, "How codeboost runs a plan").

The work lives in D's task volumes. /work/.git is read-only to the agent, and F must never write Git metadata from the host. D exposes no operation to read changes back or to commit, so F2's commit step has no real target.

Proposed operations (names are suggestions)

All take a TaskFilesystems value or a recovered storage handle (#51), run in a runner-controlled container (not the agent's), take an AbortSignal with an overall deadline, settle only after their own container stops, and bound their output.

  1. snapshotDeclaredLinks(storage, paths), before launch. A no-follow snapshot of every declared symlink target (identity, type, mode, content hash, descendants for directory targets, and absent-path identity for dangling links). It is returned as data and kept by F.

  2. inspectTaskChanges(storage, { baseHead, linkSnapshot }), after settlement. Returns a change manifest, read without following links:

    • changes[]: { path, oldPath?, kind: 'add'|'modify'|'delete'|'rename'|'mode', oldType?, newType?, oldMode?, newMode?, newLinkTarget?, underGit: boolean }, where types are file|symlink|gitlink|directory|other;
    • agentCommits[]: commits the agent made on top of baseHead;
    • metadataChanged: whether anything under .git changed (config, hooks, refs, objects);
    • linkTargetChanges[]: every difference from linkSnapshot;
    • nestedGitlinkContent[]: any content under gitlink paths.

    A bound on the number of changes must fail closed, never truncate.

  3. commitTaskChanges(storage, { baseHead, message, trailers, author }). Undoes agent commits while keeping their changes, stages exactly the audited paths, and commits with hooks disabled (core.hooksPath=/dev/null) and isolated Git config. Returns the new head SHA. It must refuse when the work tree differs from the manifest the audit approved (pass the manifest digest).

  4. materializeTaskFilesystems(clone, head, limits). Covered by the "clean invocation state" rule: a fresh filesystem from the recorded trusted head before every invocation, never a reset of a used one. This may simply be prepareTaskFilesystems from a clone at head; confirm and document it.

F2a will implement the audit as a pure function over exactly this manifest shape, so the shapes should be agreed here first.

Done when

  • The operations are exported with contract tests. The real-Docker suite covers:
    • a new symlink, and writes through a link to an undeclared file, a directory child and a dangling target;
    • a .git change;
    • an agent commit, which is undone while its changes are kept;
    • a gitlink with nested content;
    • an oversized change set failing closed;
    • commitTaskChanges refusing a work tree that changed after the audit.
  • Each operation is abortable, has a deadline, and settles only after its container stops.

Activity

  1. mchwang commented on Sep 29, 2026

    @mchwang
    ContributorAuthor

    Decisions before implementation (2026-09-28)

    Research on main found two gaps in this issue as written:

    • The agent cannot write /work/.git in any phase. The metadata volume is mounted read-only (agents/container/profile.ts), and the validator refuses a container without that mount (agents/container/run.ts). So agent commits and .git changes cannot happen today.
    • Nothing gets the runner's commit out of the task volume. The commit is made in the metadata volume, which is removed after the terminal write. The next clone and the push both read the host repo.

    The decisions:

    # Question Decision
    1 How does the runner's commit reach the host repo? Add exportTaskCommit(storage, { base, head }) to this issue. A read-only container writes a bounded git bundle to stdout. F fetches it into a runner-owned host repo and checks that the SHA matches the audited commit. "F must never write Git metadata from the host" becomes "only through a verified bundle".
    2 Agent commits and .git changes Keep agentCommits and metadataChanged in the manifest as fail-closed checks, compared against a metadata digest taken at seeding. A non-empty result means needs human, and commitTaskChanges refuses. No undo path is built for a case the mounts prevent.
    3 Who the runner commit is by commitTaskChanges requires author and committer (name, email, date) from F. D takes nothing from the environment, so the SHA is reproducible from recorded inputs.
    4 Gitlink mounts and the pre-launch tree check (plan-format.md lines 131 and 135) Split out to #81.

    Settled with those, unless there are objections:

    • commitTaskChanges recomputes the manifest in its own container and compares its digest before staging, so the check and the commit cannot drift apart.
    • An empty change set returns baseHead with no commit ("planned but unchanged").
    • Each task storage serves one invocation (the "clean invocation state" rule), so base is the clone's head.
  2. added 20 commits that reference this issue on Sep 29, 2026
  3. mchwang commented on Oct 1, 2026

    @mchwang
    ContributorAuthor

    Decision 1 revised (2026-10-01): Part 2 is in #90.

    There is no separate exportTaskCommit. commitTaskChanges does both steps:

    • It runs one container, with both task volumes read-only.
    • It inspects again, then builds the commit from those same reads into a scratch object store in /tmp.
    • It returns the commit as a bounded git bundle: one ref, refs/heads/codeboost, with base as its prerequisite.

    Node keeps the bundle only when:

    • the manifest digest equals the approved one;
    • the commit ID equals the ID that tree, base, identities and message describe.

    The metadata volume is never writable after seeding. Calling it again with the same inputs gives the same SHA.

    Why: the manifest digest is computed in TypeScript (linkTargetChanges comes from the snapshot). So a container could not check it before writing to the metadata volume. Here, nothing is kept until it is checked.

    Two contract points, from review:

    • A retargeted link-target entry (the item editing a declared link) is not refused. The audit judges it.
    • "F must never write Git metadata from the host" now reads "only by fetching a verified bundle into a runner-owned repository".

    🤖 Generated with Claude Code

  4. mchwang commented on Oct 20, 2026

    @mchwang
    ContributorAuthor

    Done: part 1 in #85 (task-change inspection) and part 2 in #90 (runner commit as a bounded git bundle), both merged. Recorded in docs/architecture.md, lane D row.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions