refine spawns a stray agent session named literally voro-{task_id} - #113
Merged
Conversation
A note-driven refine borrowed the dispatch template and never substituted
`{task_id}`, so every refine of every task launched a backgrounded session
called, literally, `voro-{task_id}` — one stray name shared by all of them.
Underneath sat three places that each computed a launch's identity ad hoc and
each replaced a different set of placeholders.
A `Launch` value in voro-core now names a dispatch, a refine or a planning
session once, and its session name, prompt/log file slug and launch-log label
all come from it. Templates gain `{session_name}`, which the built-in claude
verbs use in place of spelling `voro-{task_id}` themselves; a dispatch is still
`voro-<id>`, a refine is `voro-<id>-refine`, a planning session
`voro-plan-<project>`. `{session_name}` is honoured on dispatch and plan and
refused elsewhere, the rule `{model}` already followed, and `{task_id}` is now
refused on plan, whose target may be a project with no task. No verb was added:
an agent defining only `dispatch` refines exactly as before.
`ResolvedAgent::launch_command`/`plan_launch_command` return a fully rendered
command line, so the TUI crate no longer calls `.replace` on one, and every
template — commands, dispatch preamble, planning and refine prompts — goes
through a single-pass renderer that emits each bound value verbatim. Chained
replaces could not: a task body, branch name, document title or project name
discussing `{task_id}` or `{db}` was rewritten before the agent read it, which
is what corrupted this task's own body.
Verified with `cargo test --workspace` and `cargo clippy --workspace
--all-targets -- -D warnings`, plus an end-to-end run against a scratch
database: refining then dispatching one task launches `voro-1-refine` and
`voro-1`, with the placeholder-laden body and note reaching the agent
unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FxRm8eUG7HDnuZrJAs7niu
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Give every launch one identity, rendered in one place.
What changed
A note-driven refine borrowed the dispatch template and never substituted
{task_id}, so every refine of every task launched a backgrounded session called, literally,voro-{task_id}— one stray name shared by all of them. Underneath sat three renderers, each computing a launch's identity ad hoc and each replacing a different set of placeholders.One launch identity.
Launch(voro-core,agent.rs) names aDispatch, aRefineor aPlanonce; itssession_name(),slug()andtask_id()feed the session name, the prompt/log filenames and the launch-log label together. File slugs are byte-identical to what the three paths computed before (task-<id>,refine-<id>,plan-<p>), so no prompt or log filename moved.{session_name}, and one place that renders a command. The built-in claudedispatchandplannow name their session from{session_name}instead of spellingvoro-{task_id}: a dispatch staysvoro-<id>(the published contract), a refine isvoro-<id>-refine, a planning sessionvoro-plan-<project>.claude --helpconfirms--nameis not a--bg-only flag, so the foregroundplanverb carries it too.{session_name}is permitted ondispatch/planand refused on the session verbs — the rule{model}already followed — and{task_id}is now refused onplan, whose target may be a project with no task id to bind.ResolvedAgent::dispatch_command/plan_commandare replaced bylaunch_command/plan_launch_command, which take aLaunchSpecand return a fully rendered command line, so thevorocrate never calls.replaceon one again;shell_quotemoved to voro-core and is re-exported fromdispatch.rs, leaving app.rs untouched.No
expandverb. Refine keeps using thedispatchtemplate and passes a different name. The roster staysdispatch/sessions/attach/resume/plan, so a third-party agent defining onlydispatch/cmdrefines exactly as before. Refine also still opens no session row, unchanged, and stays fire-and-forget.One substitution routine.
voro_core::render(template, bindings)makes a single left-to-right pass, emits each bound value verbatim and never re-scans it; unknown{…}is copied through. ChainedString::replacecould not promise that — whichever untrusted value went in last, the ones before it were scanned — so a task body, branch name, document title or project name discussing{task_id}or{db}was rewritten before the agent read it. That is the defect that corrupted this task's own body.render_refine_prompt,render_preambleandrender_planning_promptnow compose innermost-block-first through it.refine()'s summary reports the session name as well as the log, since the launcher exits at birth and the log holds only its banner:task 1 sent for refinement by 'claude' as voro-1-refine (pid …) — log ….Docs
DESIGN.md §5 (placeholder inventory), §6 (refine launches a named session and still opens no row;
Expansioninherits its identity fromLaunch), §8 (the naming invariant beside the{model}rule; the one-renderer and one-identity paragraph; why no verb was added). docs/agent-integration.md: the built-in TOML block, a new{session_name}bullet, the reworked{task_id}andplanbullets.STARTER_HEADERprose. CHANGELOGChangedentry — this moves the published agent contract.Tests
New:
Launchnaming and slugs; the built-in claude rendering distinct names for dispatch/refine/plan with no{surviving; prompt-file quoting;validate_agentrefusing{session_name}/{task_id}on the session verbs and{task_id}onplan, and accepting both on dispatch/plan; the renderer itself in voro-core (a value containing another placeholder emitted verbatim in either binding order, longest-match wins); a refine and a dispatch of one task rendering distinct names asserted through markers andlaunches.log; a refine of a task whose body and note are full of{task_id}/{db}reaching the agent unchanged; a dispatch whose branch name and linked-doc title contain{task_id}/{db}reaching the agent unchanged. The existing refine and{task_id}-dispatch tests pass unchanged.Verification
cargo test --workspace(566 tests),cargo clippy --workspace --all-targets -- -D warnings,cargo fmt --all— all clean. Plus an end-to-end run against a scratch database with a stub agent: refining task 1 launchedvoro-1-refine, dispatching the same task launchedvoro-1, both with every placeholder bound, and the prompt carriedthe flag is {db}, literallyandkeep {task_id} as writtenverbatim whilevoro set 1 --db '…' --body-filestill resolved.Not done
Not pushed — the operator's standing rule is that no background session writes to a remote. The branch
launch-identityis committed locally in the worktree at.claude/worktrees/launch-identity.