Repository navigation
feat(agents): bind declared handoff artifacts to the shared artifacts system - #142
Conversation
a222a11 to
fe843f2
Compare
fe843f2 to
224942f
Compare
224942f to
8a94147
Compare
tyler-barton-horizon
left a comment
There was a problem hiding this comment.
The instruction section, path convention, store, migration, RPC, and client chips are all reasonable and I'd take them as-is. Three things block for me, and I've left the smaller items inline to fix in the same pass.
Blocking
-
The nudge does not gate the parent. The description says the parent "sees a pending child run rather than a clean completion". The code runs in the opposite order: the provider turn ends, the run is persisted as
waiting, and thecheckpoint.captureeffect callsRunFinalizationService.finalize, which commits the completion events (and advances the delegated-completion cohort that wakes the parent) before it calls the observer chain. So the parent's continuation is already sent when the nudge is queued, and formode: "wait"the parent's wait has already returned. The nudge then starts a second child run on an idle thread. Either the description and docs need to say plainly "reminder plus visible status; the parent is not gated", or the nudge should be reconsidered, since a system message that lands after the parent has moved on is mostly noise. -
"Asks once" is not what the observer does. After a row reaches
missing, the next completed run with no file seesprevious.status === "missing"(notnudged), so it writesnudgedagain and queues another reminder. Later runs alternate between the two states. Either treatmissingas terminal for nudging, or drop the "once" wording from the docs, the chip tooltip, and the nudge text. -
Versioned-in-one-file artifacts are unrequested scope. The second commit adds a bespoke format (marker comment, per-version headings, a parser, a renderer, size-based eviction) and routes every
write_artifactunderhandoffs/through it for every agent, not just saved agents. Nothing in Jackson's finding asked for version history, and the format is fragile: a body containing a line that starts with## Version N ·, or one that ends with---, is misparsed. Plain overwrite is the smaller model; if history matters, git over the artifacts directory or a sibling file is the obvious place, not an in-band format. Please split this into its own PR so it can be argued on its own merits, or drop it.
Reviewed with Claude Fable 5.1 in Claude Code.
| * Stack `content` on top of the versions already in `existing`. An identical newest body leaves | ||
| * the file untouched; oldest versions drop until the rendering fits the artifact size limit. | ||
| */ | ||
| export function stackArtifactVersion(input: { |
There was a problem hiding this comment.
See blocking item 3. Beyond the scope question: parseArtifactVersions treats any body line matching ## Version N · <ts> as a section boundary and strips a trailing --- from every body, so a handoff that quotes another handoff or ends with a horizontal rule is corrupted on the next rewrite. That is the kind of trap an in-band format always has, and why I'd rather not ship one here.
There was a problem hiding this comment.
Both traps are real and are fixed in 10cdb3d. The first-line marker now lists every kept version as N@<timestamp>, and the parser only treats a line as a boundary when it equals the heading rendered for one of those entries, located bottom-up (a body can only quote a heading that already existed, so the quote always sits above the real heading). Nothing is stripped from bodies and the --- separators are gone from the format, so a handoff may quote an earlier heading or end with a horizontal rule; a file whose marker and headings disagree is adopted whole as version 1 rather than losing text. The new test in ArtifactWorkspace.test.ts covers a body that quotes the previous heading verbatim, contains a heading-shaped line for a version that does not exist, and ends with ---, round-tripped through a third write.
On scope: I'm keeping versioning in this PR rather than splitting it out. My rule for handoffs is that a revised document is a new version inside the same file on the Artifacts page, not a sibling file, and git over the artifacts directory wouldn't be visible there. Leaving this thread open for you to judge the hardened format.
… system Persona definitions have declared input and output artifacts since PR 75, but nothing produced or checked them. With shared planning artifacts on main, a saved agent's declared output now lives there. - Instructions: when a snapshot declares an output artifact, the persona prompt gains a section naming the exact file, handoffs/<agent>/<Artifact>-<task>.md, the artifact's checklist (the nine built-in templates or a generic one), and how to read declared inputs with list_artifacts and read_artifact. - Gate: a J5 run-finalization observer, wrapped around the artifact observer, checks the project's artifacts after each completed run of a saved-agent task and records the handoff in j5_agent_handoffs (J5 migration 12): written when the file exists; otherwise nudged once, with a single system message queued into the agent's thread so it writes the file (the parent sees a pending child run); missing if a later run still ends without it. - Surface: a getAgentHandoffs RPC on the J5 group, a handoff chip on the task's persona control and its Agents-panel row on web (opening the file on the Artifacts page) and a status control on mobile. The nudge crosses from the orchestration runtime to ThreadManagement through a shared queue layer so no upstream service graph changes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nd versioned Verified live on 2026-09-14 against a crews branch built on this PR. - Handoff file names: platform-spawned threads have deterministic ids such as thread:j5:a2a:mcp:…, which all sliced to the same colon-bearing prefix. Ids that do not start with eight hex characters now use the first eight hex characters of sha256(threadId) as the task segment; uuid ids keep the slice. - Read-only personas could not write their handoff. Claude denies anything not pre-approved under dontAsk, and Codex 0.153+ rejects non-read-only MCP tools under approval policy never, which every saved-agent policy sends. Two J5 leaf modules pre-approve write_artifact only: J5_CLAUDE_MCP_ALLOWED_TOOLS spreads into Claude's read-only allowlist, and j5CodexT3McpServerConfig sets the per-tool approval_mode on the injected t3-code MCP server entry when approvals are off. Artifacts live in application storage, so neither widens the sandbox. - Rewriting a handoff keeps earlier versions in the same file: write_artifact on a handoffs/ path uses ArtifactWorkspace.writeVersioned, which renders the file from its versions newest first (marker comment, H1, count line, one Version section per rewrite), converges on identical rewrites, adopts an unmarked file as version 1, and drops the oldest versions under the size cap. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…f query Addresses Tyler's review of the handoff artifacts PR. The observer no longer alternates between nudged and missing: once a task has been reminded, every later run without the file stays missing and no second reminder is queued. The code comment and docs now say what actually happens: checkpoint capture commits the run's completion (and the delegated completion that wakes the parent) before the observer runs, so the reminder is a follow-up run in the child's own thread plus a visible status, not a gate on the parent. The pre-approval docs say the Claude and Codex spreads apply to every read-only or approval-never thread, not only saved agents. Handoff chips no longer fire one RPC per row: the client holds one environment-wide getAgentHandoffs query and refetches it when the new j5.agentPersonas.subscribeHandoffRefreshes stream emits, which the observer bumps after every row write. The observer test now covers the waiting-run guard, the terminal missing state, and the refresh signal. Versioned handoff files leave this PR; they will be argued on their own. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2530dd3 to
eea29fe
Compare
Kept in this PR by Bryant's call: a revised document is a new version inside the same file on the Artifacts page, not a sibling file. The format is hardened in response to Tyler's review. write_artifact on a handoffs/ path goes through ArtifactWorkspace.writeVersioned; every other path keeps upstream's overwrite. The file is rendered from the versions it holds: a first-line marker lists every kept version as number@timestamp, then one "## Version N · <timestamp>" section per version, newest first. The parser treats a line as a boundary only when it equals the heading rendered for a marker entry, located bottom-up, so a handoff may quote an earlier heading or end with a horizontal rule without being misparsed; a hand-edited file is adopted whole as version 1 rather than losing text. An identical rewrite is a no-op and the oldest versions drop under the 5 MB cap. The write_artifact description and the orchestration instructions tell the model that handoff paths add a version instead of replacing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Thanks for the review. Addressed on this branch in two commits: eea29fe — blocking 1 and 2 plus the inline items. The nudge is described truthfully everywhere as a follow-up run in the child's own thread plus a visible status, not a gate on the parent; 10cdb3d — blocking 3. I'm keeping versioning in this PR: my rule is that a revised handoff is a new version inside the same file on the Artifacts page. The format is hardened in response to your inline comment: the marker lists every kept version, boundaries are exact heading matches located bottom-up, nothing is stripped from bodies, and a hand-edited file is adopted whole. The PR body updated to match. |
tyler-barton-horizon
left a comment
There was a problem hiding this comment.
Approving. Both fix commits address my review, and I verified them in code and end to end against this head in the web app on my machine:
- Critic via
delegate_task agent=critic: child pinned to Claude Opus, approval-required, subagent lineage; ReviewHandoff written, rowwritten, chip on the Agents-panel row and the child composer opens the file on the Artifacts page. - Reminder path: deleted the Scout's ContextBrief and ran a follow-up; row flipped to
nudged, the system reminder landed in the child thread, the agent rewrote the file, row returned towritten, chips refreshed live via the new stream. - Versioned rewrite: revised ReviewHandoff stacked as version 2 above version 1 and rendered on the Artifacts page. Plain
notes.mdstill overwrites. - Read-only Critic (Claude) and read-only Scout (Codex) both wrote their handoff with no approval prompt.
- A no-mention subagent request used the provider's native subagent, and
agent+runtimeModetogether returnedinvalid_requestverbatim.
Not reached live: the terminal missing state (both agents complied with the reminder; covered by the unit test) and mobile. Unrelated observation: a hard load of an Artifacts URL shows "environment is not connected" until Try again; that view is untouched by this PR.
On the versioned-file scope, I accept Bryant's product call now that the parser is hardened and tested.
Reviewed and tested with Claude Fable 5.1 in Claude Code.
Closes the last of Jackson's #124 findings: the artifact contracts documented in #75 had no implementation. Now that #109's shared planning artifacts are on
j5/main, a saved agent's declared output artifact is written there and checked.Instructions. When a launch snapshot declares an
outputArtifact, the persona prompt gains a "Handoff artifacts" section naming the exact file,handoffs/<agent>/<Artifact>-<task>.md(task = first eight characters of a uuid thread id, or eight hex characters of a hash for platform-spawned ids with colons), the artifact's checklist (the nine built-in templates from the persona contract, or a generic one for custom names), and how to find declaredinputArtifactswithlist_artifactsandread_artifact.Check and reminder. A J5 run-finalization observer (
agentHandoffObserver.ts), wrapped around the existing plan-export observer inserver.ts, checks the project's artifact list after each completed run of a saved-agent task and records one row per task inj5_agent_handoffs(J5 migration 12):writtenwhen the file exists; otherwisenudgedthe first time, with one system message queued into the agent's own thread asking it to write the file;missingon any later run that still ends without it, with no further reminder. This does not gate the parent:RunFinalizationService.finalizecommits the run's completion (and the delegated completion that wakes the parent) before it calls the observer, so the reminder is a follow-up run in the child's own thread plus a visible status. The nudge crosses from the orchestration runtime toThreadManagementServicethrough a shared queue layer, so no upstream service graph, command, or event changes.Writable for read-only agents. Two adapter appends make
write_artifactcallable without a prompt for every read-only Claude thread (dontAskallowlist) and every Codex thread under approval policynever; artifacts live in application storage, so neither widens the workspace sandbox.Versioned handoffs.
write_artifacton ahandoffs/path goes throughArtifactWorkspace.writeVersioned: the file keeps every version, newest first, behind a first-line marker that lists them, so a revised review can be compared with the earlier one on the Artifacts page. Other paths keep upstream's overwrite. Thewrite_artifactdescription and the orchestration instructions tell the model that handoff paths append.Surface.
getAgentHandoffs(read scope) andsubscribeHandoffRefreshes(stream) on the J5 RPC group; every handoff chip in an environment shares one query refreshed by that stream. The chip sits on the task's persona lock control and on its Agents-panel row on web/desktop and opens the file on the Artifacts page (the route gains apathsearch parameter); mobile shows a status control.Docs: user guide, artifacts guide, operations, the persona contract's artifact section, FORK.md.
Validation
Merge order
Part of GitHub stack
120, on top of #141.🤖 Generated with Claude Code