| name | spec |
| disallowed-tools | NotebookEdit |
| description | Requirements specification writer with multi-round Q&A discovery. Transforms vague ideas into structured specs compatible with /harness input. Modes — quick (orchestrator only, inline) / deep (4 analysts + Critic via plugin-shipped native Workflow segments, opt-in gated). Use when you need a well-defined spec before starting implementation. |
Agent Harness Spec
You are orchestrating a requirements specification workflow with multi-round Q&A discovery and selectable quick or deep analysis mode.
Core principle: Ambiguity is the enemy of good specs. Every vague requirement must be surfaced and resolved before writing the spec. The output must be immediately usable as /harness input.
Zero-setup: Works with or without a git repository. No codebase required — spec can describe a greenfield project.
Table of Contents (NEW in 8.4 — m5 maintainability; v2 hardening anchor accuracy fix)
Skim this list to locate sections by name. Section-anchor cross-references throughout the document use these heading names rather than line numbers (see m3+M3 anchor-rot fix). Each entry below lists the literal heading text so Ctrl+F finds the section on first try.
## Environment Detection — has_git flag.
## User Language Detection — user_lang.
## Mode Gate — quick(inline) vs deep(workflow) path resolution (single source: templates/_shared/mode_gate.md; the roundtrip is removed except §Ambiguity Prompt when opt-in is absent).
## Standard Status Format — phase enumeration (setup → completed; new in 8.4: convention_scan_active, critic_*, re_*, critic_halted). Phase labels listed inline.
## Session Recovery — Resume jump table per phase; backward-compat policy (M14 + saved_phase mechanism); segments re-RUN across sessions (runIds audit-only).
## Workflow (top-level container) → ### Step 1: Setup — slugify + slug-collision check (M15) + state.json schema doc + §Atomicity Contract (C6, v2-extended enumeration).
### Step 1.5: Convention Scan (NEW in 8.4) — --reference flag step 4.5, has_git=true/false branches, §Step 1.5 conventions field contract (canonical SSOT for state.conventions enum + M16 SYNC-WITH markers).
### Phase 1 — Requirements Discovery (Multi-round Q&A) — idempotent state init (qa_round preserved on Resume).
### Phase 2: Spec Generation — Quick mode (#### Phase 2-Q, inline single synthesis) / Deep mode (#### Phase 2-D, WORKFLOW path: ##### Plan segment + ##### Eval segment + Critic Gate + ##### Re-synthesis).
### HARD GATE — Spec Approval — final approval gate with translated 3-way options + Modify reset.
### Phase 3 — Handoff — artifacts persistence, slug-safe /harness invocation, cleanup safety guard, M17 variable↔filename mapping.
### Status Check (anytime) — status summary command (subsection of Phase 3).
## Spec Output Format — 7-section template with /harness compatibility mapping.
## Model Selection — model_config preset → role mapping (advisor; workflow args.models).
Environment Detection
At startup, detect whether the current directory is inside a git repository:
git rev-parse --is-inside-work-tree 2>/dev/null
- If the command succeeds →
has_git = true
- If the command fails →
has_git = false
Store has_git in state.json. This flag controls whether git operations (branch creation) are performed.
User Language Detection
Detect the user's language from their most recent message. Store as user_lang in state.json (e.g. "ko", "en", "ja", "zh", "es", "de", etc.).
All user-facing communication must be in the detected language: progress updates, questions, confirmations, error messages, spec content, confirmation gate prompts and options.
Re-detection: On every user message, check if the language has changed. If so, update user_lang and switch all subsequent communication.
What stays in English: Template instructions (this file and templates/*.md), state.json field names, file names (spec.md), git branch names (if has_git), Workflow args field names.
Mode Gate — path & mode resolution (single source: templates/_shared/mode_gate.md)
Apply the shared opt-in convention in templates/_shared/mode_gate.md. /spec-specific resolution (the mode-selection roundtrip is removed EXCEPT §Ambiguity Prompt, which fires only when opt-in is absent):
| Signal (first match wins) | mode | path_resolved |
|---|
has_git == false | quick | inline (engine isolation requires git) |
--mode quick | quick | inline |
Workflow tool NOT available this session | quick | inline (notify only if an explicit --mode deep was requested) |
--mode deep (or comprehensive/thorough/multi) | deep | workflow |
no --mode AND session is in ultracode mode | deep | workflow |
no --mode, no opt-in | quick | inline (interactive + engine available → asks first, §Ambiguity Prompt) |
- Deep mode exists ONLY on the workflow path — the engine's
parallel() fan-out replaces the old hand-rolled "Launch 4 sub-agents in parallel" prose (pilot precedent: /harness standard/multi). The inline path is the preserved quick mode (Phase 2-Q: orchestrator writes spec.md directly).
- Graceful fallback: if a
Workflow invocation errors at any step, print [harness] ⚠ Workflow engine unavailable — falling back to the inline quick path. (in user_lang), set path_resolved → "inline", mode → "quick", and continue the CURRENT step on the quick path (see Phase 2-D fallback rules). Never error out.
- Record
path_resolved in state.json and show Path in the status format.
Standard Status Format
When displaying status, read .harness/state.json and print (in user_lang):
[harness]
Skill : spec
Task : <task>
Mode : <quick | deep>
Path : <inline | workflow> (<reason per §Path Transparency>)
Model : <model_config preset name> ← omit if quick mode
Phase : <phase label>
QA Round : <qa_round> / 3
Branch : <branch> ← omit if has_git == false
Output : <docs_path>
Phase labels (in roughly execution order):
setup → "Setup — initializing"
convention_scan_active → "Convention Scan — running"
qa_active → "Q&A — discovering requirements"
qa_complete → "Q&A complete — ready for spec generation"
gen_ready → "Generating specification"
critic_active → "Critic — reviewing spec" (deep mode only)
re_synthesis_active → "Re-synthesis — second round (Critic findings applied)" (deep mode only)
re_critic_active → "Re-critic — second round review" (deep mode only)
critic_complete → "Critic complete — awaiting Final HARD-GATE" (deep mode only)
critic_halted → "Critic flow halted — manual intervention required" (terminal, deep mode only)
spec_ready → "Spec ready — awaiting approval"
completed → "Completed"
Session Recovery
Before starting a new task, check if .harness/state.json already exists and state.json.skill equals "spec":
-
If it exists and matches, print status in the standard format (including Model line from model_config if deep mode), prefixed with [harness] Previous spec session detected.
-
Restore model_config from state.json (deep mode only). Apply it to all subsequent sub-agent launches and Workflow args.models.
2.5. Re-resolve §Mode Gate (the new session may lack the Workflow tool or the opt-in) and update path_resolved — a session that started on the workflow path may legitimately resume on the inline path. Cross-session resume re-RUNS segments; state.runs.*.runId values are audit-only (resumeFromRunId is same-session only — never attempt it across sessions). If a deep-mode resume lands on the inline path, phases that require a segment run degrade per the Phase 2-D fallback rules (notify the user). This re-resolution reuses the stored { mode, path_resolved } and MUST NOT re-fire §Ambiguity Prompt — only the existing workflow→inline downgrade (engine or git now absent) may change the stored path.
-
Ask the user using AskUserQuestion (in user_lang):
header: "Session"
question: "[harness] Previous spec session detected. [print status in standard format]. Resume, restart, or stop?"
options:
- label: "Resume" / description: "Continue from {phase} where the previous session left off"
- label: "Restart" / description: "Delete .harness/ and start from scratch"
- label: "Stop" / description: "Delete .harness/ and halt"
(M5) Dynamic Resume description override for critic_halted: when state.json.phase == "critic_halted" (terminal), the Resume option does not advance any phase — it only surfaces the manual-intervention message defined in the critic_halted action below. To prevent users from selecting Resume expecting auto-progress, override the Resume option's description (in user_lang) to: "Surface manual intervention message only — no automatic phase advance possible from critic_halted." Keep the label "Resume" so the option key remains stable; make the action explicit through the description.
(M14) Dynamic Resume description override for pre-8.4 sessions (NEW in 8.4 hardening): if state.conventions is null (the 8.4 schema field) AND state.phase is qa_complete / gen_ready / qa_active (any phase that would route into Phase 2-D), the session was created under a pre-8.4 /spec and resuming it under 8.4 will hit the Phase 2-D step 1 null-conventions hard-error halt before producing useful output. Override the Resume option description (in user_lang) to: "Pre-8.4 session detected — Convention Scan (Step 1.5) will re-run before Phase 2 to populate conventions. Existing Q&A answers are preserved." Resume control flow (NEW in 8.4 v2 hardening — saved_phase mechanism): (1) capture saved_phase = state.phase into local memory BEFORE entering Step 1.5; (2) re-run Step 1.5 (which transitions phase: null/qa_complete → convention_scan_active → qa_active by exit time, idempotent on re-entry); (3) once Step 1.5 exits, restore the original phase: write state.phase = saved_phase (single atomic write per §Atomicity Contract); (4) jump to the standard Resume jump table entry for saved_phase (qa_complete → Phase 2 entry, gen_ready → Phase 2 entry, qa_active → Phase 1 Q&A resume). Without this saved_phase capture+restore, Step 1.5's phase rewrite to qa_active would silently regress qa_complete/gen_ready resumes back to Q&A — a backward-compat data loss bug. Keep the option label "Resume" stable.
Actions per selection:
- Resume: Jump to the step matching state.json phase:
setup → Step 1
convention_scan_active (NEW in 8.4) → re-run Step 1.5 from start (idempotent — scanner overwrites .harness/conventions.md; --reference priority branch is also idempotent)
qa_active → Phase 1 Q&A (resume current qa_round)
qa_complete → Phase 2 Spec Generation (entry)
gen_ready → Phase 2 Spec Generation (entry)
critic_active (NEW in 8.4, deep mode) — branch on state.critic.applied:
"approved" → skip directly to Final HARD-GATE (Critic already concluded). (m5) Reachability note: in normal execution Phase 2c-D step 5 "Approve as-is selected" branch wrote critic.applied = "approved" then phase = "critic_complete" in that order before the §Atomicity Contract was added; a crash between those two legacy non-atomic state.json writes is the only path that leaves phase == "critic_active" AND critic.applied == "approved". This branch handles that legacy recovery edge case. Under the 8.4 §Atomicity Contract (single-write rule), new transitions cannot reproduce this race; this Recovery branch is preserved for backward compatibility with pre-fix sessions. (NEW in 8.4 v2 hardening) last_findings_path null-safe guard: before HARD-GATE display, check state.critic.last_findings_path. If null (legacy race recovery + no findings file produced + no valid path), HARD-GATE display MUST surface the message [harness] ⚠ Recovery from legacy critic_active+approved race — no critic_findings.md path recorded. Manual review recommended. and proceed without the findings-path display panel. If non-null but the file does not exist (cleanup ran), surface a similar message and proceed without panel. This prevents a null dereference / file-not-found error during the HARD-GATE rendering.
"pending" → re-present Critic Gate using state.critic.last_findings_path (do NOT re-run the Eval segment — findings already exist on disk)
"revised" → re-enter Phase 2d-D step 3 (re-run the Eval segment on the revised spec.md)
- otherwise (null/unknown) → re-run Phase 2c-D (Eval segment) from start
re_synthesis_active (NEW in 8.4, deep mode) → re-enter Phase 2d-D step 1 (Re-synthesis — Plan segment with reSynthesisOnly: true; priorProposals from .harness/spec/proposals.json, criticFindings from state.critic.last_findings_path. If proposals.json is missing, re-run the full Plan segment instead — reSynthesisOnly: false — and warn the user that analysts re-ran); state.critic.round is at its pre-increment value (0)
re_critic_active (NEW in 8.4, deep mode) → re-enter Phase 2d-D step 3 (Eval segment re-run); state.critic.round is already 1
critic_complete (NEW in 8.4, deep mode) → Final HARD-GATE (HARD-GATE entry will normalize phase → "spec_ready")
critic_halted (NEW in 8.4, deep mode, terminal) — do NOT auto-resume. Surface this state to the user with the message (in user_lang) (m6): "Previous spec session halted in Critic flow (failure_reason: <state.critic.failure_reason>). Manual intervention required — review .harness/spec/critic_findings.md. To restart cleanly: choose Restart/Stop, or fix manually + edit state.json setting phase to qa_active (fresh start with same Q&A). (M1) Important when manually editing: also verify state.conventions — if it is "file:.harness/conventions.md" but .harness/conventions.md no longer exists (e.g., cleanup ran or .harness/ was deleted), reset state.conventions to null before resuming, so Step 1.5 re-runs to repopulate conventions; otherwise Phase 2-D step 1 will hard-error halt on the null-conventions guard immediately after resume." Do NOT auto-route to any phase.
spec_ready → HARD GATE (show existing spec.md)
completed → no active session, proceed to Step 1
- Restart: Delete
.harness/ directory and proceed to Step 1
- Stop: Delete
.harness/ directory and halt
Backward compat (8.4): pre-8.4 state.json files lack the cli_flags.reference, conventions, and critic fields. Treat each as null default via the state.get(field, null) pattern. (M9) Cross-skill policy asymmetry — intentional: this /spec policy diverges from /harness's pre-v8.1 soft-default backward-compat policy in skills/harness/SKILL.md. /harness recovers missing fields silently and proceeds; /spec halts. Reason: 8.4 analyst injection requires state.conventions to be set (the 4-analyst deep pipeline depends on convention context for high-quality output), so silently degrading pre-8.4 sessions through the 8.4 flow would produce lower-quality specs without user awareness. A pre-8.4 state.json reaching qa_complete and resuming under 8.4 will: (a) treat conventions as null and trigger a hard-error halt at Phase 2-D step 1 (per single-source-of-truth contract), forcing the user to either Restart or fix conventions manually — this halt is the intentional design, not a bug.
If .harness/state.json does not exist (or skill field is not "spec"), proceed to Step 1 normally.
Smart Routing
Before beginning, evaluate the user's request. If it does not match a requirements specification task, suggest a better skill using AskUserQuestion (in user_lang):
| Signal | Suggested Skill |
|---|
| User describes working code to improve | /refactor |
| User has a spec and wants implementation | /harness |
| User wants to understand existing code | /codebase-audit |
When a mismatch is detected, ask using AskUserQuestion:
header: "Routing"
question: "[detected mismatch]. A different skill may be more appropriate."
options:
- label: "Switch: /{suggested skill}" / description: "{why the suggested skill fits better}"
- label: "Continue with /spec" / description: "Proceed with requirements specification anyway"
If the user selects "Continue with /spec", proceed normally.
Workflow
When the user provides a task description (via $ARGUMENTS or in conversation), execute this workflow:
Step 1: Setup
-
Detect user language from the task description. Store as user_lang.
-
Slugify the task: lowercase, transliterate non-ASCII to ASCII, remove non-word chars except hyphens, replace spaces with hyphens, truncate to 50 chars. Store as <slug>.
(M15) Slug collision check (NEW in 8.4 hardening): before creating directories in step 3, check whether docs/harness/<slug>/ already contains any of the spec-persisted artifacts (spec.md, qa_notes.md, critic_findings.md, conventions.md). If yes, the slug collides with a prior /spec run — Phase 3 step 3 would silently overwrite those artifacts on cleanup. Resolve via AskUserQuestion (translate to user_lang):
header: "Slug collision"
question: "docs/harness/<slug>/ already contains spec artifacts from a prior run. Continuing will overwrite them at Phase 3."
options:
- label: "Append timestamp" / description: "Use slug-<UTC-YYYYMMDDHHMMSS> (e.g. <slug>-20261231235959) to preserve prior artifacts"
- label: "Overwrite" / description: "Continue with this slug; prior artifacts WILL be overwritten at Phase 3"
- label: "Stop" / description: "Halt — choose a different task description and retry"
On "Append timestamp", recompute <slug> accordingly (the timestamp suffix keeps slug ≤ 50 chars by truncating the base slug to 35 chars before suffixing). On "Stop", halt. On "Overwrite", proceed.
-
Create directories: .harness/, .harness/spec/, docs/harness/<slug>/
-
Create git branch (if has_git): git checkout -b harness/spec-<slug>. If has_git == false, skip this step entirely.
4.5. Parse --reference <path> flag (NEW in 8.4):
- If
--reference <path> provided, validate as follows. On any validation failure, halt with an explicit error message naming the failed check (mirror workflow --output-dir halt-on-fail behavior — do NOT silently fall back to auto-detect):
- Base validation: pass
validate_path(path, kind=file_reference) per workflow §Path Validator (relative path, no .. segment, inside repo_path, outside .harness/, docs/harness/*, memory/).
- Extension whitelist (NEW in 8.4 for
--reference): file extension MUST be one of .md, .txt, .markdown. Other extensions (e.g., .env, .json, .yml, .pem, binary) → halt with "unsupported reference extension."
- Symlink rejection (NEW in 8.4 for
--reference): if Path(path).is_symlink() → halt with "symbolic links not allowed for --reference." (s2) Intermediate-directory symlink check (NEW in 8.4 hardening): even if the leaf is not a symlink, an intermediate directory in path (e.g., path = "docs/foo.md" where docs/ itself is a symlink to /etc) can still escape the repo. Compare Path(path).resolve() against Path(path).absolute() — if they differ, an intermediate symlink resolved away from the literal path; halt with "symbolic links in intermediate directories not allowed for --reference." On Windows, this also catches NTFS junction points (which .resolve() follows but .absolute() does not). The Base validation step 1's validate_path(kind=file_reference) containment check (inside repo_path) is then the second line of defense: even if a symlink resolves to within the repo (rare but possible), the explicit literal-vs-resolved check above flags it for review.
- Size limit (NEW in 8.4 for
--reference): file size > 200 KB OR line count > 5000 → halt with "reference file too large." Convention files should be human-curated, not auto-generated dumps.
- On valid path: normalize to a repo-relative path (relative to
repo_path; no leading / or drive letter; e.g., docs/references/spec.md) and store as cli_flags.reference: <normalized_path> in state.json (audit-friendly). Will be consumed by Step 1.5 Convention Scan. (M4: prior wording "absolute repo-relative path" was internally contradictory and risked OS-absolute paths leaking into state.json on Windows.)
- If not provided:
cli_flags.reference: null.
-
Mode Gate resolution: apply §Mode Gate INCLUDING §Ambiguity Prompt (single source: templates/_shared/mode_gate.md). The mode roundtrip is removed EXCEPT this prompt, which fires only when NO opt-in is present (no --mode, ultracode OFF, Workflow tool available, has_git == true, interactive session, no --no-prompt). Skill modes: quick(inline) / deep(workflow). ultracode-target (step 4 default): deep. Store mode and path_resolved in state.json. Then emit §Path Transparency — show Path : <inline | workflow> (<reason>). If the user explicitly requested --mode deep but the gate resolved to inline (Workflow tool unavailable or has_git == false), notify (in user_lang): "deep mode requires the native Workflow engine and git — proceeding on the inline quick path."
-
Model configuration selection (deep mode only):
If mode is quick, skip this step entirely — no sub-agents are used.
If mode is deep:
If --model-config <preset> was passed, use it directly. Otherwise, use AskUserQuestion to ask the user (in user_lang):
header: "Model"
question: "Select model configuration for sub-agents:"
options:
- label: "default" / description: "Inherit parent model, no changes"
- label: "all-opus" / description: "All sub-agents use Opus (highest quality)"
- label: "balanced (Recommended)" / description: "Sonnet executor + Opus advisor (cost-efficient)"
- label: "economy" / description: "Haiku executor + Sonnet advisor (max savings)"
If "Other" selected: Parse custom format executor:<model>,advisor:<model>. Validate each model name — only opus, sonnet, haiku are allowed (case-insensitive). If any model name is invalid, inform the user which value is invalid and re-ask (max 3 retries, then apply balanced as default). Show parsed result and ask for confirmation before proceeding.
Model config is set once at session start and cannot be changed mid-session.
Store result as model_config object: { "preset": "<name>", "executor": "<model|null>", "advisor": "<model|null>" }. For default preset, store { "preset": "default" }.
-
Write .harness/state.json with the following fields:
-
task — the user's task description string
-
skill — "spec" (constant)
-
mode — "quick" or "deep"
-
path_resolved — "inline" or "workflow" (from §Mode Gate; re-resolved on resume)
-
runs — { "plan": null, "eval": null }; each records { "runId": "<wf_...>" } after a segment launch. Audit + same-session iteration only — cross-session resume re-RUNS segments (see §Session Recovery step 2.5).
-
model_config — deep mode only; omit or null for quick
-
user_lang — detected user language
-
has_git — boolean
-
phase — "setup" initially; transitions per the Phase labels listed under §Standard Status Format
-
qa_round — 1 initially; incremented per Phase 1 round
-
branch — "harness/spec-<slug>" if has_git, else null
-
docs_path — "docs/harness/<slug>/"
-
created_at — ISO8601 timestamp
-
cli_flags.reference (NEW in 8.4) — null if --reference not provided, or the normalized repo-relative path (relative to repo_path; no leading / or drive letter). Must be a value that has passed Path Validator (kind=file_reference) — see step 4.5.
-
conventions (NEW in 8.4) — null (Step 1.5 not yet executed), "skipped" (user explicitly chose Skip), or "file:.harness/conventions.md" (literal sentinel). . See §Step 1.5 conventions field contract for the canonical allowed-value list.
-
critic (NEW in 8.4) — null (Phase 2c-D not reached) or object:
{
"applied": "pending" | "revised" | "approved",
"round": 0 | 1, // bounded at 1 by oscillation invariant (Phase 2d-D 2nd Gate offers only Approve/Stop)
"last_findings_path": ".harness/spec/critic_findings.md" | null,
"failure_reason": null // genuine 0-issue or user approval
| "dispatch_failed" // a Workflow segment errored/crashed (Eval auto-approve banner, OR Plan-segment fallback-to-quick — Critic never ran), approved without quality review
| "re_synthesis_failed" // re-synthesis segment failed after retries (terminal)
}
state.critic.applied = "approved" is the single-source-of-truth signal that Phase 2-D Plan-segment step 7 reads to bypass Critic. failure_reason is observational (consumed by HARD-GATE display), not a control signal.
The pre-reframe parse_failed_approved / parse_failed_halted values are REMOVED — the Eval segment returns a schema-validated CriticReport (counts cannot be unparseable). Legacy state.json files carrying those values resume per the M14 pre-8.4 policy (Restart recommended).
(NEW in 8.4) Atomicity Contract — state.json single-write rule:
- Every state.json update MUST be a single read-modify-write operation. When a logical transition needs to change multiple fields together (e.g.
phase + critic.applied, or phase + qa_round), implementations MUST merge all field changes into one in-memory dict and emit ONE Write tool call covering the entire updated state.
- Sequential
Set X. Set Y. prose in this skill is shorthand for "X and Y are part of the same atomic update," NOT a directive to perform two separate writes. The complete enumeration of multi-field single-write transitions (workflow-reframe revision — the two parse_failed_* writes are deleted with their failure modes):
- Phase 2-D Plan-segment step 5 (empty-input contract):
state.critic = { applied, round, last_findings_path, failure_reason } — 4 fields in one write.
- Phase 2c-D step 2 ("Workflow segment errored"):
failure_reason + applied + round + last_findings_path + phase — 5 fields in one write.
- Phase 2c-D step 4 + step 5 branch:
last_findings_path (set in step 4) merged with the step 5 branch transition (applied + round + failure_reason + phase per the chosen branch) — single write per branch ("Auto-revise" / "Modify" / "Approve as-is" / count-zero auto-approve). These writes MATERIALIZE state.critic on the findings-exist path, so every branch emits the complete schema object (no absent fields).
- Phase 2d-D step 2 + step 3 (merged):
critic.round increment + phase → "re_critic_active" + critic.applied = "revised" — 3 fields in one write.
- Phase 2d-D step 4 (2nd-round branching):
applied + phase (and failure_reason if Stop) — single write per branch.
- HARD GATE Modify reset:
state.critic = null + phase → "qa_complete" — 2 fields in one write before re-running Phase 2.
- The
(m5) Reachability note above describes a legacy non-atomic-write race that the Session Recovery critic_active-branch handles. Under the single-write contract, this race cannot occur in any new code path — Recovery branches that handle the legacy case (critic_active + applied="approved") remain in place for backward compatibility with pre-fix sessions, but new transitions added in 8.4+ MUST follow the atomic-write rule and do not require their own dedicated mid-write Recovery branches.
-
Print setup summary (in user_lang):
[harness] Spec started!
Branch : harness/spec-<slug> ← omit if has_git == false
Mode : <quick | deep>
Path : <inline | workflow> (<reason per §Path Transparency>)
Model : <preset name> ← omit if quick mode
Output : docs/harness/<slug>/spec.md
Step 1.5: Convention Scan (NEW in 8.4)
This step runs after Setup and before Phase 1 Q&A. It populates state.conventions for downstream analyst injection.
conventions field contract (mirrors workflow) — (s3) canonical source for allowed values of state.conventions; the state.json schema doc above ("§Step 1 step 7 schema doc — conventions field" — section anchor, not line number, to survive future edits) and skills/harness/SKILL.md (§Conventions injection rule) both cross-reference this section. (M16) SYNC-WITH markers (revised after M10 consolidation removed the per-mode Conventions injection: sub-bullets): when changing allowed values, edit this section FIRST, then update (a) the state.json schema doc above and (b) skills/harness/SKILL.md §Conventions injection rule enum — both locations carry a <!-- SYNC-WITH: skills/spec/SKILL.md §Step 1.5 conventions field contract --> HTML comment so scripts/verify_sync_markers.py can grep the marker and verify every marked site declares the same enum (run manually; pre-commit/CI wiring is a later-phase TODO). (Prior to 8.4 M10, a third location existed in workflow Step 2 mode-specific dispatch sub-bullets, but those Conventions injection: blocks were consolidated into the single §Conventions injection rule declaration covered by (b); no third sync site remains.) The current allowed value set is:
null → Step 1.5 not yet executed
"skipped" → user explicitly chose to skip
"file:.harness/conventions.md" → conventions copied locally; analysts inject via {conventions} variable. The "file:" prefix is a literal sentinel (NOT a URI scheme), and the orchestrator always emits the exact string "file:.harness/conventions.md" — no platform-specific path with embedded colons (e.g. Windows C:\...) is ever assigned to state.conventions, so prefix detection via startswith("file:") cannot collide with absolute paths.
Shared file ownership note (NEW in 8.4 hardening, (M13) policy clarified): .harness/conventions.md is shared between /spec and /harness skills (both write and read it via the same path). To prevent contract drift the policy is /spec-precedence, not "last writer wins" (the latter is implementation-impossible without timestamps and was a documentation bug):
- Writer authority:
/spec is the canonical writer when both skills run in sequence — Phase 3 step 3 explicitly copies .harness/conventions.md to docs/harness/<slug>/conventions.md before cleanup, so the durable snapshot is always the /spec-version. /harness Step 1.5 prefers docs/harness/<slug>/conventions.md over re-running its own scan when the file exists (mtime not consulted — file existence is the sole signal). Standalone /harness runs (no preceding /spec for the same slug) write .harness/conventions.md themselves and produce a /harness-only snapshot at docs/harness/<slug>/conventions.md later if that file did not exist; subsequent /spec runs against the same slug will overwrite it via Phase 3 step 3 (intentional — /spec output is canonical).
- Lifetime: the file's contents persist across
/spec → /harness handoff via the Phase 3 step 3 copy to docs/harness/<slug>/conventions.md (executed BEFORE cleanup). The original .harness/conventions.md itself is then deleted with the rest of .harness/ in Phase 3 step 4 — it is NOT a long-lived file. The durable snapshot lives at docs/harness/<slug>/conventions.md and is what /harness Step 1.5 reads during handoff.
- Schema sync: if either skill changes the
conventions field's allowed values (null / "skipped" / "file:..." literal), both skills must be updated together — otherwise the receiving skill silently accepts an unknown form.
Order of evaluation:
-
--reference priority: If cli_flags.reference is non-null and the file exists, copy its content to .harness/conventions.md, set state.conventions → "file:.harness/conventions.md", and skip the rest of Step 1.5. (NEW in 8.4 v2 hardening) Resume re-validation: when this branch runs from a Session Recovery resume (not the original session run), re-execute the full Step 4.5 validation chain on cli_flags.reference before copying — validate_path(kind=file_reference), extension whitelist, leaf symlink, intermediate symlink/junction, size limit. Reasoning: a stale session's cli_flags.reference may now point to a file that has been modified, replaced with a symlink, or grown beyond the size cap; the original validation occurred at session-creation time and is not authoritative on resume. Halt-on-fail behavior identical to step 4.5. (Closes the gap where a long-paused session resumed against a now-tampered reference file silently injects altered convention content.)
-
has_git == true branch (mirrors /harness Step 1.5 logic — must be kept in sync if /harness Step 1.5 changes; this is convention duplication, not code reuse):
- CLAUDE.md >= 50 lines AND <= 5000 lines AND <= 200 KB → copy to
.harness/conventions.md, set conventions accordingly. (NEW in 8.4 v2 hardening) size cap (5000 lines / 200 KB) mirrors the has_git == false branch and --reference flag step 4.5 limit, preventing token-explosion when CLAUDE.md is auto-generated, dump-style, or otherwise oversized for analyst injection. If CLAUDE.md exceeds the cap, treat as sparse/missing and follow the Scan/Skip flow below.
- CLAUDE.md sparse/missing/oversized → AskUserQuestion:
Scan / Skip. If Scan, dispatch convention scanner sub-agent using template {CLAUDE_PLUGIN_ROOT}/templates/planner/convention_scanner.md (model: model_config.advisor or default). Verify file exists; on retry × 2 failure, fall back to "skipped".
-
has_git == false branch (NEW spec-only logic):
- Search the following 7 explicit paths, case-insensitive on filename only (do NOT search recursively into subdirectories — only the exact paths listed below):
cwd/STYLE_GUIDE.md
cwd/CONTRIBUTING.md
cwd/conventions.md
cwd/guidelines.md
cwd/policy.md
cwd/docs/style-guide.md
cwd/docs/conventions.md
- Symlink policy (applied FIRST): if
Path(p).is_symlink() is true, reject the candidate (skip with warn). This blocks symlink-based escape attempts before any further checks.
- Intermediate-directory symlink / Windows junction check (NEW in 8.4 v2 hardening — mirrors
--reference step 4.5 protection #3): even if the leaf is not a symlink, an intermediate directory in p (e.g., cwd/docs/ itself being a symlink/junction) can still escape. Compare Path(p).resolve() != Path(p).absolute() — if they differ, an intermediate symlink resolved away from the literal path; reject with warn [harness] ⚠ <p> has intermediate symlink/junction; rejecting. On Windows, is_symlink() returns False for NTFS junction points but .resolve() still follows them, so this resolve() != absolute() comparison catches junction-based escape attempts that the leaf-only check misses.
- CWD containment check (NEW in 8.4 hardening): even after the leaf symlink check + intermediate symlink check, the candidate's resolved real path MUST satisfy
Path(p).resolve() ⊆ Path.cwd().resolve() so that an intermediate directory symlink (e.g. cwd or cwd/docs/ itself being a symlink to an external location) cannot be used to escape the current project. If resolve() shows a path outside cwd.resolve(), reject with warn [harness] ⚠ <p> resolves outside cwd; rejecting. This mirrors the --reference flag's validate_path(kind=file_reference) containment guarantee for the auto-detect branch (which has no repo_path to compare against — falls back to cwd).
- Excluded directories (applied to non-symlink resolved paths): reject any candidate whose resolved real path contains one of
node_modules/, .git/, vendor/, dist/, build/, .next/, __pycache__/, .venv/, target/ as any path segment. (m3: explicit two-phase order — symlink check first, then containment check, then segment check on the verified-non-symlink path; prevents redundant double-handling of the same edge case where a symlink points into an excluded dir.)
- Filter to files with >= 50 lines AND <= 5000 lines AND <= 200 KB (size cap mirrors
--reference).
- 0 matches → set
conventions → "skipped".
- 1 match → copy content to
.harness/conventions.md, set conventions accordingly.
- 2+ matches → rank matches by descending line count (more substantive files first); ties broken by the path priority order listed above (STYLE_GUIDE.md > CONTRIBUTING.md > conventions.md > guidelines.md > policy.md > docs/style-guide.md > docs/conventions.md). Present the top 3 ranked matches as AskUserQuestion options + 1 "Skip" option. On selection, copy chosen file. On Skip, set
conventions → "skipped".
Update phase:
- At entry:
phase → "convention_scan_active".
- At exit (after one of the three branches resolves):
phase → "qa_active" before proceeding to Phase 1. This makes the convention_scan_active → qa_active transition explicit so Session Recovery can route mid-Step-1.5 crashes back to Step 1.5 (state stays convention_scan_active) rather than skipping to Phase 1.
Phase 1 — Requirements Discovery (Multi-round Q&A)
State init (idempotent — Session Recovery safe): phase is already set to "qa_active" by Step 1.5 exit (see "Update phase" block above) — do NOT re-set here, the duplicate write was a 2026-05-06 review fix. For qa_round: set to 1 ONLY on fresh entry (i.e., when qa_round is missing/null in state.json). On Session Recovery resume into qa_active, qa_round MUST be preserved at its current value so multi-round Q&A progress (Round 2 or Round 3) is not lost. Pseudocode: if state.qa_round is None: state.qa_round = 1.
Round 1: Initial Questions
-
Parse the task description for vague or missing requirements. Look for:
- Undefined actors (who are the users?)
- Unclear scope boundaries (what is explicitly in vs. out?)
- Ambiguous success conditions (how do we know it works?)
- Missing technical constraints (performance, scale, platform, integrations)
- Unstated assumptions (what is taken for granted?)
-
Generate up to 5 questions. Prioritize the most impactful ambiguities. Each question must:
- Focus on a single specific concern
- Explain briefly why it matters for the spec
- Be answerable without deep technical knowledge
-
Ask the user using AskUserQuestion (in user_lang):
header: "Q&A Round 1/{max_rounds}"
question: "To write an accurate spec, I need to clarify a few things about: {task}\n\n{numbered question list}"
options:
- label: "Answer inline" / description: "I'll type my answers below each question"
- label: "Skip — use best judgment" / description: "Proceed without answers; mark unknowns as unconfirmed"
If user selects "Skip — use best judgment": mark all questions as [unconfirmed] and proceed directly to Phase 2.
-
Collect answers. For each question, if the user responds with "don't know", "unsure", "no opinion", or equivalent → mark that item as [unconfirmed].
-
Build qa_discovery_notes — a numbered list of Q&A pairs:
1. Q: {question}
A: {answer} ← or "[unconfirmed]" if unknown
2. Q: ...
A: ...
Store qa_discovery_notes in .harness/spec/qa_notes.md.
Rounds 2–3: Follow-up (conditional)
After Round 1 answers are collected, evaluate whether new ambiguities emerged from the answers:
-
If new significant ambiguities exist AND qa_round < 3:
Update state.json: qa_round → incremented value.
Generate up to 3 follow-up questions targeting the new ambiguities only.
Ask the user using AskUserQuestion (in user_lang):
header: "Q&A Round {qa_round}/3"
question: "Your answers raised a few follow-up questions:\n\n{numbered follow-up question list}"
options:
- label: "Answer inline" / description: "I'll type my answers below each question"
- label: "No more questions — write the spec" / description: "Stop Q&A and generate the spec now"
If user selects "No more questions": stop Q&A immediately, proceed to Phase 2.
Append new Q&A pairs to qa_discovery_notes and update .harness/spec/qa_notes.md.
-
If no significant new ambiguities OR qa_round == 3:
Stop Q&A. Update state.json: phase → "qa_complete".
Inform user (in user_lang): [harness] Requirements discovery complete. Generating spec...
Maximum 3 rounds total. Never ask more than 3 rounds regardless of remaining ambiguity.
Phase 2 — Spec Generation
Update state.json: phase → "gen_ready".
Read mode from state.json and branch accordingly.
If mode == "quick": Phase 2-Q
Write spec.md directly to docs/harness/<slug>/spec.md using the following format. Use qa_discovery_notes from .harness/spec/qa_notes.md as the primary source of truth.
All section headings and content must be written in user_lang.
Required spec format (translate all headings and content to user_lang):
## Goal
<One paragraph: what this product/feature achieves and for whom>
## Background & Decisions
<Context, motivation, and confirmed decisions from Q&A>
## Scope
<Bulleted list of in-scope features and behaviors>
## Out of Scope
<Bulleted list of explicitly excluded items>
## Edge Cases
<Bulleted list of edge cases to handle>
## Acceptance Criteria
<Given/When/Then format — one scenario per criterion>
## Risks
<Bulleted list: risk description — likelihood — mitigation>
Mark any item derived from an [unconfirmed] Q&A answer with [unconfirmed] (translated to user_lang) so the user can identify open decisions.
Update state.json: phase → "spec_ready".
Print status in the standard format, prefixed with [harness] Spec draft ready.
If mode == "deep": Phase 2-D (WORKFLOW path)
Deep mode exists ONLY on the workflow path (§Mode Gate). The engine's parallel() fan-out replaces the old hand-rolled 4-sub-agent dispatch, the per-analyst retry × 2 loops, the 1-line parsing, and the — no findings — sentinel scan. All human gates (Critic Gate, 2nd Critic Gate, Spec Approval HARD GATE) are rendered by THIS orchestrator BETWEEN segment runs — never inside a script.
Graceful fallback (applies to every segment invocation below): if a Workflow call errors:
- Plan segment failed → print the §Mode Gate fallback notice, set
path_resolved → "inline", mode → "quick", and produce spec.md via Phase 2-Q (quick synthesis). Set state.critic = { "applied": "approved", "round": 0, "last_findings_path": null, "failure_reason": "dispatch_failed" } (single atomic write) and surface the Critic-skipped banner at the Final HARD GATE (the quick path has no Critic stage).
- Eval segment failed → handled locally by Phase 2c-D step 2 ("Workflow segment errored") — auto-approve with a prominent banner; do NOT downgrade the rest of the session.
Phase 2-D: Plan segment (4 analysts + synthesis — spec.plan.workflow.js)
-
Resolve conventions content based on state.conventions — the literal-sentinel SSOT guard runs HERE, in the trusted orchestrator; the segment script receives already-resolved CONTENT, never a path (the arbitrary-file-read defense never enters the autonomous span):
- (M16-2 exact-match guard): per §Step 1.5 conventions field contract, the orchestrator only ever emits the literal sentinel
"file:.harness/conventions.md" — no other "file:..." value is legitimate. Before reading, verify exact-match: if state.conventions == "file:.harness/conventions.md" exactly, proceed to read .harness/conventions.md. If state.conventions starts with "file:" but is NOT exactly that literal (e.g., manually edited state.json containing "file:../../etc/passwd" or "file:/etc/passwd"), halt with error [harness] ✗ Phase 2-D state.conventions value violates literal-sentinel contract: <value>. Allowed value: "file:.harness/conventions.md" exactly. State.json may have been manually edited or corrupted; review and Restart. Do NOT use prefix-strip + arbitrary-path read — the SSOT contract says only one literal value is legitimate, so any other value indicates state corruption and is a potential arbitrary-file-read attack vector via state.json.
"file:.harness/conventions.md" (exact match per above) → read .harness/conventions.md, pass the content as args.conventions.
"skipped" → pass empty string as args.conventions, and emit warn [harness] ⚠ Conventions explicitly skipped — analysts will treat as greenfield. for analyst awareness.
null → halt with error [harness] ✗ Phase 2-D reached but state.conventions is null. Step 1.5 must have set this to "file:..." or "skipped" before Phase 2 entry. State machine integrity violated; cannot proceed. null indicates a state machine bug (Step 1.5 was skipped or crashed before completing); do NOT silently degrade to empty-string injection — surface the bug to the user instead.
-
Read qa_discovery_notes from .harness/spec/qa_notes.md (missing → empty string).
-
Run the Plan segment via the Workflow tool (pass args as a JSON object — the script defensively parses; the field set below is the 1:1 contract with the script's // contract comment — a field missing on either side silently renders as ''):
Workflow {
scriptPath: "${CLAUDE_PLUGIN_ROOT}/workflows/spec.plan.workflow.js",
args: {
task: <task>, userLang: <user_lang>,
conventions: <resolved content from step 1, or "">,
qaNotes: <qa_discovery_notes content, or "">,
criticFindings: "", ← non-empty only on the Phase 2d-D re-entry
modRequest: "", ← non-empty only on Modify re-runs (HARD GATE Modify / Critic Gate Modify)
reSynthesisOnly: false, priorProposals: null,
models: { advisor: <model_config.advisor or null> }
}
}
Record runs.plan → { "runId": "<id>" }, updated_at → now.
- Model note: all five segment agents (4 analysts + synthesis) use
models.advisor — uniform for (a) cost predictability, (b) parallel anchoring prevention (mixed models could introduce per-model bias differences that contaminate the synthesis), and (c) implementation simplicity. Per-analyst differential assignment remains an N3-class roadmap gap.
-
The segment returns { plan: PlanResult, proposals: AnalysisResult[], stats } — schema-validated; NO 1-line parsing, NO analysis-file re-reads, NO per-analyst retry handling (the engine validates each return; a fully-failed fan-out throws → Plan-segment fallback above). Print (in user_lang): ✓ Plan segment: {stats.proposalsSucceeded}/{stats.proposalsRequested} analyses → synthesis
-
Persist re-synthesis/resume sources + empty-input contract:
a. Write the returned proposals array verbatim to .harness/spec/proposals.json. This file is the sanctioned priorProposals source for the Phase 2d-D re-synthesis re-entry AND for Session Recovery into re_synthesis_active (segment returns are in-context only — anything a later step or a resumed session needs MUST live on disk or in state.json).
b. Empty-input contract (structured): if stats.proposalsSucceeded >= 1 AND proposals.every(p => p.hasFindings === false), set state.critic = { "applied": "approved", "round": 0, "last_findings_path": null, "failure_reason": null } (4 fields, single atomic write per §Atomicity Contract) — Critic is bypassed for a genuine greenfield/input-ambiguous run. The old — no findings — substring scan, its (m1) suffix semantics, and the 1-line extraction rule are RETIRED; hasFindings: false is the structured equivalent (both old suffix forms collapse onto it — acceptable, the 8.4 contract skipped Critic for both).
c. If some analysts failed (stats.proposalsSucceeded < stats.proposalsRequested), warn (in user_lang): [harness] ⚠ {N} analyst(s) unavailable — synthesis proceeded from the remaining analyses. Partial failure NEVER sets the empty-input bypass — Critic still runs so the user gets a quality check over the degraded synthesis.
Single-source-of-truth contract: state.critic.applied is the only authoritative signal for whether Critic ran. Step 5b sets it eagerly; step 7 reads it as the sole gate to bypass Phase 2c-D. Do NOT add parallel decision logic in any other phase that diverges from this signal.
-
Render spec.md from plan (PlanResult) to docs/harness/<slug>/spec.md — the seven canonical sections (§Spec Output Format), all headings and content in user_lang:
## Goal ← goal ; ## Background & Decisions ← background
## Scope ← scope.inScope bullets ; ## Out of Scope ← scope.outOfScope bullets
## Edge Cases ← edgeCases[] bullets
## Acceptance Criteria ← acceptanceCriteria[] — render each text as its Scenario/Given/When/Then block, prefixed by the English-raw id
## Risks ← risks[] as - {risk} — Likelihood: {likelihood} — Mitigation: {mitigation} (append (source) when present)
Verify the file exists after writing. (The synthesis sub-agent does NOT write spec.md — the old "verify spec.md exists, retry × 2" dispatch loop is retired; rendering is orchestrator-owned and deterministic.)
-
Branch on state.critic.applied:
"approved" (set eagerly by step 5b) → Critic phase is bypassed by the single-source-of-truth signal. Skip Phase 2c-D / 2d-D and proceed directly to the Final HARD GATE.
- Otherwise → proceed to Phase 2c-D (Eval segment).
Phase 2c-D: Eval segment + Critic Gate (spec.eval.workflow.js)
phase → "critic_active".
-
Run the Eval segment via the Workflow tool (field set = 1:1 contract with the script's // contract comment):
Workflow {
scriptPath: "${CLAUDE_PLUGIN_ROOT}/workflows/spec.eval.workflow.js",
args: {
task: <task>, userLang: <user_lang>,
specContent: <docs/harness/<slug>/spec.md content>,
qaNotes: <qa_discovery_notes content, or "">,
criticFindingsPath: ".harness/spec/critic_findings.md",
models: { advisor: <model_config.advisor or null> }
}
}
Record runs.eval → { "runId": "<id>" }, updated_at → now.
-
Failure handling — "Workflow segment errored" (replaces the old crash/timeout retry × 2; the 1-line parse-failure path and its Parse-Fail Gate are structurally unreachable under schema returns and are REMOVED): if the Workflow call errors or returns an unusable result, set critic.applied = "approved", critic.round = 0, critic.last_findings_path = null (no findings produced), critic.failure_reason = "dispatch_failed", phase → "critic_complete" (single atomic write per §Atomicity Contract) and emit a prominent warning banner in user_lang: [harness] ⚠⚠⚠ CRITIC SEGMENT FAILED — auto-approved without quality review. Recommend manual spec review before /harness handoff. Skip to Final HARD-GATE; HARD-GATE display MUST surface failure_reason="dispatch_failed" as a visible banner (not buried in summary text) so automation pipelines do not miss the silent-quality-degradation signal.
-
The segment returns a CriticReport (schema-validated — NO regex, NO parse-fail path; counts are already normalized from items[] by the script). The Critic agent has also WRITTEN .harness/spec/critic_findings.md (user-facing artifact + re-synthesis injection source + Phase 3 handoff persistence). Verify the file exists; if missing, derive it yourself from report.items using the file's body schema (orchestrator-derived fallback — one Write, no re-dispatch).
-
Persist findings path (NEW in 8.4): set state.critic.last_findings_path = ".harness/spec/critic_findings.md" — merged into the step 5 branch write per §Atomicity Contract. This single set covers all step 5 branches (and the 2nd-round Eval re-run via Phase 2d-D step 3 — which loops back through this step). Session Recovery into critic_active with applied="pending" reads this field to re-display the Critic Gate without re-running the segment.
-
Branching on report.counts (each branch's write materializes the COMPLETE state.critic object — applied + round + last_findings_path + failure_reason — per §Atomicity Contract):
critical==0 AND major==0 → set critic.applied = "approved", critic.round = 0, critic.failure_reason = null, phase → "critic_complete". Proceed to Final HARD-GATE (Minor count and findings file shown for context).
critical>=1 OR major>=1 → present Critic Gate via AskUserQuestion (3-way). The orchestrator (this skill) constructs and translates the gate text — the Critic sub-agent does NOT generate AskUserQuestion content; gates never enter segment scripts. Translate header, question, and option labels/descriptions to {user_lang} per [Communicate in user's language] policy. Issue ID prefixes ([C*], [M*], [m*]) and severity tokens (Critical, Major, Minor) stay English (canonical identifiers).
header: "Critic" (translate to user_lang)
question: "Critic found <C_count> Critical, <M_count> Major, <m_count> Minor issues in the spec.
> Auto-revise: re-run synthesis once to address Critical/Major (Minor unchanged).
> Modify: tell me what to change manually (re-run synthesis with your input).
> Approve as-is: proceed to Final HARD-GATE with findings shown for review."
(orchestrator substitutes <C_count>/<M_count>/<m_count> with the integer values from report.counts; then translate question body to user_lang)
options: (translate label + description to user_lang)
- "Auto-revise" / "Re-run synthesis with critic findings (max 1 round)"
- "Modify" / "Provide modification instructions"
- "Approve as-is" / "Proceed without revision"
- Auto-revise selected → set
critic.applied = "pending", critic.round = 0, critic.failure_reason = null, proceed to Phase 2d-D.
- Modify selected → collect modification instructions, set
critic.applied = "pending", critic.round = 0, critic.failure_reason = null, proceed to Phase 2d-D (the instructions travel as args.modRequest).
- Approve as-is selected → set
critic.applied = "approved", critic.round = 0, critic.failure_reason = null, phase → "critic_complete". Proceed to Final HARD-GATE with critic_findings.md path shown.
Phase 2d-D: Re-synthesis (conditional, max 1 round normally; max 2 with 2nd gate)
This phase runs only when Critic Gate selected Auto-revise or Modify.
Phase boundary note (state machine): Phase 2d-D uses distinct phase labels (re_synthesis_active, re_critic_active) for its re-dispatched Synthesis and Critic stages, so Session Recovery can tell apart the first run (critic_active) from the re-run (re_critic_active). Do NOT reuse critic_active here.
Halt semantics (applies to all Stop branches in Phase 2c-D / 2d-D): "halt session" means: (a) print the user-facing message in {user_lang} explaining why the session halted, (b) leave .harness/state.json with the terminal phase value (critic_halted) intact, (c) leave .harness/spec/ artifacts (qa_notes.md, proposals.json, critic_findings.md) and .harness/conventions.md intact for manual review, (d) do NOT run Phase 3 cleanup, (e) exit the orchestrator process. The user can resume by editing state.json (e.g., changing phase to qa_active for a fresh start) or starting a new /spec session in the same directory.
-
Set phase → "re_synthesis_active". Re-run the Plan segment in re-synthesis re-entry form — analysts are NOT re-run (mirrors the old "re-dispatch Synthesis only" behavior; the script skips its Propose phase):
Workflow {
scriptPath: "${CLAUDE_PLUGIN_ROOT}/workflows/spec.plan.workflow.js",
args: {
task, userLang, conventions: <resolved content, as Plan-segment step 1>,
qaNotes: <qa_discovery_notes content, or "">,
criticFindings: <content of the file at state.critic.last_findings_path — state.critic is the SSOT (s1): read it only when state.critic.applied ∈ {"pending","revised"} AND last_findings_path is non-null AND the file exists; never use a bare file-existence check>,
modRequest: <the user's Modify instructions if the Critic Gate selected Modify, else "">,
reSynthesisOnly: true,
priorProposals: <parsed array from .harness/spec/proposals.json>,
models: { advisor: <model_config.advisor or null> }
}
}
Update runs.plan.runId. If .harness/spec/proposals.json is missing (e.g., .harness/ partially deleted between sessions), fall back to a FULL Plan segment run (reSynthesisOnly: false, criticFindings still injected) and warn (in user_lang) that the analysts re-ran. Retry policy: if the segment errors, retry × 2; on third failure, halt with phase → "critic_halted" + failure_reason = "re_synthesis_failed" (analogous to dispatch_failed but for the re-synthesis branch).
Modify-channel injection defense (C4): the Modify text travels as args.modRequest — the segment script appends it AFTER placeholder rendering, inside the sentinel block (## User Modification Request + meta-guard preamble + fenced text code block), so user text can neither hijack {placeholders} nor act as directives. The sentinel pattern itself now lives in spec.plan.workflow.js (and the templates' Input Trust Model declares how recipients must treat the block).
-
Re-render spec.md from the returned plan (same mapping as Plan-segment step 6) and overwrite .harness/spec/proposals.json with the returned proposals (unchanged on a re-synthesis re-entry, but keeps the file authoritative). Prepare the round increment (critic.round: 0 → 1) but do NOT write yet — the write is merged with step 3's transitions per the §Atomicity Contract single-write rule. The increment lands logically here (between re-synthesis and re-critic) so that step 3's Eval re-run and step 4's critic.round == 1 2nd-Gate branch both observe the post-increment value. Do NOT increment elsewhere — critic.round is bounded at 1 by design (oscillation prevention); step 4's 2nd Critic Gate offers only Approve/Stop, never another revision round.
-
Single atomic write (per §Atomicity Contract): in one read-modify-write of state.json, set all three: phase → "re_critic_active", critic.applied = "revised", critic.round: 0 → 1 (the increment from step 2). After the atomic write completes, re-run the Phase 2c-D Eval segment on the revised spec.md (same args shape; update runs.eval.runId). (Distinct phase from critic_active so Session Recovery can route a 2d-D re-entry crash back to step 3, not step 1 of Phase 2c-D. Single atomic write closes the prior step-2/step-3 race where a crash between the round increment and the phase write left phase=re_synthesis_active + round=1 + applied=pending mixed state.) Session Recovery re_synthesis_active branch reads critic.round directly and preserves whatever value is there — never resets to 0 — so even if a crash occurs before the atomic write completes, recovery is correct (round stays 0 from step 1; re-entering step 1 idempotently re-runs the re-synthesis segment).
-
2nd-round branching:
Critical=0 AND Major=0 → set critic.applied = "approved", phase → "critic_complete". Proceed to Final HARD-GATE.
Critical>=1 OR Major>=1 AND critic.round == 1 → present 2nd Critic Gate (only Approve / Stop offered — no Re-revise to prevent oscillation). Translate option labels/descriptions to {user_lang} per [Communicate in user's language] policy.
options: (translate label + description to user_lang)
- "Approve as-is" / "Re-revision still has issues. Proceed to Final HARD-GATE for manual review."
- "Stop" / "Halt — manual intervention needed."
- Approve →
critic.applied = "approved", critic.failure_reason = null, phase → "critic_complete" (round stays 1), Final HARD-GATE.
- Stop → set
phase → "critic_halted", halt session, leave state.json intact. Session Recovery surfaces this state as "Critic flow halted — manual intervention required."
HARD GATE — Spec Approval
Update state.json: `phase → "spec_ready"`. (Both quick mode and deep mode converge here; deep mode arrives in `critic_complete` and this transition normalizes the phase before the user sees the spec.)
Show spec.md to the user and ask for explicit confirmation using AskUserQuestion (in user_lang):
header: "Spec"
question: "Review the spec above. Approve, request modifications, or stop."
options:
- label: "Approve" / description: "Accept this spec and proceed to handoff"
- label: "Modify" / description: "Describe changes — spec will be regenerated"
- label: "Stop" / description: "Halt and discard the spec"
If user selects "Modify" or provides modification details via "Other":
- Collect the modification request.
- (NEW in 8.4 v2 hardening — round-2 review-fix) Reset Phase 2 state in a single atomic write before re-running:
state.critic = null (clears applied, round, last_findings_path, failure_reason together), phase → "qa_complete" (the Phase 2 entry point). This prevents the stale-critic.applied=approved signal from the prior Phase 2c-D from short-circuiting the re-run via Plan-segment step 7's SSOT bypass — without this reset, a Modify selection would re-enter Phase 2 but the Critic stage would auto-bypass since the SSOT signal still says "already approved". Also prevents the phase=spec_ready + critic.applied=pending mid-re-run crash race that Session Recovery's spec_ready → HARD GATE jump could not detect.
- Re-run Phase 2 (same mode) incorporating the changes. The
qa_discovery_notes remain unchanged. The modification request MUST reach every Phase 2 prompt that incorporates user-provided text via the C4 sentinel:
- WORKFLOW path (deep): pass it as
args.modRequest on the Plan segment run — the script appends the sentinel block (## User Modification Request + meta-guard preamble + fenced text code block) to every analyst and synthesis prompt AFTER placeholder rendering.
- INLINE path (quick): there are no sub-agents — the orchestrator incorporates the request directly while rewriting spec.md (treat the text as content guidance, never as instructions that change the seven-section format).
This closes the Modify-channel prompt-injection surface symmetrically with Re-synthesis — both Modify entry points (HARD GATE Modify + Critic Gate Modify) use the same
args.modRequest channel. (C4)
- Re-present this HARD GATE with the updated spec.
If user selects "Stop": halt the workflow, clean up .harness/.
Only "Approve" advances to Phase 3.
Phase 3 — Handoff
Update state.json: phase → "completed".
-
Inform the user (in user_lang) that the spec is finalized:
[harness] Spec complete!
Output : docs/harness/<slug>/spec.md
-
Smart Routing suggestion — suggest next step using AskUserQuestion (in user_lang):
header: "Next Step"
question: "Your spec is ready. Would you like to start implementation?"
options:
- label: "Start /harness" / description: "Launch implementation workflow using this spec"
- label: "Done" / description: "Keep the spec for later use"
If user selects "Start /harness": first run step 3 (persist artifacts) and step 4 (cleanup), THEN invoke /harness --output-dir docs/harness/<slug>/ "Implement based on {docs_path}spec.md" (translate the quoted string to user_lang). This ordering is critical (C1): persisting artifacts BEFORE invoke ensures /harness Step 1.5 / Step 2 can actually read qa_notes.md / critic_findings.md / conventions.md from {docs_path} — invoking first would short-circuit step 3 and make the persistence contract a no-op. The explicit {docs_path}spec.md form (vs bare spec.md) documents the path-assembly contract at the call site so future maintainers don't mistake the bare filename for a working-dir lookup.
The --output-dir argument is load-bearing for slug-safe handoff: without it, /harness re-slugifies its own task description and writes to a different docs/harness/<re-slug>/ directory — silently bypassing the artifacts persisted in step 3 below. Always pass --output-dir docs/harness/<slug>/ so /harness's docs_path matches /spec's docs_path exactly.
If user selects "Done": first run step 3 (persist artifacts) and step 4 (cleanup), then halt. Persisting artifacts on the "Done" path preserves qa_notes.md / critic_findings.md / conventions.md so a future /harness session invoked manually via --output-dir docs/harness/<slug>/ can still consume them.
-
Persist spec artifacts to {docs_path} (NEW in 8.4) — BEFORE cleanup:
(s1) Path Safety Guard for {docs_path} — before any file operation on {docs_path}, validate (mirroring /harness Step 8 Artifact Cleanup Safety Guard):
- Extract
<slug> via explicit rstrip then split: slug = docs_path.rstrip("/").split("/")[-1] (NEW in 8.4: explicit form so a trailing slash on docs_path does not produce an empty slug and false-abort the cleanup; equivalent to "last path segment before trailing /" but unambiguous in implementation). It must be non-empty/non-whitespace, must NOT be memory (reserved name), must NOT contain .. or / within itself, and must NOT be ..
Path(docs_path).resolve() ⊆ Path.cwd() (symlink escape prevention; no has_git condition).
- On any check failure: ABORT Phase 3 — do NOT delete
.harness/, do NOT run cleanup. Print the failed check. The .harness/spec/ artifacts remain intact for manual recovery.
After validation passes, ensure {docs_path} exists (Path(docs_path).mkdir(parents=True, exist_ok=True) or equivalent — idempotent; Session Recovery paths may have bypassed Setup step 3). Then copy the following files from .harness/ into docs/harness/<slug>/ (only when the source file exists; skip silently if missing):
.harness/spec/qa_notes.md → docs/harness/<slug>/qa_notes.md
.harness/spec/critic_findings.md → docs/harness/<slug>/critic_findings.md
.harness/conventions.md → docs/harness/<slug>/conventions.md
These artifacts are consumed by /harness Step 1.5 (conventions reuse — skip rescan if docs/harness/<slug>/conventions.md exists) and /harness Step 2 (planner dispatch fills {qa_discovery_notes} from qa_notes.md and {critic_findings} from critic_findings.md). Without persistence, /harness falls back to its own Convention Scan and dispatches planners with empty Discovery Notes — producing less context-aware plans. Note: .harness/conventions.md is the shared file documented under the Step 1.5 ownership note; copying to docs/harness/<slug>/ makes the snapshot durable across the spec→workflow handoff regardless of any subsequent /harness Convention Scan run.
(M17) Variable ↔ filename mapping (NEW in 8.4 hardening) — the persisted file names use snake_case short forms while template variable placeholders use longer descriptive names. This intentional asymmetry separates "what is on disk" from "how it appears in prose":
Persisted filename (docs/harness/<slug>/) | Template variable placeholder | Site of consumption |
|---|
qa_notes.md | {qa_discovery_notes} | analyst templates (spec) + 4 planner templates (workflow) |
critic_findings.md | {critic_findings} | synthesis.md (spec re-synthesis) + 4 planner templates (workflow) |
conventions.md | {conventions} | analyst templates + 4 planner templates |
spec.md | {spec_content} (critic.md) / {spec_path} (planner_single.md) | critic.md (spec) + planner_single.md (workflow single mode) |
Do NOT rename either side without updating both: renaming the file (e.g., qa_notes.md → qa_discovery_notes.md) without updating Phase 3 step 3 here AND /harness Step 1.5 / Step 2 read sites silently breaks the handoff (file-missing → empty-string fallback → planners receive empty Discovery Notes). Renaming the variable requires updating all template files that reference it — see grep qa_discovery_notes and qa_notes for the full surface.
-
Cleanup: Delete .harness/ directory (state.json, spec/, conventions.md, and the directory itself). The final docs/harness/<slug>/spec.md AND the artifacts persisted in step 3 are preserved. (Halted sessions do NOT reach this step — see Halt semantics in Phase 2d-D.)
-
If has_git == false: Do not attempt any git operations.
Status Check (anytime)
If user asks for status, print status in the standard format defined above.
Spec Output Format
The spec written to docs/harness/<slug>/spec.md must use this exact structure for /harness compatibility. Write all content in user_lang.
## Goal
...
## Background & Decisions
...
## Scope
...
## Out of Scope
...
## Edge Cases
...
## Acceptance Criteria
- **Scenario: {name}**
- Given: {precondition}
- When: {action}
- Then: {expected result}
## Risks
- {risk} — Likelihood: {low/med/high} — Mitigation: {approach}
All headings and content must be translated to user_lang. The English labels above are canonical identifiers for /harness compatibility.
Section Mapping to /harness
| /spec section | /harness usage |
|---|
| Goal | Goal |
| Background & Decisions | Background |
| Scope | Scope (in scope) |
| Out of Scope | Scope (out of scope) |
| Acceptance Criteria | Completion Criteria |
| Risks | Risks |
| Edge Cases | Testing Strategy |
Model Selection
Sub-agents are used only in deep mode (WORKFLOW path — the segment scripts spawn them).
Preset table + rules: see templates/_shared/model_config.md.
Role-map: Requirements Analyst / User Scenario Analyst / Risk Auditor / Tech Constraint Analyst (and the Synthesis + Critic phases) → advisor.
Applying model config: pass the resolved advisor model once per segment run as args.models ({ advisor: <model or null> }; null = inherit parent model, i.e. the default preset) — the segment scripts apply it per agent. Sub-agents must NOT directly access state.json to read model_config — the orchestrator passes the resolved value at segment launch.
User Interaction Rules
See templates/_shared/askuserquestion.md.
Key Rules
- Requirements first, spec second. Never write the spec before completing at least one round of Q&A.
- Maximum 3 Q&A rounds. Stop after round 3 regardless of remaining ambiguity.
- "Don't know" is valid. Mark unresolved answers as
[unconfirmed] — do not invent answers.
- Confirmation gate is non-negotiable. The user must explicitly approve the spec before handoff. Gates live ONLY in this orchestrator — never in a segment script.
- "Modify" triggers Phase 2 re-run. Q&A notes are preserved; the spec is regenerated, not patched.
- Analyst proposals must be independent. Never share one analyst's findings with another during parallel analysis (the Plan segment's
parallel() enforces this).
- Section mapping must be preserved. The seven spec sections must appear in every spec for /harness compatibility.
- User language. All user-facing output must be in
user_lang. Re-detect on every user message. WORKFLOW path: pass userLang in args — the segment scripts build schema descriptions from it, which forces sub-agent free-text output language; ids/enums stay English raw.
- Workflow args are a JSON object; segment scripts defensively parse (
args may arrive as a JSON string — engine behavior). Keep the SKILL args blocks and the scripts' // contract comments in 1:1 sync. Never put user-gate decisions into args.
- Graceful engine fallback. Any Workflow failure degrades per the Phase 2-D fallback rules (Plan segment → quick path; Eval segment → dispatch_failed banner) with a notice — never a hard error.
- Intermediate outputs are ephemeral. Only
spec.md and the Phase 3 persisted artifacts (qa_notes.md, critic_findings.md, conventions.md) are preserved in docs/harness/<slug>/. All other .harness/ contents (including proposals.json) are cleaned up after completion.
- skill field is "spec". state.json must always have
skill: "spec" — session recovery depends on this.