Imported from NarenKarthikBM/specseyal (
extensions/council/skills/speckit-council/SKILL.md). Install upstream withnpx skills add NarenKarthikBM/specseyal --skill speckit-council. Copyright stays with the author.
User Input
$ARGUMENTS
You MUST consider the user input before proceeding (if not empty). The only recognized flag is --reopen <delta|full>. For delta, everything after the tier word is the triggering finding, taken verbatim (e.g. --reopen delta "the migration and schema edit are still bundled after the last revision"). full takes no further argument. Anything else in $ARGUMENTS with no --reopen present is out of contract scope for this command — ignore it rather than inventing behavior commands.md doesn't define.
What this is
You are the orchestrator. /speckit-council runs in the main thread and never reviews the plan itself — it dispatches subagents (the "separate sessions" of the session-boundary rule) and holds four barriers: deck-prep → stage 1 (bench-size-many independent opinions, parallel) → stage 2 (anonymized peer review) → stage 3 (1× chairman synthesis). The bench size is resolved per feature in Pre-Execution step 4 (profile-schema.md §10 M1) — five members at the shipped default, 1–8 when a feature's profile.yaml sets council_members. This is the speckit-implement-parallel wave pattern applied to a review pipeline instead of a task DAG (plan.md Chosen Approach C / research.md R-D2).
Ceremony tier (D56). The peer-review shape and the context members load are set by council_tier, resolved in Pre-Execution step 4 alongside the bench size — the two are orthogonal (profile-schema.md §10: "every (size, tier) pair in 1–8 × {full, standard} is legal"). Under full (the default) the barriers are exactly as above — stage 2 is one parallel per-member review per bench member, members read deck+plan+spec eagerly, no output cap: 12 sessions at the shipped bench-size-5 default (bench size scales the 5 in that count 1:1), the 002 5.25M baseline measured at that default. Under standard stage 2 collapses to one consolidated peer-critique session regardless of bench size, members load context lazily (the technical deck is the sole up-front read; plan/spec/graph are consulted only on demand), and an output cap applies: 8 sessions at the shipped bench-size-5 default. At a bench size of 1, stage 2 does not run at either tier — see Pre-Execution step 4 and Stage 2, below (M4/FR-017). Everything else — anonymity (FR-006), the status-only-returns invariant (S2/SC-005), the Opus chairman, resumability, and per-session traces — is identical across tiers. Only three things branch on the tier: stage 1's dispatch appendix, stage 2's session count, and the chairman's input list. Where a stage below reads "5 peer sessions" or "read eagerly," that is the full path at the shipped default; each standard delta and each bench-size effect is called out inline as it arises.
The invariant that makes this safe (S2, SC-005): every subagent's entire return value is one status line. All review content is file-mediated — members write to opinions/, the chairman alone reads them, and you read only the final round-N/suggestions.md. You never open, grep, or otherwise inspect anything under opinions/, at any point, for any reason — existence checks are test -f / ls, never a content read. If a dispatched subagent ever returns more than its one-line status, that is a broken contract on its part: do not repeat, quote, or forward the excess anywhere (not into traces.jsonl, not into your completion report) — note the violation by role/letter only and continue.
Pre-Execution
-
Extension hooks. Check
.specify/extensions.ymlforhooks.before_councilentries, using the same rules every speckit command uses: parse if present, skip silently if absent/unparsable; drop entries withenabled: false; a hook with noconditionis executable, one with aconditionis left to the HookExecutor; dots become hyphens when building the slash command (speckit.foo→/speckit-foo); mandatory hooks (optional: false) are announced and actually invoked, waiting for completion, before continuing; optional hooks are announced only. In practice this is a no-op today — the council extension declareshooks: none(extension.yml;plan.mdChosen Approach A) and nothing else in this repo hooks intobefore_council— the check stays so a future extension can hook/speckit-councilwithout an edit here. -
Resolve the feature. Run
.specify/scripts/bash/check-prerequisites.sh --json --paths-onlyfrom the repo root; parseREPO_ROOT,BRANCH,FEATURE_DIR(absolute paths — use them absolute in every subsequent command in this session, per your own tool discipline). The feature/spec ID isbasename(FEATURE_DIR). -
Precondition:
plan.mdmust exist. If$FEATURE_DIR/plan.mdis missing, this is the contract's no-plan exit state: report the error and stop. Write nothing — nocouncil/directory, no trace line, no partial state. -
Read the council config.
$REPO_ROOT/.specify/extensions/council/council-config.yml→member_count(repo-global default; v1 ships 5),member_lenses(the five evidenced lenses —lens_resolutiongoverns what backs any position beyond them, next),models.{chairman,member,deck_prep}(opus/sonnet/sonnet, D18 — fixed regardless of bench size, M3),max_rounds(1 — informational here; the round cap is/speckit-council-triage's concern via the delta-check escalation, not something this command enforces by refusing to run).Resolve the council bench size (
council_members,profile-schema.md§10 M1). Read$FEATURE_DIR/profile.yaml'scouncil_membersif the file is present and the key parses; else fall back tocouncil-config.yml'smember_countabove — the identical two-place default/override arrangement the tier resolution below already uses.validate-profile.pyhas already hard-blocked any value outside the closed range 1–8 (M2) before this skill ever runs, so this is a lookup, not a re-validation. Call the resolved integer the bench size for the rest of this file — never write it as a bareN;round-Nalready uses that letter for the round number throughout this file, and reusing it for a second, different quantity would be exactly the kind of ambiguity this contract's own "unknown keys are a validation error, not a warning" ethos exists to prevent elsewhere. At the shipped default (nocouncil_membersinprofile.yaml,member_count: 5in the config) the bench size is 5 — precisely what v1 always resolved to before this key existed, so every rule below that reduces to a bench size of 5 reproduces today's dispatch count, lenses, and letters exactly, with no behavioral difference (M3's byte-identical intent).Assign letters and lenses (M5). Letters run
A, B, C, D, E, F, G, Hin fixed order — take as many from the front as the bench size. Zipmember_lensespositionally to those letters, in order, up to the smaller of the bench size and 5 — with the v1 defaults that'sA=correctness, B=risk, C=simplicity, D=testability, E=sequencing(research.mdR-D1); derive it from the config, don't hardcode it, since trimmingmember_count/council_membersbelow 5 is the documented M1 cost lever. Beyond a bench size of 5 (letters F, G, H), the evidenced list has nothing left to zip —council-config.yml'slens_resolution.positions_6_to_8: convene_time_from_feature_under_reviewgoverns instead, and R1-S11 forbids inventing a sixth-through-eighth lens name to fill the gap, here or anywhere in this skill. Resolve it by delegating the derivation to the member itself, at dispatch — and anchor each of the three possible extra letters in a different one of the three feature-grounded sourcescouncil-config.yml's own comment onlens_resolutionalready names, so that up to three parallel, mutually-invisible self-derivations diverge from each other structurally rather than by chance (FR-018's distinctness requirement, which parallel dispatch — "True parallelism," below — can't otherwise guarantee): F anchors inspec.md's risk areas, G inplan.md's complexity tracking / rejected alternatives, H in the deck's own claims under review. Render that letter's{{lens}}slot not as a fixed word but as this self-derivation instruction, substituted verbatim in its place, with<anchor>filled from the mapping above:"No lens is pre-assigned to you this round. Read the technical deck (and, per your tier,
plan.md/spec.md), then identify the single analytical emphasis this specific plan most needs that the five standing lenses (correctness, risk, simplicity, testability, sequencing) don't already cover. Ground your choice specifically in<anchor>— not a generic guess and not a survey of the whole deck — since that is where this round's convene-time derivation for your position is anchored. State that chosen emphasis, in one or two words, as your ownlens:frontmatter value, and use it as your emphasis for this review exactly as a pre-assigned lens would be used."This keeps the resolution exactly where the config places it — at convene time, inside this dispatch, from the feature actually under review — without you, the orchestrator, ever reading
plan.md/spec.md/the deck yourself to compute it (see "Paths, not content," below): the member does that reading, not you. Re-derive independently at every dispatch of that letter (stage 1, and stage 2 under tierfull) — the anchor stays fixed for that letter across both stages, but the chosen word need not match between a letter's stage-1 and stage-2 sessions; only the letter is the identity FR-006 protects, never the lens word riding on it.Resolve the ceremony tier (D56,
profile-schema.md§7). Read$FEATURE_DIR/profile.yaml'scouncil_tierif the file is present and the key parses; else fall back tocouncil-config.yml's top-levelcouncil_tier; if neither is set, the tier isfull(T1 — absent ⇒ the fullest review; never silently pick the cheaper tier). Load that tier's parameters fromcouncil-config.yml'stiers.<tier>block:peer_review(per_member|consolidated),context(eager|lazy), andmember_output_cap(noneor an integer). These three values are the only thing that branches stages 1–3; nothing else in this command reads the tier. Note the resolved tier + its three params for your completion report, and cross-check the model map is unchanged (a tier never alters D18 — Sonnet members, Opus chairman, both tiers). -
Note graph availability.
test -f "$REPO_ROOT/graphify-out/graph.json". This does not gate anything (FR-019 — degrade, don't block) and you don't need to relay it to subagents —member-prompt.mdanddeck-technical.mdboth already instruct their own sessions to check for the graph themselves. You only need this fact for your own completion report's "Grounding" line as a sanity cross-check against the chairman's authoritative reduced-grounding banner insuggestions.md. -
Resolve the round number (resumability, Constitution III). List
$FEATURE_DIR/council/round-*/, numerically sorted.- No rounds exist → this run is round 1.
- The highest existing
round-Nhassuggestions.md→ it is complete; never reopen it — this run is round N+1 (contract idempotency: "re-run… starts round-(N+1); never overwrites a prior round"). - The highest existing
round-Nhas nosuggestions.md→ it is an interrupted run; resume round N in place: skip deck-prep ifdefense-deck/{technical,overview}.mdalready exist from this attempt, skip stage 1 for any letter that already hasopinions/<letter>.md, skip stage 2 for any letter that already hasopinions/peer/<letter>.md, then continue at whichever stage is first incomplete. Never re-run a stage whose artifact is already on disk.
-
If
--reopenwas passed, read § Reopen now before dispatching anything — it changes what context stage 1 gets and whether deck-prep runs at all, but not the round-number resolution above.
Dispatch mechanics (applies to every stage below)
- Paths, not content. Beyond the two prompt templates (next bullet) and, only for
--reopen delta, the plan-diff text, you do not readplan.md,spec.md, the deck, or any opinion into your own context. Subagents have their ownRead/Bashaccess and read their inputs themselves from the paths you give them. - Prompt templates are rendered by you, once, before dispatch.
member-prompt.mdandchairman-prompt.mdare prompt templates, not output shapes — read$REPO_ROOT/.specify/extensions/council/templates/member-prompt.mdonce and, per letter, substitute{{member_letter}}and{{lens}}textually to get that letter's base prompt (one render per letter, reused for both stage 1 and stage 2 — only the stage-specific appendix you append differs between the two dispatches). Read.../templates/chairman-prompt.mdonce for stage 3 and render its §2 fenced block with{{mode}} = "synthesis"and the slots from its own §3 table. The rendered text becomes the literalpromptargument to theAgenttool call, with your stage-specific appendix (concrete paths, which stage) appended at the end. deck-technical.md,deck-overview.md,suggestions.mdare output shapes, not prompts — point deck-prep and the chairman at their installed template paths and let them read the guidance/placeholders themselves; you don't pre-render these.- True parallelism = one message, multiple
Agenttool calls. Stage 1's members — as many as the resolved bench size — and, under tierfull, stage 2's per-member reviewers, must each be dispatched as that manyAgenttool-use blocks in the same turn, never one at a time (five, at the shipped bench-size-5 default — unchanged). Under tierstandard, stage 2 is a singleAgentcall (the consolidated reviewer) regardless of bench size, so the one-turn-multiple-calls rule applies only to stage 1 there. At a bench size of 1, stage 2 dispatches zero calls, at either tier (M4/FR-017) — see Stage 2, below.subagent_type: general-purposeis sufficient for every council role (deck-prep, member, chairman): generic file-writing, tool-using tasks, not specialized personas. Noisolationis needed anywhere in this flow — every subagent writes its own disjoint file (opinions/<letter>.md,opinions/peer/<letter>.md,opinions/peer/consolidated.md, or the one deck/suggestions path); there is no shared-file collision to isolate against. - A barrier means what it says. Do not start stage 2 until every stage-1 return is in hand; do not dispatch the chairman until the stage-2 returns are in hand — all of them under tier
full, the single consolidated return understandard, none at all at a bench size of 1 (stage 2 doesn't run — proceed straight to the chairman with the required disclosure, Stage 3 below). If one member fails or times out, proceed with the rest, recordoutcome: "failed"in that letter's trace fragment, and let the chairman's own missing-file handling (chairman-prompt.md§ Inputs) absorb the gap — never block the whole round on one member. (Astandard-tier consolidated-peer failure leaves the chairman reading the stage-1 opinions alone — a degraded but valid round; note it rather than blocking.)
Stage 0 — Deck prep
Dispatch one Sonnet subagent. Prompt = point it at .../templates/deck-technical.md and deck-overview.md (the shapes to fill) and at $FEATURE_DIR/plan.md, spec.md, graphify-context.md, graphify-receipts.md (its sources — the receipts diet is the arm-3 D62 concept/rationale enrichment source deck-prep mines per deck-technical.md, 005). Tell it to write council/defense-deck/technical.md and council/defense-deck/overview.md (paths relative to $FEATURE_DIR), overwriting in place if they already exist (D38 — the deck is not round-scoped; git on the feature branch holds prior versions). Its return is one status line confirming the two files were written — nothing else.
Skip this stage entirely for --reopen delta (§ Reopen) — the delta tier's context is the diff + finding, not a re-rendered deck, and defense-deck/ is left untouched. Run it normally for a fresh/resumed round and for --reopen full.
Barrier: wait for the return, then append its trace fragment (§ Traces) before dispatching stage 1.
Stage 1 — Independent opinions
Dispatch the resolved bench size's worth of Sonnet subagents, in one turn — one per resolved letter (five, dispatched A–E, at the shipped default; unchanged). Each gets its rendered member-prompt.md base (letter + lens already substituted — a real lens word for a letter within the evidenced five, the self-derivation instruction from Pre-Execution step 4 for any letter beyond it) plus an appendix stating: this is Stage 1; the concrete paths for defense-deck/technical.md, plan.md, spec.md; and the write target council/round-N/opinions/<letter>.md. (For --reopen delta, the appendix instead names the plan-diff text and the triggering finding as the entire review context — § Reopen.) Each member's return is one status line — Wrote opinions/<letter>.md — <n> suggestions. — and nothing else ever crosses back to you.
Tier deltas to the Stage-1 appendix (D56). The base member-prompt.md is unchanged; the tier only tunes the appendix you append:
context: lazy(tierstandard) — add, verbatim in intent: "Lazy context: your primary and only required read is the technical deck (defense-deck/technical.md). Do NOT readplan.mdorspec.mdwholesale, and do not sweep the graph — consult them ON DEMAND only to verify a specific deck claim you actually doubt. Their paths are<plan.md>,<spec.md>; open them only if a check requires it." Undercontext: eager(tierfull) the appendix is exactly the base paragraph above — deck+plan+spec read up front.member_output_cap: <n>(tierstandard, e.g. 6) — add: "Write at most<n>suggestions; if you have more, keep the highest-signal ones and drop the rest rather than padding." Undernone(tierfull) no cap line is added.- Read-rate telemetry (tier
standardonly) — extend the required return line to carry a metadata-only suffix naming which sources the member actually opened:Wrote opinions/<letter>.md — <n> suggestions. consulted: deck[,plan][,spec][,graph]. This is process metadata, not review content — the status-only invariant (S2) still holds; it simply lets you compute the per-memberplan.mdread-rate (the D56 lazy-loading effectiveness metric) for your completion report without ever opening an opinion. Under tierfullthe return line is the plain form above.
Barrier: wait for all of them, then append their trace fragments (§ Traces), serially, before dispatching stage 2.
Stage 2 — Peer review
A bench size of 1 skips this stage entirely (M4, profile-schema.md §10; FR-017) — at either tier. One opinion has nothing to peer-review against. If the resolved bench size is 1, dispatch no stage-2 sessions — not per-member, not consolidated — regardless of peer_review's tier value, and go straight to stage 3. Nothing under opinions/peer/ is ever created this round, and no stage-2 trace fragment is appended. This is not a silent omission: the required disclosure is carried in the chairman's dispatch appendix (Stage 3, below) and lands in suggestions.md's Chairman's note, so the round's own artifact states plainly that peer review did not run and why — an absent opinions/peer/ directory must never be mistaken for a failed or forgotten stage.
For a bench size of 2 or more, Stage 2's shape is set by the tier's peer_review value (Pre-Execution step 4). Both paths barrier before stage 3, both return status-only, and both write under opinions/peer/; only the session count and file layout differ. You never open any peer file — you name paths and check existence with test -f, exactly as in stage 1.
per_member (tier full). Re-dispatch that many new Sonnet subagent sessions in one turn — fresh sessions with no memory of stage 1, not continuations; the same letter→lens assignment carries over so opinions/peer/A.md pairs with opinions/A.md (for a self-derived letter F/G/H, the anchor carries over identically — Pre-Execution step 4 — the chosen word need not). Each gets the same rendered member-prompt.md base plus an appendix stating: this is Stage 2 (per-member); its own letter's opinion is excluded; read every other resolved letter's opinion at council/round-N/opinions/<other-letter>.md for each letter besides its own (you name the paths — you do not open them yourself); write to council/round-N/opinions/peer/<letter>.md. Return is one status line, same discipline as stage 1. At the shipped bench-size-5 default this is exactly today's five-session peer stage, reading "the other four" — unchanged.
Barrier: wait for all of them, then append their trace fragments (§ Traces), serially, before dispatching stage 3.
consolidated (tier standard). Dispatch one Sonnet subagent — a single neutral peer reviewer, not a lettered member: it carries no {{lens}} and reviews every stage-1 opinion at once, so render member-prompt.md with {{member_letter}} = consolidated and {{lens}} = consolidated (or point it at the deck-less consolidated task directly). Its appendix states: this is Stage 2 (consolidated peer critique); read every stage-1 opinion at council/round-N/opinions/<letter>.md for each resolved letter (you name the paths; you never open them yourself); critique and rank them as a set — which findings are strongest and which weakest and why, which specific suggestions to endorse or challenge (by letter), and any genuinely-new point the set missed; obey the same member_output_cap (≤ 15 consolidated points, regardless of bench size); write a single consolidated critique to council/round-N/opinions/peer/consolidated.md. Return is one status line — Wrote opinions/peer/consolidated.md — <n> points. — same status-only discipline (no critique content ever crosses back). This one file is where the chairman reads the peer round under standard; collapsing a multi-member stage into one session is the tier's largest lever (the 002 finding: each avoided member session also avoids its 25–38 graphify tool-call turns and their cache churn). At the shipped bench-size-5 default this is exactly today's single consolidated session over five opinions — unchanged.
Barrier: wait for the single return, then append its one trace fragment (§ Traces) before dispatching stage 3.
Stage 3 — Chairman synthesis
Dispatch one Opus subagent, mode synthesis — never delta-check; that mode belongs exclusively to /speckit-council-triage's post-revision check, and this skill never invokes it, reopen included. Prompt = the rendered chairman-prompt.md §2 block with {{feature}}, {{round}}, {{opinions_dir}} = council/round-N/opinions, {{peer_dir}} = council/round-N/opinions/peer, {{plan_path}} = plan.md substituted, plus an appendix stating this round's resolved bench size and letters. At the shipped bench-size-5 default with letters A–E, that statement is a no-op relative to what chairman-prompt.md's own "five … A.md … E.md" framing already assumes — nothing changes there. For a bench size other than 5, the appendix also overrides that framing explicitly, in intent: "This round convened <bench size> members, not the template's assumed five — list {{opinions_dir}}/ yourself and read every file present rather than assuming A.md … E.md." At a bench size of 1, the appendix additionally states, verbatim in intent: "This round's bench size is 1: Stage 2 (peer review) was skipped by design, not by failure — a single independent opinion has no second opinion to compare against (M4/FR-017). There is no opinions/peer/ directory this round; do not treat its absence as a missing file. Your ## Chairman's note MUST say plainly that peer review did not run this round and why, in your own words — this disclosure is required, not optional color." The chairman reads every stage-1 opinion present and every stage-2 peer file present under opinions/peer/ — twice the bench size in files under tier full (bench-size opinions + bench-size per-member peer; ten at the shipped default), bench size + 1 under standard (bench-size opinions + one consolidated.md; six at the shipped default), just the bench size, with nothing under opinions/peer/, at a bench size of 1 (peer review skipped, above) — it is the only session ever permitted to open them, and it discovers both the opinions and the peer files by listing rather than assuming a fixed count. It writes council/round-N/suggestions.md: classified rows, stable R<round>-S<nn> IDs, the reduced-grounding banner iff any opinion flagged it, and a Chairman's note (carrying the required bench-size-1 disclosure above when applicable). Its return is status + verdict counts only — no suggestion text, no opinion excerpts.
Council-apparatus provenance (I-29). suggestions.md's metadata block records who prepared it but not which version of the council machinery produced it. Once the chairman's return confirms the file exists, resolve APPARATUS_SHA=$(git rev-parse HEAD -- extensions/council/) yourself — the same direct-git-command mechanism /speckit-council-triage uses for its own plan/deck provenance, never delegated to the chairman — and insert one new line immediately after the file's existing **Reads**: line:
**Council apparatus**: `extensions/council/` @ `<APPARATUS_SHA>`
Before resolving it, run git status --porcelain -- extensions/council/: any output means extensions/council/ is dirty and the SHA alone doesn't fully describe what ran — append the literal suffix (dirty — uncommitted changes present) to the line; omit it when the tree is clean. Write this line only into the suggestions.md you generate this run — never back-fill it into a round-N/suggestions.md written before this instruction existed; that would violate the immutability guardrail below (a completed round is never touched again) even though it isn't literally an overwrite.
Barrier: wait for the return, insert the council-apparatus provenance line above, then append the chairman's trace fragment (§ Traces).
Query-ceiling enforcement (arm 4 — D77, S09, 005-graphify-context)
A council member's graph-query loop is bounded by a hard, tier-aware ceiling — tiers.<tier>.query_ceiling in council-config.yml (Pre-Execution step 4): standard: 15 (D77, calibrated from this round's uncapped per-member max of 9); full: unset/uncapped until its own baseline is measured. This applies to every council-member dispatch — stage 1, and stage 2's per-member (full) or consolidated (standard) reviewer.
The member reports its count; the orchestrator enforces the consequence — mechanically (S09/D53). The member prompt (member-prompt.md, wired by 005's T027) instructs the member to stop after its Nth graph query and to append its graph-query count to its status-line return (graph_queries: <count>, the same metadata-only channel as the standard-tier consulted: suffix — still status-only, S2). A member scrambling at its ceiling is prompt-following at its least reliable, so the load-bearing guarantee is not the member's own prose — it is mechanical, on you, the orchestrator, at each member barrier:
- Run
.specify/extensions/council/scripts/ceiling-check.sh <tier> <count>(source:extensions/council/extension/scripts/ceiling-check.sh). It readsquery_ceilingfor the tier and printsceiling_hit: true|falseand, iff hit, the exact reduced-grounding disclosure line on a second line. - On
ceiling_hit: true, mechanically append that disclosure line to the member's own opinion file — a blind append (ceiling-check.sh <tier> <count> | tail -n +2 >> "$FEATURE_DIR/council/round-N/opinions/<letter>.md"), never reading the opinion's content. An append is a write, not a read, so the S2/SC-005 context-hygiene invariant (you never openopinions/) still holds by construction. The member's own prose, if it disclosed anything, is courtesy; this mechanical append is what guarantees the chairman weights a ceiling-limited opinion rather than trusting it as fully grounded (SC-008). - Record
graph_queries: <count>andceiling_hit: <bool>in that member's trace fragment (§ Traces) — the "never silent" flag (SC-006/SC-008): a ceiling-limited opinion is auditable after the round from the trace itself, not only mid-round.
Under full (uncapped), ceiling-check.sh returns ceiling_hit: false for any count, no disclosure fires, and the fragment still records the observed graph_queries — the count that will calibrate full's own eventual ceiling (the SC-006 measurement trigger, booked against the first post-arm-4 full-tier round). If a member's return omits graph_queries (e.g. a pre-T027 member prompt, or a failed member), record ceiling_hit: false with the count you can determine, or omit both fields for that fragment if none is knowable — never fabricate a count (the exact-or-null ethos, D47).
Context hygiene — the one rule that cannot bend (S2, SC-005)
Across every stage above, you never read opinions/ or opinions/peer/ — not to confirm a member finished (use test -f), not to sanity-check quality, not for any reason. The only council artifact whose content you ever read is round-N/suggestions.md, once, after the stage-3 barrier — that is what your completion report is built from. This is what makes SC-005's grep-based conformance check sufficient: there is no opinion content in your transcript to leak, by construction, not by discipline alone.
Traces — serial append, never parallel (R-D3, trace-fragment.md §5)
/speckit-council's own invocation is not itself a traced role — the council roles this command traces are exactly deck-prep, council-member, and chairman (never an orchestrator fragment for itself). Every fragment you collect therefore carries parent_trace_id: null — the same reasoning that gives /speckit-council-triage's own fragment parent_trace_id: null: it, too, is dispatched directly by the interactive main thread rather than by another traced session, so there is no in-file trace_id for it to reference.
After each barrier (deck-prep's one return; stage 1's returns — as many as the resolved bench size; stage 2's returns — the bench size under tier full, one under standard, zero at a bench size of 1 regardless of tier (M4/FR-017); chairman's one return), append that barrier's fragment(s) to $FEATURE_DIR/traces.jsonl one line at a time, immediately, before dispatching the next stage — never batch across barriers, never write two lines concurrently. However many members convene, their stage-1 returns are still that many separate, sequential appends, in letter order — five, in letter order A–E, at the shipped default. The standard-tier consolidated peer session traces as council-member (Sonnet, phase: "council"), exactly like a per-member peer session — same role, same fields; there is one fragment instead of the bench size (and none at all at a bench size of 1).
Per fragment, fixed council values (trace-fragment.md §2): agent_id: null, skills: [], elevated_grants: [], cost_usd: null, schema_version set to the version in force per docs/contracts/trace-schema.md's Status line (currently "1.5"). Per-role values (§3, D18 via council-config.yml):
| Role | phase |
model |
effort |
artifact |
|---|---|---|---|---|
deck-prep |
"deck-prep" |
"claude-sonnet-5" |
"medium" |
the deck path |
council-member |
"council" |
"claude-sonnet-5" |
"medium" |
null (an opinion is chairman-only, never an artifact-out) |
chairman |
"council" |
"claude-opus-4-8" |
"xhigh" |
the suggestions.md path |
Arm-4 fields on council-member fragments (D77, 005; trace-schema.md §1/§7 rule 12, role-scoped). Every council-member fragment (stage 1, and stage 2's per-member/consolidated reviewer) additionally carries graph_queries: <int> and ceiling_hit: <bool> — the two present or absent together, and only on council-member records (never on deck-prep/chairman). They are the Query-ceiling-enforcement outputs above: graph_queries is the count the member reported; ceiling_hit is ceiling-check.sh's verdict for the tier. See trace-fragment.md §3.1.
tokens/capture_method follow trace-fragment.md §4's policy exactly, via the shared token_capture helper (011-token-capture): after the dispatched session (deck-prep / member / chairman) returns, locate its own transcript JSONL (the dispatched session's file under ~/.claude/projects/<project>/) and call python3 <token_capture.py> <transcript_path> <aggregate>, passing the dispatch's own aggregate token count (subagent_tokens from the Agent-tool completion). Use the helper's returned {tokens, capture_method} verbatim: the four-field {input, output, cache_read, cache_creation} breakdown with capture_method: "transcript" on a clean capture, or capture_method: "unavailable" + tokens: null when it declines (transcript absent / ambiguous span / unreconcilable — never a guessed, partial, or estimated number, D47). When that output additionally carries capture_basis: "reconciled" (013-token-capture-fix's R1-S02 ruling — the cross-checked, multi-span-unique-match case; token-capture-v2.md §2/§5), record it on the fragment verbatim too; when it does not, omit the key entirely — never capture_basis: null, never "" — which is the single-clean-span row, or any capture predating this field. The helper is the single home of this parse — do not re-implement the extraction here; resolve it installed-first (.specify/extensions/workforce/scripts/token_capture.py) else source (extensions/workforce/extension/scripts/token_capture.py). Mint a fresh, unique trace_id per fragment (e.g. trc_ + a ULID/timestamp-random token) — never reused across sessions.
Reopen — --reopen delta|full (FR-017, D46, research.md R-D7)
Both tiers still run the round-resolution in Pre-Execution step 6 and still produce a normal round-N/suggestions.md via a synthesis-mode chairman — a reopen is a round with a different-shaped context package, not a different pipeline.
Mid-implementation self-reopen guard (S17,
005-graphify-contextarm 4). While005itself is mid-implementation — after arm 4 is spec'd but before its member-prompt change (T027) wires the query-cap instruction intomember-prompt.md— a/speckit-council --reopen deltaon005would dispatch the pre-ceiling member prompt (nograph_queriesreport, no cap). That is expected and harmless (the Query-ceiling-enforcement section above recordsceiling_hit: falseand omits the count it cannot know); this note simply flags the pre-ceiling prompt status until T027 lands. Once T027 is in, the member prompt reports the count and every reopen is fully ceiling-aware. Do not confuse this with/speckit-council-triage's own "chairman-only delta check" (FR-010) — that is a same-round, append-to-the-existing-suggestions.mdre-adjudication triage runs after one blocking-triggered revision; it is a different mechanism entirely and this skill never touches it.
full — no special handling: run Stage 0–3 exactly as a fresh round would (deck-prep regenerates defense-deck/ in place; members get the full deck + plan + spec). The only difference from an ordinary re-run is cosmetic — note "reopen (full)" in the completion report.
delta — the cheap tier, and FR-017's default:
- The triggering finding is required, verbatim, from
$ARGUMENTS. If--reopen deltawas passed with no finding text, stop and ask for it — it is factual input only the caller has; do not invent or infer one. - Compute the plan diff yourself. This is plain
plan.mdcontent, notopinions/— reading it is normal and expected. Find the most recent## Round Nsection in$FEATURE_DIR/council/decision-record.mdand itsPlan reviewed: plan.md @ <sha>line; that<sha>is the diff base. Rungit diff <sha>..HEAD -- "$FEATURE_DIR/plan.md". Ifdecision-record.mddoesn't exist yet, there is nothing to reopen — that's a contract error: tell the user and suggest--reopen fullor a plain/speckit-councilinstead. - Skip Stage 0 (deck-prep) entirely.
defense-deck/is not touched. - Stage 1's appendix changes: instead of deck/plan/spec paths, each member's dispatch gives the diff text and the finding text as the entire review context, and says so explicitly — "this is a delta reopen: you have no deck and no full plan; review only the diff and the finding below." Stage 2 and Stage 3 are otherwise unchanged (peer review reads this same round's stage-1 opinions; the chairman still runs
synthesismode over whatever the members produced). - You do not write
## Reopentodecision-record.md— that section is/speckit-council-triage's to write when it processes this round, not this skill's. Your job ends atsuggestions.md; make your completion report state the tier, the diff base sha, and the finding text plainly enough that whoever runs triage next has what they need.
Exit states
| State | Trigger | Result |
|---|---|---|
success |
stage 3 barrier cleared | round-N/suggestions.md written; completion report returned |
no-plan |
$FEATURE_DIR/plan.md missing |
error reported; nothing written (Pre-Execution step 3) |
no-graph |
graphify-out/graph.json absent |
still success — the round completes deck-only, with the reduced-grounding banner surfaced in suggestions.md (FR-019) |
Completion Report
## Council Round Complete — <feature> (round <N>[, reopen: delta|full])
Tier: full | standard (D56)
Bench size: <size> (M1 — profile.yaml council_members, else council-config.yml member_count; 5 is the shipped default)
Suggestions: council/round-<N>/suggestions.md
Verdict: <b> blocking · <s> strong · <c> consider
Sessions: 1 deck-prep + <size|0> stage-1 + <size|1|0> stage-2 + 1 chairman = <total> (traces appended)
Read-rate: <k>/<size> stage-1 members consulted plan.md [· spec <k>/<size> · graph <k>/<size>] (standard tier only; from the consulted: return metadata)
Grounding: full | reduced (no graphify-out/graph.json)
Peer review: — | skipped (bench size 1 — M4/FR-017; see suggestions.md Chairman's note)
Reopen: — | delta (base <sha>, finding: "<text>") | full
Next: /speckit-council-triage
Stage-2 session count: the resolved bench size under tier full (per-member; 5 at the shipped default), 1 under standard (consolidated, regardless of bench size), 0 for --reopen-only edge states that skip it, and 0 at a bench size of 1, at either tier (M4/FR-017 — recorded in suggestions.md's Chairman's note, not merely absent; surface it on the Peer review: line above). Omit the Read-rate line entirely under tier full (eager context — every member reads plan.md by construction, so the metric is trivially <size>/<size> and carries no signal). Omit the Peer review: line entirely except at a bench size of 1, where it is required. Return this — never opinion content, never suggestion text beyond the verdict counts already shown; the next command reads suggestions.md directly for the detail.
Guardrails
- Subscription auth only (D28) — never reference or set
ANTHROPIC_API_KEY; every dispatched subagent runs on the same Claude subscription as this session. - Never dispatch the chairman in
delta-checkmode from this skill — that belongs to/speckit-council-triagealone. - Never overwrite an existing round's
opinions/,opinions/peer/, orsuggestions.md— a completed round is immutable; onlydefense-deck/is ever overwritten in place, and never for--reopen delta. - Never write to
decision-record.md— that is triage's artifact, not this skill's. - Stage 1 is always one message with as many
Agentcalls as the resolved bench size (never issued as separate messages — five, at the shipped default); stage 2 is likewise that many calls under tierfull, a singleAgentcall understandard(the consolidated reviewer), or zero calls at a bench size of 1, at either tier (M4/FR-017 — see Stage 2). - Tier resolves to
fullwhen unresolved (D56, T1). An absent/unparseablecouncil_tierin bothprofile.yamlandcouncil-config.ymlmeans the fullest review — never silently drop tostandard. A tier never changes the D18 model map or the gate mode; it only changes stage-2 session count, stage-1 context, and the output cap. - Bench size resolves to
council-config.yml'smember_countwhen unresolved (profile-schema.md§10 M1). An absentcouncil_membersinprofile.yamlfalls back to the config default (5, shipped) — the same two-place arrangement ascouncil_tier(T1), applied a second time, not a second pattern invented.validate-profile.pyhas already rejected any out-of-range or malformed value before this skill runs (M2); this skill never re-validates, clamps, or second-guesses the resolved value itself. - Never invent lens content for letters beyond the evidenced five (R1-S11). A bench size above 5 resolves letters F/G/H's lens content at convene time via the self-derivation instruction (Pre-Execution step 4) — never a fixed word typed into this skill or into
council-config.yml. Bench size never changes the D18 model map or who signs the gate (M3) — it only changes how many independent opinions convene. - If
plan.mdis missing, stop before creating anything.
Done When
- Bench size resolved (
profile-schema.md§10 M1):profile.yamlcouncil_members→council-config.ymlmember_count→ letters/lenses assigned (M5) — bench size ≤5 takes the evidenced set first-N in order, bench size >5 resolves letters F/G/H at convene time (R1-S11, never hardcoded); bench size 5 reproduces today's dispatch, lenses, and letters exactly - Ceremony tier resolved (D56):
profile.yamlcouncil_tier→council-config.ymldefault →full; itspeer_review/context/member_output_capparams loaded - Round number resolved by the resumability rule (fresh, resumed, or reopened) — no prior round ever overwritten
- Deck prep done (skipped only for
--reopen delta) - All applicable stage-1 and stage-2 sessions returned status-only, and their trace fragments were appended serially after their barrier (stage 2 = bench size per-member under
full, 1 consolidated understandard, 0 at a bench size of 1 — recorded, not silent, M4/FR-017) - Chairman ran in
synthesismode andround-N/suggestions.mdexists, classified and ID'd, with the reduced-grounding banner iff triggered, and — at a bench size of 1 — the Chairman's note states plainly that peer review did not run and why - No
opinions/content ever entered this session's own context - Model map (Sonnet members, Opus chairman) and tier handling unchanged at every bench size (M3)
- Completion report returned in the format above