Skip to content

feat(agents): bind declared handoff artifacts to the shared artifacts system - #142

Merged
bryantderosier merged 4 commits into
feature/agent-delegate-taskfrom
feature/agent-handoff-artifacts
Sep 15, 2026
Merged

bryantderosier merged 4 commits into
feature/agent-delegate-taskfrom
feature/agent-handoff-artifacts

Conversation

@bryantderosier

@bryantderosier bryantderosier commented Sep 14, 2026 •

Copy link
Copy Markdown
Collaborator

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 declared inputArtifacts with list_artifacts and read_artifact.

Check and reminder. A J5 run-finalization observer (agentHandoffObserver.ts), wrapped around the existing plan-export observer in server.ts, checks the project's artifact list after each completed run of a saved-agent task and records one row per task in j5_agent_handoffs (J5 migration 12): written when the file exists; otherwise nudged the first time, with one system message queued into the agent's own thread asking it to write the file; missing on any later run that still ends without it, with no further reminder. This does not gate the parent: RunFinalizationService.finalize commits 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 to ThreadManagementService through a shared queue layer, so no upstream service graph, command, or event changes.

Writable for read-only agents. Two adapter appends make write_artifact callable without a prompt for every read-only Claude thread (dontAsk allowlist) and every Codex thread under approval policy never; artifacts live in application storage, so neither widens the workspace sandbox.

Versioned handoffs. write_artifact on a handoffs/ path goes through ArtifactWorkspace.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. The write_artifact description and the orchestration instructions tell the model that handoff paths append.

Surface. getAgentHandoffs (read scope) and subscribeHandoffRefreshes (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 a path search parameter); mobile shows a status control.

Docs: user guide, artifacts guide, operations, the persona contract's artifact section, FORK.md.

Validation

  • Server: J5 agent suite including the path convention, the instruction section, and the observer's waiting → nudged → missing (terminal) → written sequence with the refresh signal, against in-memory SQLite with a fake artifact workspace; artifact workspace versioning including quoted headings and trailing rules; adapter allowlist tests; J5 migration manifest; MCP surface tests. Contracts, client-runtime, web typecheck; J5 tests pass; lint clean.
  • Not exercised: a real provider writing the artifact end to end.

Merge order

Part of GitHub stack 120, on top of #141.

🤖 Generated with Claude Code

@bryantderosier
bryantderosier added this pull request to stack #120 September 14, 2026 12:36
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 effective changed lines (test files excluded in mixed PRs). labels Sep 14, 2026
@bryantderosier
bryantderosier force-pushed the feature/agent-handoff-artifacts branch 2 times, most recently from a222a11 to fe843f2 Compare September 14, 2026 12:45
@bryantderosier bryantderosier self-assigned this Sep 14, 2026
@bryantderosier
bryantderosier force-pushed the feature/agent-handoff-artifacts branch from fe843f2 to 224942f Compare September 14, 2026 13:14
@Jacksondr5
Jacksondr5 force-pushed the feature/agent-handoff-artifacts branch from 224942f to 8a94147 Compare September 14, 2026 21:36

@tyler-barton-horizon tyler-barton-horizon left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

  1. 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 the checkpoint.capture effect calls RunFinalizationService.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 for mode: "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.

  2. "Asks once" is not what the observer does. After a row reaches missing, the next completed run with no file sees previous.status === "missing" (not nudged), so it writes nudged again and queues another reminder. Later runs alternate between the two states. Either treat missing as terminal for nudging, or drop the "once" wording from the docs, the chip tooltip, and the nudge text.

  3. 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_artifact under handoffs/ 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.

Comment thread apps/server/src/j5/agents/agentHandoffObserver.ts Outdated
Comment thread apps/server/src/j5/agents/agentHandoffObserver.ts Outdated
Comment thread apps/server/src/j5/agents/agentHandoffNudgeWorker.ts
* 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: {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

@bryantderosier bryantderosier Sep 15, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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.

Comment thread apps/server/src/mcp/ArtifactMcpService.ts Outdated
Comment thread apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts
Comment thread packages/client-runtime/src/j5/agentPersonaAtoms.ts
Comment thread apps/server/src/j5/agents/agentHandoffObserver.test.ts
Comment thread apps/server/src/j5/agents/agentHandoffStore.ts Outdated
Comment thread FORK.md Outdated
bryantderosier and others added 3 commits September 15, 2026 10:41
… 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>
@bryantderosier
bryantderosier force-pushed the feature/agent-handoff-artifacts branch from 2530dd3 to eea29fe Compare September 15, 2026 14:55
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>
@github-actions github-actions Bot added size:XXL 1,000+ effective changed lines (test files excluded in mixed PRs). and removed size:XL 500-999 effective changed lines (test files excluded in mixed PRs). labels Sep 15, 2026
@bryantderosier

bryantderosier commented Sep 15, 2026 •

Copy link
Copy Markdown
Collaborator Author

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; missing is terminal so a task is reminded exactly once; the pre-approval docs say the spreads apply to every read-only Claude thread and every approval-never Codex thread; the chips share one environment-wide query refreshed by a new subscribeHandoffRefreshes stream; the observer test covers the waiting-run guard; the two nits are fixed.

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 write_artifact description and the orchestration instructions now say handoff paths append. I left that inline thread open for you.

PR body updated to match.

@tyler-barton-horizon tyler-barton-horizon left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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, row written, 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 to written, 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.md still 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 + runtimeMode together returned invalid_request verbatim.

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.

@bryantderosier
bryantderosier merged commit b9f456f into j5/main Sep 15, 2026
20 checks passed
@bryantderosier
bryantderosier deleted the feature/agent-handoff-artifacts branch September 15, 2026 16:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ effective changed lines (test files excluded in mixed PRs). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants