# DSH orchestration for the security audit workflow How to run the six phases of `SKILL.md` with this harness's delegation tools. The phase semantics, evidence bar, and artifacts come from the upstream corpus; this file only covers mechanics. ## Which tool | Situation | Use | |---|---| | One agent, one job, result needed in this conversation | `subagent` | | A wave of hunters, one verifier per candidate, fan-out verification | `workflow` with `agent()` | | Work that genuinely needs *this* conversation's reasoning | `subagent_fork` — never for hunters, verifiers, or critics | `workflow` is the default for phases 2, 3, and 5 because the runs are homogeneous fan-outs: the script holds the orchestration, and each `agent()` gets a fresh context. Subagent failures resolve to `null` (or throw, for a bad prompt schema) and must be reported, never silently dropped. `pipeline(items, ...stages)` runs each item through every stage with no barrier, which is what candidate validation wants: one candidate can be verified while another is still being written up. `parallel(thunks)` is a barrier — use it only when a stage needs every prior result at once, such as a coverage critic that must see all hunter results. ## Prompt skeleton Every hunter, verifier, and critic prompt needs these fields, because a fresh agent knows nothing: ``` Role: {research | general} — {hunter | verifier | critic} for one coverage unit. Target (read-only): {absolute target path} Assigned root: {output-dir}/agents/{agent-id}/ - write ONLY inside scratch/ in that root - never write artifacts/, target source, or any file outside this root - never edit {architecture.md, coverage-ledger.json, findings.json} Unit: {unit id, surface, boundary, attack classes} Corpus to follow: {abs path to HUNTING.md / ATTACK-CLASSES.md section + companion} Evidence allowed: source reading only. Do NOT run builds, tests, installers, or target code: no verified OS-enforced sandbox is available on this host. Return: {exact output contract from HUNTING.md} ``` Pass absolute corpus paths (`/ATTACK-CLASSES.md`), not names — the child does not load this skill. ## Phase 1 — reconnaissance Four baseline calls, in one `parallel` block since they are independent: ```js phase("recon"); const recon = await parallel([ () => agent(reconPrompt("architecture"), { label: "recon-architecture" }), () => agent(reconPrompt("trust-boundaries"), { label: "recon-boundaries" }), () => agent(reconPrompt("input-surfaces"), { label: "recon-surfaces" }), () => agent(reconPrompt("prior-evidence"), { label: "recon-prior" }), ]); return recon.filter(Boolean).length; ``` The parent — not the agents — writes `architecture.md` and seeds `coverage-ledger.json` from their returned text, then runs: ```powershell node "\validate-coverage-ledger.cjs" "\coverage-ledger.json" ``` ## Phase 2 — hunter waves One `agent()` per assigned unit. Keep the wave in a `pipeline` so a unit that fails cannot stall the others: ```js phase("wave-1"); const units = [...]; // from the ledger, priority order, budget-clipped const results = await pipeline(units, (unit) => agent(hunterPrompt(unit), { label: `hunter-${unit.id}`, schema: { type: "object", properties: { unit_id: { type: "string" }, candidates: { type: "array", items: { type: "object", properties: { fingerprint: { type: "string" }, claim: { type: "string" }, source_trace: { type: "array", items: { type: "string" } }, result: { type: "string" }, }, required: ["fingerprint", "claim", "source_trace", "result"], additionalProperties: false } }, checked: { type: "array", items: { type: "string" } }, gaps: { type: "array", items: { type: "string" } }, }, required: ["unit_id", "candidates", "checked", "gaps"], additionalProperties: false, }, })); const failed = results.filter((r) => r === null).length; ``` A structured `schema` is worth it here: the parent must deduplicate fingerprints mechanically, and prose results cannot be deduplicated honestly. A `null` result is a failed unit — mark it `deferred` with the failure reason; never treat it as "no findings". Write the wave's accepted candidates and `checked` ranges into the ledger yourself. Then spend the reserved post-wave critic as its own `agent()` call that sees the wave results — that is the one place a `parallel` barrier is appropriate. ## Phase 3 — candidate validation Fresh verifier per unique fingerprint, each asked to *disprove* the claim: ```js phase("validate"); const verdicts = await pipeline(candidates, (c) => agent(verifierPrompt(c), { label: `verify-${c.fingerprint.slice(0, 8)}`, schema: verdictSchema, // confirmed | needs_validation | rejected + evidence })); ``` Never batch two candidates into one verifier, and never let a verifier see the hunter's reasoning — only the claim, the fingerprint, and the source path. ## Phase 4 — structured output Parent writes `findings.json`, then runs both validators. Treat a validator failure as a real defect in the record, not as noise to work around. Exit codes: `node validate-findings.cjs` and `node validate-coverage-ledger.cjs` both exit non-zero on invalid input and print the failing pointer. ## Phase 5 — independent record verification A second fresh wave over the `confirmed` and `needs_validation` records that survived Phase 4. A material replacement (a corrected source claim, a changed verdict) needs *another* independent verifier, not the same one re-run — re-running an agent that already wrote a verdict is not verification. ## Phase 6 — report Parent writes `REPORT.md`, `FINDINGS-DETAIL.md`, `NEEDS-VALIDATION.md` from the validated records only. No new agents; no live probes in the reproduction steps. ## Budget accounting - Every `agent()` and `subagent` call is one invocation against `budget`. - Log the count per phase in `run-metadata.json` as you spend it, so a mid-run stop can state actual spend. - Before a wave, count: units + reserved post-wave critic + reserved final-clean critic + validation reserve. If that exceeds the remaining budget, take the `deferred` path in `SKILL.md`, not a thinner wave. ## Host-safety reminder DSH's file sandbox restricts writes, not behaviour. A subagent running `pwsh` inherits this session's authority — under `danger-full-access` that is the whole machine, and network egress is open. Hence the hard rule in `SKILL.md`: read source, do not execute target code, and report the missing boundary as `needs_validation`. If the user wants executed evidence, ask for a container or VM with blocked egress and a read-only target mount, then verify those properties before relying on them.