test+docs(sf): parent-trace test + dispatching-subagents skill doc

Follows up the parent-trace dispatch wiring (bundled into bc9cf4fef +
2508822b8). Adds:

- src/resources/extensions/subagent/tests/parent-trace.test.ts — 7 cases
  covering the composeTaskWithParentTrace helper: undefined/empty/whitespace
  pass-through, tag wrapping, task-after-trace ordering, content trimming,
  embedded verifier instructions ("hedge words", "tool errors").
- src/resources/extensions/subagent/index.ts — exports composeTaskWithParentTrace
  so the test can import it.
- skills/dispatching-subagents — new "Parent trace (for verifier/review
  subagents)" subsection documents the field at TaskItem / ChainItem /
  batch level, the per-task override, and the chain (step 0 only) and
  debate (round 1 only) behaviour.

PDD packet (inline; small follow-up to the architectural change):
- Purpose: parent-trace plumbing has a falsifiable test and is documented in
  the canonical dispatching-subagents skill so callers know how to use it.
- Consumer: the dispatching-subagents skill (loaded by every agent that
  calls the subagent tool); the test (covers regression).
- Contract: 7 test cases pass; SKILL.md contains the documented field at
  three schema levels with the override and per-mode behaviour notes.
- Evidence:
  - tests/parent-trace.test.ts → 7/7 pass via the SF resolve-ts loader
  - npm run typecheck:extensions exits 0
  - All 35 subagent suite tests pass
- Non-goals: not changing the dispatch wiring (already in); not adding
  parent-trace handling to background jobs (separate slice if needed).
- Invariants (safety only — sync helper + pure prose docs):
  - composeTaskWithParentTrace returns task unchanged when trace is empty.
  - The original task always appears after the closing tag.
  - Trimmed content is what gets injected, not the raw padded input.
- Assumptions: tests load TS via the resolve-ts.mjs hook (standard SF
  pattern); skills load SKILL.md from dist via copy-resources.
This commit is contained in:
Mikael Hugo 2026-05-02 03:29:56 +02:00
parent 2508822b8f
commit fc1ed49d72
3 changed files with 107 additions and 1 deletions

View file

@ -121,6 +121,34 @@ Use debate mode for high-stakes decisions, plan review, architecture review,
and migrations where the cost of a weak plan is high. Keep `rounds` between 2
and 3 by default; max is 5.
### Parent trace (for verifier/review subagents)
Set `parentTrace` when dispatching a verifier or reviewer that should audit
what the parent **actually did**, not just what the parent's prose claims. The
dispatch tool wraps the value in a `<parent_trace>...</parent_trace>` block
prepended to the task, with embedded verifier instructions to look for hedge
words, glossed-over tool errors, and claims without Command/Output traces. The
parent assembles its own trace (recent tool-call summary); the dispatch tool
only plumbs it through.
```
subagent({
parentTrace: "<recent tool-call summary: commands run, outputs, edits made>",
tasks: [
{ agent: "reviewer", task: "Audit the migration for correctness against the spec." },
{ agent: "tester", task: "Audit test coverage for the new code paths." }
]
})
```
Per-task `parentTrace` overrides the batch-level value — set it on a single
`TaskItem` (or `ChainItem`) when one subagent needs a different trace.
For chain mode, only step 0 receives the trace; later steps see `{previous}`.
For debate mode, only round 1 receives the trace; later rounds have the debate
transcript. For parallel and single mode, the trace is always injected when
set.
### Agent selection and model overrides
sf routes subagents through agent definitions in `src/resources/agents/`,

View file

@ -1034,7 +1034,7 @@ async function mapWithConcurrencyLimit<TIn, TOut>(
* words, glossed-over tool errors, untraced self-reports) so review subagent
* prompts do not need to repeat them.
*/
function composeTaskWithParentTrace(
export function composeTaskWithParentTrace(
task: string,
parentTrace: string | undefined,
): string {

View file

@ -0,0 +1,78 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { composeTaskWithParentTrace } from "../index.js";
describe("composeTaskWithParentTrace", () => {
it("returns task unchanged when parentTrace is undefined", () => {
const task = "do the thing";
assert.equal(composeTaskWithParentTrace(task, undefined), task);
});
it("returns task unchanged when parentTrace is an empty string", () => {
const task = "do the thing";
assert.equal(composeTaskWithParentTrace(task, ""), task);
});
it("returns task unchanged when parentTrace is whitespace-only", () => {
const task = "do the thing";
assert.equal(composeTaskWithParentTrace(task, " \n\t "), task);
});
it("wraps trace content in <parent_trace> tags when content is provided", () => {
const task = "verify the parent's claim";
const trace = "ran `npm test`\nexit 0";
const result = composeTaskWithParentTrace(task, trace);
assert.ok(
result.includes("<parent_trace>"),
"should include opening tag",
);
assert.ok(
result.includes("</parent_trace>"),
"should include closing tag",
);
assert.ok(result.includes(trace), "should include trimmed trace content");
});
it("places the original task after the closing </parent_trace> tag", () => {
const task = "verify the parent's claim";
const trace = "ran `npm test`\nexit 0";
const result = composeTaskWithParentTrace(task, trace);
const closeIdx = result.indexOf("</parent_trace>");
const taskIdx = result.indexOf(task);
assert.ok(closeIdx >= 0, "closing tag must be present");
assert.ok(taskIdx >= 0, "task must be present");
assert.ok(
taskIdx > closeIdx,
"task must appear after the closing </parent_trace> tag",
);
});
it("trims whitespace from the parentTrace content before injection", () => {
const task = "audit";
const trace = "the actual trace line";
const padded = `\n\n ${trace} \n\n`;
const result = composeTaskWithParentTrace(task, padded);
assert.ok(
result.includes(trace),
"trimmed trace content should appear in result",
);
assert.ok(
!result.includes(padded),
"untrimmed padded trace should not appear verbatim",
);
});
it("includes verifier instruction phrases for hedge words and tool errors", () => {
const task = "audit";
const trace = "some trace";
const result = composeTaskWithParentTrace(task, trace);
assert.ok(
result.includes("hedge words"),
"should mention hedge words in verifier instructions",
);
assert.ok(
result.includes("tool errors"),
"should mention tool errors in verifier instructions",
);
});
});