Repository navigation
feat(review): emit Step 3A's fan-out as a workflow instead of a roster #8846
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
09a3971
ad6578c
f748caf
31cc238
7e9c945
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1565,12 +1565,15 @@ export function findingsSection( | |
| * Build one agent's brief and launch prompt, write the brief beside the plan, and | ||
| * return the key and the prompt for the caller to record and print. | ||
| * | ||
| * One body for both callers on purpose: the single-agent path and `--roster` must | ||
| * emit byte-identical prompts for the same agent, because the delivery check | ||
| * compares agents against records — a drift between the two paths would read as a | ||
| * rewritten launch on a run that did everything right. | ||
| * One body for every caller on purpose: the single-agent path, `--roster` and | ||
| * `emit-workflow` must emit byte-identical prompts for the same agent, because | ||
| * the delivery check compares agents against records — a drift between the | ||
| * paths would read as a rewritten launch on a run that did everything right. | ||
| * Exported for that reason: a third caller that rebuilt this would be a second | ||
| * implementation of the invariant, and byte-parity would become something a | ||
| * test asserts rather than something the code cannot break. | ||
| */ | ||
| function buildLaunch( | ||
| export function buildLaunch( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Declined (no code change) with evidence: the gate for this hunk exists and is exercised — the probe's affected set simply did not include it. 中文说明拒绝修改(不动代码),附证据:这个 hunk 的把关测试存在且会运行——只是探针的影响集没有包含它。 |
||
| report: PlanReport, | ||
| planPath: string, | ||
| spec: { | ||
|
|
@@ -1619,6 +1622,44 @@ function buildLaunch( | |
| return { key, prompt: buildChunkLaunchPrompt(report, id, briefFile) }; | ||
| } | ||
|
|
||
| /** | ||
| * Every launch the plan's roster requires, built through `buildLaunch` and | ||
| * matched to the roster's keys under one guard. | ||
| * | ||
| * The roster is what coverage holds the run to, and the key is what the | ||
| * records and briefs are written under. They are derived in two files, and if | ||
| * they ever disagree, every delivery check downstream reads "brief never | ||
| * reached an agent" on a run that did everything right. Both dispatchers — | ||
| * `--roster` and `emit-workflow` — consume this one mapping so the guard | ||
| * exists once; a caller that rebuilt the loop would be a second implementation | ||
| * of the invariant. | ||
| */ | ||
| export function buildRosterLaunches( | ||
| report: PlanReport, | ||
| planPath: string, | ||
| rules?: string, | ||
| ): Array<{ req: RequiredAgent; key: string; prompt: string }> { | ||
| const roster = requiredAgents(report as RosterPlan); | ||
| return roster.map((req) => { | ||
| const { key, prompt } = buildLaunch( | ||
| report, | ||
| planPath, | ||
| req.role === 'chunk' | ||
| ? { chunk: req.chunk } | ||
| : { role: req.role, file: req.file }, | ||
| rules, | ||
| ); | ||
| if (key !== req.key) { | ||
| throw new Error( | ||
| `built "${key}" where the roster requires "${req.key}" — the ` + | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] R3-2: Extracting the key-mismatch guard into the shared Failure scenario: run Suggested fix: restore command attribution in the shared message, e.g. 中文说明把 key 不一致守卫抽取进共享的 失败场景:在 roster key 派生与 建议修复:在共享信息中恢复命令归属,例如 — DeepSeek/deepseek-v4-flash via Qwen Code /review (v0.21.8) |
||
| 'record could never be matched to the requirement. This is a bug ' + | ||
| 'in the CLI, not in the call.', | ||
| ); | ||
| } | ||
| return { req, key, prompt }; | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * The digest that keys a findings role's record and brief: the identity of the | ||
| * launch material the key must tell apart — the findings list AND the effective | ||
|
|
@@ -1712,7 +1753,7 @@ function findingsFileFor( | |
| * paste to an agent as if the CLI wrote it. Control characters are flattened to | ||
| * spaces, and the separator glyph is stripped so a name cannot imitate one. | ||
| */ | ||
| function rosterLabel(req: RequiredAgent): string { | ||
| export function rosterLabel(req: RequiredAgent): string { | ||
| if (req.role === 'chunk') return `chunk ${req.chunk}`; | ||
| // The brief's label already reads `Agent 1a: Line-by-line correctness`; the | ||
| // rebuild hint downstream names roles, so keep the id visible when the label | ||
|
|
@@ -1737,29 +1778,10 @@ function runRoster(report: PlanReport, planPath: string, rules?: string): void { | |
| // The roster reads `plan.effort` (written by the capturing command), so a | ||
| // `medium` plan builds the reduced set here without an `--effort` flag — and | ||
| // `check-coverage` holds the run to that same set from the same field. | ||
| const roster = requiredAgents(report as RosterPlan); | ||
| const blocks = roster.map((req, i) => { | ||
| const { key, prompt } = buildLaunch( | ||
| report, | ||
| planPath, | ||
| req.role === 'chunk' | ||
| ? { chunk: req.chunk } | ||
| : { role: req.role, file: req.file }, | ||
| rules, | ||
| ); | ||
| // The roster is what coverage checks; the key is what this command records | ||
| // under. They are derived in two files, and if they ever disagree, every | ||
| // delivery check downstream reads "brief never reached an agent" on a run | ||
| // that did everything right. Refuse to hand out prompts that cannot match. | ||
| if (key !== req.key) { | ||
| throw new Error( | ||
| `agent-prompt: --roster built "${key}" where the roster requires ` + | ||
| `"${req.key}" — the record could never be matched to the requirement. ` + | ||
| 'This is a bug in the CLI, not in the call.', | ||
| ); | ||
| } | ||
| const launches = buildRosterLaunches(report, planPath, rules); | ||
| const blocks = launches.map(({ req, key, prompt }, i) => { | ||
| recordPrompt(planPath, key, prompt); | ||
| return `───── agent ${i + 1} of ${roster.length} — ${rosterLabel(req)} ─────\n\n${prompt}`; | ||
| return `───── agent ${i + 1} of ${launches.length} — ${rosterLabel(req)} ─────\n\n${prompt}`; | ||
| }); | ||
| // Worktree-mode reviews: remind the orchestrator of the exact Agent tool | ||
| // parameters at the point of action. A run that passed both `working_dir` | ||
|
|
@@ -1792,20 +1814,20 @@ function runRoster(report: PlanReport, planPath: string, rules?: string): void { | |
| `block VERBATIM.`; | ||
| writeStdoutLine( | ||
| [ | ||
| `${roster.length} agents required. Launch one agent per block below, ` + | ||
| `${launches.length} agents required. Launch one agent per block below, ` + | ||
| `passing its block VERBATIM — copy, do not retype. The ───── lines are ` + | ||
| `separators, not part of any prompt. This is the same roster ` + | ||
| `\`check-coverage\` reads out of the plan: a block you skip or reword is ` + | ||
| `a dimension nobody reviewed. Blocks are numbered \`agent k of ` + | ||
| `${roster.length}\` and the output ends with an end-of-roster line — if ` + | ||
| `${launches.length}\` and the output ends with an end-of-roster line — if ` + | ||
| `either is missing, this output was truncated in transit: every prompt ` + | ||
| `is also recorded on disk, so rebuild just the missing blocks with ` + | ||
| `--chunk <id>, or --role <r> (--file <path> for an invariant agent), ` + | ||
| `plus the same --rules this call was given.` + | ||
| descNote + | ||
| paramNote, | ||
| ...blocks, | ||
| `───── end of roster — ${roster.length} agents ─────`, | ||
| `───── end of roster — ${launches.length} agents ─────`, | ||
| ].join('\n\n'), | ||
| ); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Byte-identical prompts do not mean identical agents: workflow dispatch replaces the system prompt with one that orders the agent to be terse.
This docstring's invariant is that
--rosterandemit-workflowlaunch the same agent. But the emitted script dispatches with onlylabel/phase, which takes the workflow fast path, and that path overrides the system prompt withWORKFLOW_SUBAGENT_SYSTEM_PROMPT(workflow-orchestrator.ts:437-439) — it tells the agent its final text is a return value to a script and ends with "Be concise. The script will parse your output."Review briefs ask for multi-field findings with file/line/failure-scenario prose. The same roster run through the workflow yields shorter, thinner findings than
--rosterfor the same diff. Since the PR's stated purpose is an A/B comparing two dispatchers, the comparison silently varies the agents' output contract as well as who launches them.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Escalated to maintainer — not fixable from this PR's emitted artifact. Verified at the code: the terseness directive comes from the workflow runtime's fast path —
WORKFLOW_SUBAGENT_SYSTEM_PROMPT(workflow-orchestrator.ts) replaces the general-purpose system prompt on dispatches withoutagentType, andKNOWN_AGENT_OPTSin workflow-sandbox.ts offers no opt-out. So the same roster run through the workflow really does get a different output contract than--roster; that is a property of the runtime, not of the script this command emits.Options as I see them:
My recommendation: (1) now, with (2) tracked as a follow-up — but this is an experiment-design call, so I am leaving the question open: which of the three do you want?
中文说明
升级给维护者——无法在本 PR 的发出物层面修复。 已在代码中核实:简洁化指令来自 workflow 运行时的快速路径——
WORKFLOW_SUBAGENT_SYSTEM_PROMPT(workflow-orchestrator.ts)在不带agentType的分发中替换通用系统提示,而 workflow-sandbox.ts 的KNOWN_AGENT_OPTS不提供退出选项。因此同一份 roster 经 workflow 运行时的确会得到与--roster不同的输出契约;这是运行时的属性,不是本命令所发脚本的属性。我看到的选项:
我的建议:先按 (1) 执行,并把 (2) 作为后续工作跟踪——但这属于实验设计决策,因此问题保持开放:三者之中您希望选哪一个?