cclaw-cli 6.6.0 → 6.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/artifact-linter/findings-dedup.d.ts +56 -0
  2. package/dist/artifact-linter/findings-dedup.js +232 -0
  3. package/dist/artifact-linter/plan.js +3 -2
  4. package/dist/artifact-linter/shared.d.ts +49 -0
  5. package/dist/artifact-linter/shared.js +35 -0
  6. package/dist/artifact-linter.d.ts +1 -1
  7. package/dist/artifact-linter.js +45 -3
  8. package/dist/content/hooks.js +36 -1
  9. package/dist/content/node-hooks.js +43 -0
  10. package/dist/content/skills-elicitation.js +3 -6
  11. package/dist/content/skills.js +1 -1
  12. package/dist/content/stages/brainstorm.js +4 -4
  13. package/dist/content/stages/scope.js +2 -2
  14. package/dist/content/templates.js +3 -2
  15. package/dist/delegation.d.ts +9 -0
  16. package/dist/delegation.js +3 -0
  17. package/dist/internal/advance-stage/advance.js +23 -1
  18. package/dist/internal/advance-stage/parsers.d.ts +8 -0
  19. package/dist/internal/advance-stage/parsers.js +7 -0
  20. package/dist/internal/advance-stage/proactive-delegation-trace.d.ts +3 -0
  21. package/dist/internal/advance-stage/proactive-delegation-trace.js +8 -1
  22. package/dist/internal/advance-stage/rewind.js +2 -2
  23. package/dist/internal/advance-stage/start-flow.js +4 -1
  24. package/dist/internal/advance-stage.js +32 -2
  25. package/dist/internal/flow-state-repair.d.ts +13 -0
  26. package/dist/internal/flow-state-repair.js +65 -0
  27. package/dist/internal/waiver-grant.d.ts +62 -0
  28. package/dist/internal/waiver-grant.js +294 -0
  29. package/dist/run-persistence.d.ts +70 -0
  30. package/dist/run-persistence.js +215 -3
  31. package/dist/runs.d.ts +1 -1
  32. package/dist/runs.js +1 -1
  33. package/dist/runtime/run-hook.mjs +43 -0
  34. package/package.json +1 -1
@@ -29,7 +29,7 @@ Pinned anchor: "Don't tell it what to do, give it success criteria and watch it
29
29
  These behaviors are the exact reason this skill exists. The linter will block your stage-complete if you do them.
30
30
 
31
31
  - **Bad**: User asks for a "simple web app" -> agent asks 1 question about stack -> 1 question about auth -> drafts the brainstorm artifact and asks for approval.
32
- - **Good**: User asks for a "simple web app" -> agent asks Q1 (what pain) -> Q2 (direct path) -> Q3 (do-nothing cost) -> Q4 (first operator/user) -> Q5 (no-go boundaries) -> self-eval: clear -> drafts the brainstorm artifact.
32
+ - **Good**: User asks for a "simple web app" -> agent asks Q1 (what pain) -> Q2 (direct path) -> Q3 (first operator/user) -> Q4 (no-go boundaries) -> self-eval: clear -> drafts the brainstorm artifact.
33
33
 
34
34
  - **Bad**: Agent immediately dispatches a subagent (\`product-discovery\`, \`critic\`, \`planner\`) at the start of brainstorm/scope/design to "gather context" before any user dialogue.
35
35
  - **Good**: Agent walks the Q&A loop with the user first; subagent dispatch happens only after the user approves the elicitation outcome.
@@ -121,7 +121,7 @@ Default mapping note: \`lean\` maps to a lightweight specialist tier on early st
121
121
 
122
122
  ### Topic tagging (MANDATORY for forcing-question rows)
123
123
 
124
- Each forcing question has a stable topic id (kebab-case ASCII, e.g. \`pain\`, \`do-nothing\`, \`data-flow\`). Tag the matching Q&A Log row's \`Decision impact\` cell with \`[topic:<id>]\` so the linter can verify coverage in any natural language. This is a **HARD requirement** in Wave 24 (v6.0.0): the linter no longer keyword-matches English question prose, so an un-tagged row does NOT count toward coverage even if the answer fully addresses the topic.
124
+ Each forcing question has a stable topic id (kebab-case ASCII, e.g. \`pain\`, \`direct-path\`, \`data-flow\`). Tag the matching Q&A Log row's \`Decision impact\` cell with \`[topic:<id>]\` so the linter can verify coverage in any natural language. This is a **HARD requirement** in Wave 24 (v6.0.0): the linter no longer keyword-matches English question prose, so an un-tagged row does NOT count toward coverage even if the answer fully addresses the topic.
125
125
 
126
126
  RU example (after asking \`pain\` in Russian):
127
127
 
@@ -131,21 +131,18 @@ RU example (after asking \`pain\` in Russian):
131
131
  | 1 | Какую боль мы решаем? | Регистрация занимает 30 минут. | scope-shaping [topic:pain] |
132
132
  \`\`\`
133
133
 
134
- Multiple tags in one row are allowed when one answer covers several topics: \`[topic:pain] [topic:do-nothing]\`. Stop-signal rows do NOT need a tag.
134
+ Multiple tags in one row are allowed when one answer covers several topics: \`[topic:pain] [topic:direct-path]\`. Stop-signal rows do NOT need a tag.
135
135
 
136
136
  Stage forcing question lists (id → topic):
137
137
 
138
138
  - **Brainstorm**:
139
139
  - \`pain\` — What pain are we solving?
140
140
  - \`direct-path\` — What is the most direct path?
141
- - \`do-nothing\` — What happens if we do nothing?
142
141
  - \`operator\` — Who is the operator/user impacted first?
143
142
  - \`no-go\` — What are non-negotiable no-go boundaries?
144
143
  - **Scope**:
145
144
  - \`in-out\` — What is definitely in and definitely out?
146
145
  - \`locked-upstream\` — Which decisions are already locked upstream?
147
- - \`rollback\` — What is the rollback path if this fails?
148
- - \`failure-modes\` — What are the top failure modes we must design for?
149
146
  - **Design**:
150
147
  - \`data-flow\` — What is the data flow end-to-end?
151
148
  - \`seams\` — Where are the seams/interfaces and ownership boundaries?
@@ -430,7 +430,7 @@ function completionParametersBlock(schema, track) {
430
430
  - \`delegation lifecycle proof\`: use the delegation helper recipe in this section with explicit lifecycle rows: \`--status=scheduled\` -> \`--status=launched\` -> \`--status=acknowledged\` -> \`--status=completed\` (completed isolated/generic requires prior ACK for the same span or \`--ack-ts=<iso>\`).
431
431
  - Fill \`## Learnings\` before closeout: either \`- None this stage.\` or JSON bullets with required keys \`type\`, \`trigger\`, \`action\`, \`confidence\` (knowledge-schema compatible).
432
432
  - If you edit any completed-stage artifact after it shipped (\`completedStageMeta\` timestamps exist), append a short \`## Amendments\` section with dated bullets (timestamp + reason) instead of overwriting the archived narrative silently — advisory linter rule \`stage_artifact_post_closure_mutation\` enforces visibility when this trail is missing.
433
- - Record mandatory delegation lifecycle in \`${RUNTIME_ROOT}/state/delegation-log.json\` and append proof events to \`${RUNTIME_ROOT}/state/delegation-events.jsonl\`; the ledger is current state, the event log is audit proof.${mandatoryAgents.length > 0 ? ` If a mandatory delegation cannot run in this harness, use \`--waive-delegation=${mandatoryAgents.join(",")} --waiver-reason="<why safe>"\` on the completion helper.` : ""} If proactive delegations were intentionally skipped, rerun only with \`--accept-proactive-waiver\` (optionally \`--accept-proactive-waiver-reason="<why safe>"\`) after explicit user approval.
433
+ - Record mandatory delegation lifecycle in \`${RUNTIME_ROOT}/state/delegation-log.json\` and append proof events to \`${RUNTIME_ROOT}/state/delegation-events.jsonl\`; the ledger is current state, the event log is audit proof.${mandatoryAgents.length > 0 ? ` If a mandatory delegation cannot run in this harness, use \`--waive-delegation=${mandatoryAgents.join(",")} --waiver-reason="<why safe>"\` on the completion helper.` : ""} If proactive delegations were intentionally skipped, first issue a short-lived waiver token with \`cclaw-cli internal waiver-grant --stage <stage> --reason "<short-slug>"\`, then rerun the completion helper with \`--accept-proactive-waiver=<token> --accept-proactive-waiver-reason="<why safe>"\` after explicit user approval. Tokens expire in 30 minutes and are single-use; bare \`--accept-proactive-waiver\` is no longer accepted.
434
434
  - Never edit raw \`flow-state.json\` to complete a stage, even in advisory mode; that bypasses validation, gate evidence, and Learnings harvest. If a helper fails, report a one-line human-readable failure plus fenced JSON diagnostics; never echo the invoking command line or apply a manual state workaround.
435
435
  - Stage completion claim requires \`stage-complete\` exit 0 in the current turn. Quote the single-line success JSON exactly as printed to stdout (for example \`{"ok":true,"command":"stage-complete",...}\` including \`completedStages\` / \`currentStage\` / \`runId\`); do not paraphrase. Do not infer success from empty stdout or from skipped retries (quiet mode always emits one JSON line on success).
436
436
  - Completion protocol: verify required gates, update the artifact, then use the completion helper with \`--evidence-json\` and \`--passed\` for every satisfied gate.
@@ -38,10 +38,10 @@ export const BRAINSTORM = {
38
38
  checklist: [
39
39
  "**ADAPTIVE ELICITATION COMES FIRST (no exceptions, no subagent dispatch before).** Load `.cclaw/skills/adaptive-elicitation/SKILL.md`. Walk the brainstorm forcing questions one-at-a-time via the harness-native question tool, append one row to `## Q&A Log` (`Turn | Question | User answer (1-line) | Decision impact`) after each user answer **and stamp the row's `Decision impact` cell with the matching `[topic:<id>]` tag** (e.g. `[topic:pain]`). Continue until every forcing-question topic id is tagged on a row OR Ralph-Loop convergence detector says no new decision-changing rows in last 2 iterations OR user records an explicit stop-signal row. Only then proceed to delegations, drafts, or analysis. The linter `qa_log_unconverged` rule will block `stage-complete` if convergence is not reached.",
40
40
  "**Explore project context** — after the elicitation loop converges, inspect existing files/docs/recent activity to refine the Discovered context section; capture matching files/patterns/seeds in `Context > Discovered context` so downstream stages don't redo discovery.",
41
- "**Brainstorm forcing questions (must be covered or explicitly waived)** — `pain: what pain are we solving`; `direct-path: what is the direct path`; `do-nothing: what happens if we do nothing`; `operator: who is the first operator/user affected`; `no-go: what no-go boundaries are non-negotiable`. Tag the matching `## Q&A Log` row's `Decision impact` cell with `[topic:<id>]` (e.g. `[topic:pain]`) so the linter can verify coverage in any natural language. Tags are MANDATORY for forcing-question rows; un-tagged rows do NOT count toward coverage.",
41
+ "**Brainstorm forcing questions (must be covered or explicitly waived)** — `pain: what pain are we solving`; `direct-path: what is the direct path`; `operator: who is the first operator/user affected`; `no-go: what no-go boundaries are non-negotiable`. Tag the matching `## Q&A Log` row's `Decision impact` cell with `[topic:<id>]` (e.g. `[topic:pain]`) so the linter can verify coverage in any natural language. Tags are MANDATORY for forcing-question rows; un-tagged rows do NOT count toward coverage. Round 6 (v6.7.0) removed the counterfactual `do-nothing` topic; the Problem Decision Record already captures `Do-nothing consequence`.",
42
42
  "**Discovery posture (flow-state `discoveryMode`)** — follow `lean` / `guided` / `deep` from the active run. Use lean for smallest safe discovery pass; guided as the default balanced pass; escalate to deep when ambiguity, architecture, external dependency, security/data risk, or explicit think-bigger requests warrant fuller option pressure and mandatory specialist coverage.",
43
43
  "**Write the Problem Decision Record** — pick a free-form `Frame type` label that names how this work is framed (examples: product, technical-maintenance, research-spike, ops-incident, infrastructure), then fill the universal Framing fields: affected user/role/operator, current state/failure mode/opportunity, desired observable outcome, evidence/signal, why now, do-nothing consequence, and non-goals.",
44
- "**Premise check (one pass)** — answer the three gstack-style questions in the artifact body: *Right problem? Direct path? What if we do nothing?* Take a position; do not hedge.",
44
+ "**Premise check (one pass)** — answer the two gstack-style questions in the artifact body: *Right problem? Direct path?* Take a position; do not hedge. Round 6 (v6.7.0): the counterfactual premise line was retired; Do-nothing consequence already lives in the Problem Decision Record.",
45
45
  "**Reframe with How Might We** — write a single `How Might We …?` line that names the user/operator, the desired outcome, and the constraint. This is the altitude check before approaches.",
46
46
  "**Run Clarity Gate** — record ambiguity score (0.00-1.00), decision boundaries, reaffirmed non-goals, and residual-risk handoff before locking recommendations. If ambiguity remains high (>0.40), ask one decision-changing question before recommending.",
47
47
  "**Sharpening question discipline** — ask one decision-changing question at a time. Do not default to 3-5 batched questions; record only questions that changed the direction or a critical stop decision.",
@@ -62,7 +62,7 @@ export const BRAINSTORM = {
62
62
  "\"If something is unclear, stop. Name what's confusing. Ask.\"",
63
63
  "Start from observed project context; if the idea is vague, first narrow the project type with **one** structured question, then keep going.",
64
64
  "Honor the run's `discoveryMode` (`lean` | `guided` | `deep`) from flow-state: lean stays fastest, guided is the default breadth, deep pulls in fuller critique and mandatory delegations when the run is classified that way.",
65
- "Lead with the premise check (right problem / direct path / what if nothing) and the `How Might We` reframing before approaches; both go in the artifact, not just the chat.",
65
+ "Lead with the premise check (right problem / direct path) and the `How Might We` reframing before approaches; both go in the artifact, not just the chat. Round 6 (v6.7.0) removed the counterfactual premise line; Do-nothing consequence still lives in the Problem Decision Record.",
66
66
  "Ask at most one question per turn, only when decision-changing; if using a structured question tool, send exactly one question object, not a multi-question form.",
67
67
  "Run the shared adaptive elicitation cycle from `.cclaw/skills/adaptive-elicitation/SKILL.md`, including stop-signal handling (RU/EN/UA), smart-skip, conditional grilling triggers, and append-only `## Q&A Log` updates.",
68
68
  "Only non-critical preference/default assumptions may continue inline. STOP and ask when uncertainty affects scope, architecture, security, data loss, public API, migration, auth/pricing, or user approval.",
@@ -142,7 +142,7 @@ export const BRAINSTORM = {
142
142
  artifactValidation: [
143
143
  { section: "Context", required: true, validationRule: "Must reference project state and relevant existing code or patterns. A `Discovered context` subsection (or list) is recommended for downstream traceability." },
144
144
  { section: "Problem Decision Record", required: true, validationRule: "Must include a free-form `Frame type` label (examples only: product, technical-maintenance, research-spike, ops-incident, infrastructure) and the universal Framing fields: affected user/role/operator, current state/failure mode/opportunity, desired observable outcome, evidence/signal, why now, do-nothing consequence, non-goals. The linter checks that the section has meaningful content; the field labels themselves are the structural contract." },
145
- { section: "Premise Check", required: false, validationRule: "Recommended: explicit answers to `Right problem?`, `Direct path?`, `What if we do nothing?` — take a position, do not hedge." },
145
+ { section: "Premise Check", required: false, validationRule: "Recommended: explicit answers to `Right problem?` and `Direct path?` — take a position, do not hedge. Round 6 (v6.7.0) retired the counterfactual premise line; Do-nothing consequence already lives in the Problem Decision Record." },
146
146
  { section: "How Might We", required: false, validationRule: "Recommended: a single `How Might We …?` line naming the user, the outcome, and the binding constraint." },
147
147
  { section: "Clarity Gate", required: false, validationRule: "Recommended before recommendation lock: include ambiguity score (0.00-1.00), decision boundaries, reaffirmed non-goals, and residual-risk handoff for scope." },
148
148
  { section: "Sharpening Questions", required: false, validationRule: "Recommended only when needed: one decision-changing question per turn with explicit `Decision impact`; compact tasks may record `None - early exit` with rationale." },
@@ -47,9 +47,9 @@ export const SCOPE = {
47
47
  executionModel: {
48
48
  checklist: [
49
49
  "**ADAPTIVE ELICITATION COMES FIRST (no exceptions, no subagent dispatch before).** Load `.cclaw/skills/adaptive-elicitation/SKILL.md`. Walk the scope forcing questions one-at-a-time via the harness-native question tool, append one row to `## Q&A Log` (`Turn | Question | User answer (1-line) | Decision impact`) after each user answer **and stamp the row's `Decision impact` cell with the matching `[topic:<id>]` tag** (e.g. `[topic:in-out]`). Continue until every forcing-question topic id is tagged on a row OR Ralph-Loop convergence detector says no new decision-changing rows in last 2 iterations OR user records an explicit stop-signal row. Only then propose the scope contract draft, recommend a mode, or dispatch any delegations. The linter `qa_log_unconverged` rule will block `stage-complete` if convergence is not reached.",
50
- "**Scope forcing questions (must be covered or explicitly waived)** — `in-out: what is definitely in/out`; `locked-upstream: which upstream decisions are locked`; `rollback: what rollback path protects users if scope assumptions fail`; `failure-modes: what are the top failure modes we must design for`. Tag the matching `## Q&A Log` row's `Decision impact` cell with `[topic:<id>]` (e.g. `[topic:in-out]`) so the linter can verify coverage in any natural language. Tags are MANDATORY for forcing-question rows; un-tagged rows do NOT count toward coverage.",
50
+ "**Scope forcing questions (must be covered or explicitly waived)** — `in-out: what is definitely in/out`; `locked-upstream: which upstream decisions are locked`. Tag the matching `## Q&A Log` row's `Decision impact` cell with `[topic:<id>]` (e.g. `[topic:in-out]`) so the linter can verify coverage in any natural language. Tags are MANDATORY for forcing-question rows; un-tagged rows do NOT count toward coverage. Round 6 (v6.7.0) removed the counterfactual `rollback` and `failure-modes` topics from scope forcing questions; Design still owns the Failure Mode Table and rollback evidence.",
51
51
  "**Scope contract first** — read brainstorm handoff, name upstream decisions used, explicit drift, confidence, unresolved questions, and next-stage risk hints; draft the in-scope/out-of-scope/deferred/discretion contract before any design choice.",
52
- "**Premise carry-forward (do NOT re-author)** — brainstorm OWNS the premise check (right problem / direct path / what if nothing). Cite brainstorm's `## Premise Check` section in `## Upstream Handoff > Decisions carried forward`. Add a row to `## Premise Drift` only when the scope-stage Q&A surfaced NEW evidence that materially changes the brainstorm answer (e.g. new constraint, new user signal). Otherwise mark `Premise Drift: None` — do not duplicate the brainstorm premise table.",
52
+ "**Premise carry-forward (do NOT re-author)** — brainstorm OWNS the premise check (right problem / direct path). Cite brainstorm's `## Premise Check` section in `## Upstream Handoff > Decisions carried forward`. Add a row to `## Premise Drift` only when the scope-stage Q&A surfaced NEW evidence that materially changes the brainstorm answer (e.g. new constraint, new user signal). Otherwise mark `Premise Drift: None` — do not duplicate the brainstorm premise table.",
53
53
  "**Conditional 10-star boundary** — for deep/high-risk/product-strategy work, show what would make the product meaningfully better, then explicitly choose what ships now, what is deferred, and what is excluded without vague `later/for now` placeholders. Skip this for straightforward repair work and record `not needed: compact scope`.",
54
54
  "**Pick one operational mode with the user** — HOLD SCOPE preserves focus; SELECTIVE EXPANSION cherry-picks high-leverage reference ideas; SCOPE EXPANSION explores ambitious alternatives; SCOPE REDUCTION cuts to the essential wedge. Recommend one, state why and what signal would change it, then keep elicitation focused until the user either approves or asks to proceed with draft boundaries.",
55
55
  "**Product-discovery is REQUIRED for SELECTIVE / SCOPE EXPANSION (hard gate)** — If the resolved scope mode is SELECTIVE EXPANSION or SCOPE EXPANSION, run \`product-discovery\` in proactive mode **after** adaptive elicitation converges and **before** \`stage-complete\`. Do not complete this stage until the delegation ledger shows \`product-discovery\` as \`completed\` with non-empty \`evidenceRefs\` pointing at this scope artifact. HOLD SCOPE and SCOPE REDUCTION do not require this row.",
@@ -89,7 +89,6 @@ ${renderBehaviorAnchorTemplateLine("brainstorm")}
89
89
  ## Premise Check
90
90
  - **Right problem?** (yes/no + one-line justification — take a position)
91
91
  - **Direct path?** (yes/no + one-line justification)
92
- - **What if we do nothing?** (concrete consequence, not "nothing happens")
93
92
 
94
93
  ## How Might We
95
94
  - *How might we …?* — one line naming the user, the desired outcome, and the binding constraint.
@@ -117,7 +116,7 @@ ${renderBehaviorAnchorTemplateLine("brainstorm")}
117
116
  | 1 | | | scope-shaping [topic:pain] |
118
117
 
119
118
  > Append-only by turn. Add one row after each user answer; do not rewrite prior rows.
120
- > **Topic tag is MANDATORY for forcing-question rows.** Stamp \`[topic:<id>]\` in the \`Decision impact\` cell so the linter can verify coverage in any natural language (RU/EN/UA/etc.). Brainstorm IDs: \`pain\`, \`direct-path\`, \`do-nothing\`, \`operator\`, \`no-go\`. Multiple tags allowed when one answer covers several topics. Stop-signal rows do NOT need a tag. Wave 24 (v6.0.0) removed the English keyword fallback.
119
+ > **Topic tag is MANDATORY for forcing-question rows.** Stamp \`[topic:<id>]\` in the \`Decision impact\` cell so the linter can verify coverage in any natural language (RU/EN/UA/etc.). Brainstorm IDs: \`pain\`, \`direct-path\`, \`operator\`, \`no-go\`. Multiple tags allowed when one answer covers several topics. Stop-signal rows do NOT need a tag. Wave 24 (v6.0.0) removed the English keyword fallback; Round 6 (v6.7.0) retired the counterfactual \`do-nothing\` topic (Do-nothing consequence stays in the Problem Decision Record).
121
120
 
122
121
  ## Approach Tier
123
122
  - Tier: lite | standard | deep
@@ -948,12 +947,14 @@ Execution rule: complete and verify each batch before starting the next batch.
948
947
  - **Inline recipe (if Inline executor):** TDD loop unit-by-unit with batch checkpoints
949
948
 
950
949
  ## Plan Quality Scan
950
+ <!-- linter-meta -->
951
951
  - Placeholder scan:
952
952
  - Scanned tokens: \`TODO\`, \`TBD\`, \`FIXME\`, \`<fill-in>\`, \`<your-*-here>\`, \`xxx\`, bare ellipsis in task rows.
953
953
  - Hits: 0 (required for WAIT_FOR_CONFIRM to resolve).
954
954
  - Scope reduction language scan:
955
955
  - Scanned phrases: \`v1\`, \`for now\`, \`later\`, \`temporary\`, \`placeholder\`, \`mock for now\`, \`hardcoded for now\`, \`will improve later\`.
956
956
  - Hits: 0 (required when Locked Decisions section is non-empty; reference D-XX IDs from scope).
957
+ <!-- /linter-meta -->
957
958
 
958
959
  ## WAIT_FOR_CONFIRM
959
960
  - Status: pending
@@ -60,6 +60,15 @@ export type DelegationEntry = {
60
60
  taskId?: string;
61
61
  waiverReason?: string;
62
62
  acceptedBy?: DelegationWaiverAcceptedBy;
63
+ /**
64
+ * Waiver approval token captured from `cclaw-cli internal waiver-grant`.
65
+ * Present on waiver rows written after v6.7.0. Legacy waiver rows omit
66
+ * these fields and are surfaced as the advisory linter finding
67
+ * `waiver_legacy_provenance`.
68
+ */
69
+ approvalToken?: string;
70
+ approvalReason?: string;
71
+ approvalIssuedAt?: string;
63
72
  ts?: string;
64
73
  /**
65
74
  * Run id the entry belongs to. Older ledgers written before 0.5.17 may omit this;
@@ -199,6 +199,9 @@ function isDelegationEntry(value) {
199
199
  (o.taskId === undefined || typeof o.taskId === "string") &&
200
200
  (o.waiverReason === undefined || typeof o.waiverReason === "string") &&
201
201
  (o.acceptedBy === undefined || o.acceptedBy === "user-flag") &&
202
+ (o.approvalToken === undefined || typeof o.approvalToken === "string") &&
203
+ (o.approvalReason === undefined || typeof o.approvalReason === "string") &&
204
+ (o.approvalIssuedAt === undefined || typeof o.approvalIssuedAt === "string") &&
202
205
  waiverOk &&
203
206
  (o.runId === undefined || typeof o.runId === "string") &&
204
207
  (o.fulfillmentMode === undefined ||
@@ -12,6 +12,7 @@ import { extractReviewLoopEnvelopeFromArtifact } from "../../content/review-loop
12
12
  import { unique } from "./helpers.js";
13
13
  import { AUTO_REVIEW_LOOP_GATE_BY_STAGE, reviewLoopArtifactFixHint, reviewLoopEnvelopeExample, validateGateEvidenceShape } from "./review-loop.js";
14
14
  import { ensureProactiveDelegationTrace } from "./proactive-delegation-trace.js";
15
+ import { consumeWaiverToken } from "../waiver-grant.js";
15
16
  function resolveSuccessorTransition(stage, track, transitionTargets, satisfiedGuards, selectedTransitionGuards) {
16
17
  const natural = transitionTargets[0] ?? null;
17
18
  const specialTargets = transitionTargets.filter((target) => target !== natural);
@@ -542,9 +543,30 @@ export async function runAdvanceStage(projectRoot, args, io) {
542
543
  }
543
544
  return 1;
544
545
  }
546
+ let approvalRecord = null;
547
+ if (args.acceptProactiveWaiver) {
548
+ const tokenRaw = args.acceptProactiveWaiverToken?.trim() ?? "";
549
+ if (tokenRaw.length === 0) {
550
+ io.stderr.write(`cclaw internal advance-stage: --accept-proactive-waiver now requires =<token>. Run \`cclaw-cli internal waiver-grant --stage ${args.stage} --reason "<why safe>"\` to issue one, then rerun with --accept-proactive-waiver=<token>.\n`);
551
+ return 2;
552
+ }
553
+ const consumed = await consumeWaiverToken(projectRoot, {
554
+ stage: args.stage,
555
+ token: tokenRaw,
556
+ consumedBy: "advance-stage"
557
+ });
558
+ if (!consumed.ok) {
559
+ io.stderr.write(`cclaw internal advance-stage: waiver token rejected (${consumed.reason}): ${consumed.detail}. Issue a fresh token via \`cclaw-cli internal waiver-grant --stage ${args.stage} --reason "<why safe>"\`.\n`);
560
+ return 2;
561
+ }
562
+ approvalRecord = consumed.record;
563
+ }
545
564
  const proactiveTrace = await ensureProactiveDelegationTrace(projectRoot, args.stage, {
546
565
  acceptWaiver: args.acceptProactiveWaiver,
547
566
  waiverReason: args.acceptProactiveWaiverReason,
567
+ approvalToken: approvalRecord?.token,
568
+ approvalReason: approvalRecord?.reason,
569
+ approvalIssuedAt: approvalRecord?.issuedAt,
548
570
  discoveryMode: flowState.discoveryMode,
549
571
  repoSignals: flowState.repoSignals
550
572
  });
@@ -600,7 +622,7 @@ export async function runAdvanceStage(projectRoot, args, io) {
600
622
  currentStage: successor ?? args.stage,
601
623
  interactionHints
602
624
  };
603
- await writeFlowState(projectRoot, finalState);
625
+ await writeFlowState(projectRoot, finalState, { writerSubsystem: "advance-stage" });
604
626
  if (args.quiet) {
605
627
  io.stdout.write(`${JSON.stringify({
606
628
  ok: true,
@@ -8,6 +8,14 @@ export interface AdvanceStageArgs {
8
8
  waiverReason?: string;
9
9
  acceptProactiveWaiver: boolean;
10
10
  acceptProactiveWaiverReason?: string;
11
+ /**
12
+ * Approval token issued by `cclaw-cli internal waiver-grant`. Required
13
+ * (via `--accept-proactive-waiver=<token>`) whenever the caller asserts
14
+ * `acceptProactiveWaiver`. Legacy `--accept-proactive-waiver` without a
15
+ * token is still parsed but rejected downstream by the advance-stage
16
+ * handler so operators see the error at runtime.
17
+ */
18
+ acceptProactiveWaiverToken?: string;
11
19
  skipQuestions: boolean;
12
20
  quiet: boolean;
13
21
  json: boolean;
@@ -12,6 +12,7 @@ export function parseAdvanceStageArgs(tokens) {
12
12
  let waiverReason;
13
13
  let acceptProactiveWaiver = false;
14
14
  let acceptProactiveWaiverReason;
15
+ let acceptProactiveWaiverToken;
15
16
  let skipQuestions = false;
16
17
  let quiet = false;
17
18
  let json = false;
@@ -81,6 +82,11 @@ export function parseAdvanceStageArgs(tokens) {
81
82
  acceptProactiveWaiver = true;
82
83
  continue;
83
84
  }
85
+ if (token.startsWith("--accept-proactive-waiver=")) {
86
+ acceptProactiveWaiver = true;
87
+ acceptProactiveWaiverToken = token.slice("--accept-proactive-waiver=".length).trim();
88
+ continue;
89
+ }
84
90
  if (token === "--skip-questions") {
85
91
  skipQuestions = true;
86
92
  continue;
@@ -107,6 +113,7 @@ export function parseAdvanceStageArgs(tokens) {
107
113
  waiverReason,
108
114
  acceptProactiveWaiver,
109
115
  acceptProactiveWaiverReason,
116
+ acceptProactiveWaiverToken,
110
117
  skipQuestions,
111
118
  quiet,
112
119
  json
@@ -16,6 +16,9 @@ export interface ProactiveDelegationTraceResult {
16
16
  export declare function ensureProactiveDelegationTrace(projectRoot: string, stage: FlowStage, options: {
17
17
  acceptWaiver: boolean;
18
18
  waiverReason?: string;
19
+ approvalToken?: string;
20
+ approvalReason?: string;
21
+ approvalIssuedAt?: string;
19
22
  discoveryMode: DiscoveryMode;
20
23
  repoSignals?: RepoSignals;
21
24
  }): Promise<ProactiveDelegationTraceResult>;
@@ -31,7 +31,11 @@ export async function ensureProactiveDelegationTrace(projectRoot, stage, options
31
31
  return { missingRules: [] };
32
32
  if (!options.acceptWaiver)
33
33
  return { missingRules };
34
- const waiverReason = options.waiverReason?.trim() || "accepted via --accept-proactive-waiver";
34
+ const approvalToken = options.approvalToken?.trim();
35
+ const approvalReason = options.approvalReason?.trim();
36
+ const waiverReason = options.waiverReason?.trim() ||
37
+ approvalReason ||
38
+ "accepted via --accept-proactive-waiver";
35
39
  for (const rule of missingRules) {
36
40
  await appendDelegation(projectRoot, {
37
41
  stage,
@@ -42,6 +46,9 @@ export async function ensureProactiveDelegationTrace(projectRoot, stage, options
42
46
  acceptedBy: "user-flag",
43
47
  conditionTrigger: rule.when,
44
48
  skill: rule.skill,
49
+ ...(approvalToken ? { approvalToken } : {}),
50
+ ...(approvalReason ? { approvalReason } : {}),
51
+ ...(options.approvalIssuedAt ? { approvalIssuedAt: options.approvalIssuedAt } : {}),
45
52
  ts: new Date().toISOString()
46
53
  });
47
54
  }
@@ -40,7 +40,7 @@ export async function runRewind(projectRoot, args, io) {
40
40
  const staleStages = { ...current.staleStages };
41
41
  delete staleStages[args.targetStage];
42
42
  const nextState = { ...current, staleStages };
43
- await writeFlowState(projectRoot, nextState);
43
+ await writeFlowState(projectRoot, nextState, { writerSubsystem: "rewind-ack" });
44
44
  const payload = {
45
45
  ok: true,
46
46
  command: "rewind",
@@ -85,7 +85,7 @@ export async function runRewind(projectRoot, args, io) {
85
85
  staleStages,
86
86
  rewinds: [...current.rewinds, record]
87
87
  };
88
- await writeFlowState(projectRoot, nextState);
88
+ await writeFlowState(projectRoot, nextState, { writerSubsystem: "rewind" });
89
89
  const payload = {
90
90
  ok: true,
91
91
  command: "rewind",
@@ -209,7 +209,10 @@ export async function runStartFlow(projectRoot, args, io) {
209
209
  }
210
210
  const repoSignals = await collectRepoSignals(projectRoot);
211
211
  nextState = { ...nextState, repoSignals };
212
- await writeFlowState(projectRoot, nextState, { allowReset: true });
212
+ await writeFlowState(projectRoot, nextState, {
213
+ allowReset: true,
214
+ writerSubsystem: "start-flow"
215
+ });
213
216
  await appendIdeaArtifact(projectRoot, args, current);
214
217
  const successPayload = {
215
218
  ok: true,
@@ -11,13 +11,33 @@ import { runRewind } from "./advance-stage/rewind.js";
11
11
  import { runVerifyFlowStateDiff, runVerifyCurrentState } from "./advance-stage/verify.js";
12
12
  import { runHookCommand } from "./advance-stage/hook.js";
13
13
  import { parseAdvanceStageArgs, parseCancelRunArgs, parseHookArgs, parseRewindArgs, parseStartFlowArgs, parseVerifyCurrentStateArgs, parseVerifyFlowStateDiffArgs } from "./advance-stage/parsers.js";
14
+ import { parseFlowStateRepairArgs, runFlowStateRepair } from "./flow-state-repair.js";
15
+ import { parseWaiverGrantArgs, runWaiverGrant } from "./waiver-grant.js";
16
+ import { FlowStateGuardMismatchError, verifyFlowStateGuard } from "../run-persistence.js";
17
+ /**
18
+ * Subcommands that mutate or consult flow-state.json via the CLI runtime.
19
+ * They all require the sha256 sidecar to match before continuing so a
20
+ * manual edit hard-blocks with exit code 2 (same contract as the inline
21
+ * hook checks).
22
+ */
23
+ const GUARD_ENFORCED_SUBCOMMANDS = new Set([
24
+ "advance-stage",
25
+ "start-flow",
26
+ "cancel-run",
27
+ "rewind",
28
+ "verify-flow-state-diff",
29
+ "verify-current-state"
30
+ ]);
14
31
  export async function runInternalCommand(projectRoot, argv, io) {
15
32
  const [subcommand, ...tokens] = argv;
16
33
  if (!subcommand) {
17
- io.stderr.write("cclaw internal requires a subcommand: advance-stage | start-flow | cancel-run | rewind | verify-flow-state-diff | verify-current-state | envelope-validate | tdd-red-evidence | tdd-loop-status | early-loop-status | compound-readiness | runtime-integrity | hook\n");
34
+ io.stderr.write("cclaw internal requires a subcommand: advance-stage | start-flow | cancel-run | rewind | verify-flow-state-diff | verify-current-state | envelope-validate | tdd-red-evidence | tdd-loop-status | early-loop-status | compound-readiness | runtime-integrity | hook | flow-state-repair | waiver-grant\n");
18
35
  return 1;
19
36
  }
20
37
  try {
38
+ if (GUARD_ENFORCED_SUBCOMMANDS.has(subcommand)) {
39
+ await verifyFlowStateGuard(projectRoot);
40
+ }
21
41
  if (subcommand === "advance-stage") {
22
42
  return await runAdvanceStage(projectRoot, parseAdvanceStageArgs(tokens), io);
23
43
  }
@@ -57,10 +77,20 @@ export async function runInternalCommand(projectRoot, argv, io) {
57
77
  if (subcommand === "hook") {
58
78
  return await runHookCommand(projectRoot, parseHookArgs(tokens), io);
59
79
  }
60
- io.stderr.write(`Unknown internal subcommand: ${subcommand}. Expected advance-stage | start-flow | cancel-run | rewind | verify-flow-state-diff | verify-current-state | envelope-validate | tdd-red-evidence | tdd-loop-status | early-loop-status | compound-readiness | runtime-integrity | hook\n`);
80
+ if (subcommand === "flow-state-repair") {
81
+ return await runFlowStateRepair(projectRoot, parseFlowStateRepairArgs(tokens), io);
82
+ }
83
+ if (subcommand === "waiver-grant") {
84
+ return await runWaiverGrant(projectRoot, parseWaiverGrantArgs(tokens), io);
85
+ }
86
+ io.stderr.write(`Unknown internal subcommand: ${subcommand}. Expected advance-stage | start-flow | cancel-run | rewind | verify-flow-state-diff | verify-current-state | envelope-validate | tdd-red-evidence | tdd-loop-status | early-loop-status | compound-readiness | runtime-integrity | hook | flow-state-repair | waiver-grant\n`);
61
87
  return 1;
62
88
  }
63
89
  catch (err) {
90
+ if (err instanceof FlowStateGuardMismatchError) {
91
+ io.stderr.write(`cclaw internal ${subcommand}: ${err.message}\n`);
92
+ return 2;
93
+ }
64
94
  io.stderr.write(`cclaw internal ${subcommand} failed: ${err instanceof Error ? err.message : String(err)}\n`);
65
95
  return 1;
66
96
  }
@@ -0,0 +1,13 @@
1
+ import type { Writable } from "node:stream";
2
+ interface InternalIo {
3
+ stdout: Writable;
4
+ stderr: Writable;
5
+ }
6
+ export interface FlowStateRepairArgs {
7
+ reason: string;
8
+ json: boolean;
9
+ quiet: boolean;
10
+ }
11
+ export declare function parseFlowStateRepairArgs(tokens: string[]): FlowStateRepairArgs;
12
+ export declare function runFlowStateRepair(projectRoot: string, args: FlowStateRepairArgs, io: InternalIo): Promise<number>;
13
+ export {};
@@ -0,0 +1,65 @@
1
+ import path from "node:path";
2
+ import { RUNTIME_ROOT } from "../constants.js";
3
+ import { repairFlowStateGuard } from "../run-persistence.js";
4
+ export function parseFlowStateRepairArgs(tokens) {
5
+ let reason;
6
+ let json = false;
7
+ let quiet = false;
8
+ for (let i = 0; i < tokens.length; i += 1) {
9
+ const token = tokens[i];
10
+ const nextToken = tokens[i + 1];
11
+ if (token === "--json") {
12
+ json = true;
13
+ continue;
14
+ }
15
+ if (token === "--quiet") {
16
+ quiet = true;
17
+ continue;
18
+ }
19
+ if (token === "--reason") {
20
+ if (!nextToken || nextToken.startsWith("--")) {
21
+ throw new Error("--reason requires a short slug value.");
22
+ }
23
+ reason = nextToken.trim();
24
+ i += 1;
25
+ continue;
26
+ }
27
+ if (token.startsWith("--reason=")) {
28
+ reason = token.slice("--reason=".length).trim();
29
+ continue;
30
+ }
31
+ throw new Error(`Unknown flag for internal flow-state-repair: ${token}`);
32
+ }
33
+ if (!reason || reason.length === 0) {
34
+ throw new Error("internal flow-state-repair requires --reason=<slug> (e.g. --reason=manual_edit_recovery).");
35
+ }
36
+ return { reason, json, quiet };
37
+ }
38
+ export async function runFlowStateRepair(projectRoot, args, io) {
39
+ const result = await repairFlowStateGuard(projectRoot, args.reason);
40
+ const logRel = path.relative(projectRoot, result.repairLogPath).replace(/\\/gu, "/");
41
+ const guardRel = path.relative(projectRoot, result.guardPath).replace(/\\/gu, "/");
42
+ if (args.json) {
43
+ io.stdout.write(`${JSON.stringify({
44
+ ok: true,
45
+ command: "flow-state-repair",
46
+ reason: args.reason,
47
+ sidecar: result.sidecar,
48
+ guardPath: guardRel,
49
+ repairLogPath: logRel,
50
+ runtimeRoot: RUNTIME_ROOT
51
+ })}\n`);
52
+ return 0;
53
+ }
54
+ if (!args.quiet) {
55
+ io.stdout.write(`${JSON.stringify({
56
+ ok: true,
57
+ command: "flow-state-repair",
58
+ reason: args.reason,
59
+ sidecar: result.sidecar,
60
+ guardPath: guardRel,
61
+ repairLogPath: logRel
62
+ }, null, 2)}\n`);
63
+ }
64
+ return 0;
65
+ }
@@ -0,0 +1,62 @@
1
+ import { type FlowStage } from "../types.js";
2
+ import type { Writable } from "node:stream";
3
+ interface InternalIo {
4
+ stdout: Writable;
5
+ stderr: Writable;
6
+ }
7
+ export declare const WAIVER_TOKEN_DEFAULT_TTL_MINUTES = 30;
8
+ export declare const WAIVER_TOKEN_MAX_TTL_MINUTES = 120;
9
+ export declare const WAIVER_REASON_PATTERN: RegExp;
10
+ export interface WaiverRecord {
11
+ token: string;
12
+ stage: FlowStage;
13
+ reason: string;
14
+ issuedAt: string;
15
+ expiresAt: string;
16
+ consumedAt: string | null;
17
+ issuerSubsystem: string;
18
+ consumedBy?: string;
19
+ }
20
+ export interface WaiverLedger {
21
+ schemaVersion: number;
22
+ pending: WaiverRecord[];
23
+ consumed: WaiverRecord[];
24
+ }
25
+ export interface IssueWaiverTokenOptions {
26
+ stage: FlowStage;
27
+ reason: string;
28
+ expiresInMinutes?: number;
29
+ issuerSubsystem?: string;
30
+ now?: Date;
31
+ }
32
+ export interface ConsumeWaiverOptions {
33
+ stage: FlowStage;
34
+ token: string;
35
+ consumedBy?: string;
36
+ now?: Date;
37
+ }
38
+ export declare function formatWaiverToken(stage: FlowStage, fingerprint: string, expiresAt: Date): string;
39
+ export declare function issueWaiverToken(projectRoot: string, options: IssueWaiverTokenOptions): Promise<WaiverRecord>;
40
+ export type ConsumeWaiverFailureReason = "not-found" | "wrong-stage" | "expired" | "already-consumed";
41
+ export interface ConsumeWaiverSuccess {
42
+ ok: true;
43
+ record: WaiverRecord;
44
+ }
45
+ export interface ConsumeWaiverFailure {
46
+ ok: false;
47
+ reason: ConsumeWaiverFailureReason;
48
+ record?: WaiverRecord;
49
+ detail: string;
50
+ }
51
+ export type ConsumeWaiverResult = ConsumeWaiverSuccess | ConsumeWaiverFailure;
52
+ export declare function consumeWaiverToken(projectRoot: string, options: ConsumeWaiverOptions): Promise<ConsumeWaiverResult>;
53
+ export interface WaiverGrantArgs {
54
+ stage: FlowStage;
55
+ reason: string;
56
+ ttlMinutes: number;
57
+ json: boolean;
58
+ quiet: boolean;
59
+ }
60
+ export declare function parseWaiverGrantArgs(tokens: string[]): WaiverGrantArgs;
61
+ export declare function runWaiverGrant(projectRoot: string, args: WaiverGrantArgs, io: InternalIo): Promise<number>;
62
+ export {};