cclaw-cli 6.0.0 → 6.1.1

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.
@@ -327,7 +327,9 @@ export async function readDelegationLedger(projectRoot) {
327
327
  * don't show up as corrupt lines.
328
328
  */
329
329
  const NON_DELEGATION_AUDIT_EVENTS = new Set([
330
- "mandatory_delegations_skipped_by_track"
330
+ "mandatory_delegations_skipped_by_track",
331
+ "artifact_validation_demoted_by_track",
332
+ "expansion_strategist_skipped_by_track"
331
333
  ]);
332
334
  function isAuditEventLine(parsed) {
333
335
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
@@ -471,14 +473,22 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
471
473
  const flowState = await readFlowState(projectRoot, {
472
474
  repairFeatureSystem: options.repairFeatureSystem
473
475
  });
474
- const mandatory = mandatoryAgentsFor(stage, flowState.track, options.taskClass ?? null);
476
+ // Wave 24 follow-up (v6.1.1): read `flowState.taskClass` as a fallback
477
+ // when the caller doesn't pass an explicit override. The
478
+ // `cclaw advance-stage` path (`buildValidationReport` →
479
+ // `checkMandatoryDelegations`) never forwarded `taskClass`, which left
480
+ // the `software-bugfix` skip dead for users who classified their run
481
+ // via `flow-state.json`. Forward-typed `null` callers still suppress
482
+ // the lookup explicitly; only `undefined` triggers the fallback.
483
+ const resolvedTaskClass = options.taskClass !== undefined ? options.taskClass : flowState.taskClass ?? null;
484
+ const mandatory = mandatoryAgentsFor(stage, flowState.track, resolvedTaskClass);
475
485
  const skippedByTrack = mandatory.length === 0 &&
476
486
  stageSchema(stage, flowState.track).mandatoryDelegations.length > 0;
477
487
  if (skippedByTrack) {
478
488
  await recordMandatorySkippedByTrack(projectRoot, {
479
489
  stage,
480
490
  track: flowState.track,
481
- taskClass: options.taskClass ?? null,
491
+ taskClass: resolvedTaskClass,
482
492
  runId: flowState.activeRunId
483
493
  });
484
494
  }
@@ -617,3 +627,65 @@ async function recordMandatorySkippedByTrack(projectRoot, params) {
617
627
  // best-effort audit; never block stage advance.
618
628
  }
619
629
  }
630
+ /**
631
+ * Wave 25 (v6.1.0) — append a non-delegation audit event recording
632
+ * that one or more required artifact-validation findings were
633
+ * demoted from blocking to advisory because the active run is on a
634
+ * small-fix lane (`track === "quick"` or `taskClass === "software-bugfix"`).
635
+ *
636
+ * The event mirrors the Wave 24 `mandatory_delegations_skipped_by_track`
637
+ * audit pattern: best-effort write to `delegation-events.jsonl`, no
638
+ * agent payload, recognized by `readDelegationEvents` so it does not
639
+ * corrupt downstream parsers. Failures are swallowed.
640
+ */
641
+ export async function recordArtifactValidationDemotedByTrack(projectRoot, params) {
642
+ if (params.sections.length === 0)
643
+ return;
644
+ const eventsPath = delegationEventsPath(projectRoot);
645
+ const payload = {
646
+ event: "artifact_validation_demoted_by_track",
647
+ stage: params.stage,
648
+ track: params.track,
649
+ taskClass: params.taskClass,
650
+ runId: params.runId,
651
+ sections: params.sections,
652
+ ts: new Date().toISOString()
653
+ };
654
+ try {
655
+ await fs.mkdir(path.dirname(eventsPath), { recursive: true });
656
+ await fs.appendFile(eventsPath, `${JSON.stringify(payload)}\n`, "utf8");
657
+ }
658
+ catch {
659
+ // best-effort audit; never block stage advance.
660
+ }
661
+ }
662
+ /**
663
+ * Wave 25 (v6.1.0) — append a non-delegation audit event recording
664
+ * that the scope-stage Expansion Strategist (`product-discovery`)
665
+ * delegation requirement was skipped because the active run is on a
666
+ * small-fix lane (`track === "quick"` or `taskClass === "software-bugfix"`).
667
+ *
668
+ * Mirrors the Wave 24 `mandatory_delegations_skipped_by_track`
669
+ * audit pattern: best-effort write to `delegation-events.jsonl`, no
670
+ * agent payload, recognized by `readDelegationEvents` so it does not
671
+ * corrupt downstream parsers. Failures are swallowed.
672
+ */
673
+ export async function recordExpansionStrategistSkippedByTrack(projectRoot, params) {
674
+ const eventsPath = delegationEventsPath(projectRoot);
675
+ const payload = {
676
+ event: "expansion_strategist_skipped_by_track",
677
+ stage: "scope",
678
+ track: params.track,
679
+ taskClass: params.taskClass,
680
+ runId: params.runId,
681
+ selectedScopeMode: params.selectedScopeMode,
682
+ ts: new Date().toISOString()
683
+ };
684
+ try {
685
+ await fs.mkdir(path.dirname(eventsPath), { recursive: true });
686
+ await fs.appendFile(eventsPath, `${JSON.stringify(payload)}\n`, "utf8");
687
+ }
688
+ catch {
689
+ // best-effort audit; never block stage advance.
690
+ }
691
+ }
@@ -76,6 +76,20 @@ export interface FlowState {
76
76
  stageGateCatalog: Record<FlowStage, StageGateState>;
77
77
  /** Active flow track (determines which stages are in the critical path for this run). */
78
78
  track: FlowTrack;
79
+ /**
80
+ * Wave 25 (v6.1.0) — optional task class for the active run.
81
+ *
82
+ * Mirrors the `MandatoryDelegationTaskClass` union used by Wave 24's
83
+ * `mandatoryAgentsFor` helper. When set to `"software-bugfix"`, the
84
+ * artifact-validation escape (`shouldDemoteArtifactValidationByTrack`)
85
+ * collapses lite-tier-only checks (Architecture Diagram async/failure
86
+ * edges, Interaction Edge Case mandatory rows, Stale Diagram Drift,
87
+ * Expansion Strategist) from required → advisory.
88
+ *
89
+ * Persistence is best-effort: existing flow-state.json files written
90
+ * before Wave 25 simply omit the field (treated as `null`).
91
+ */
92
+ taskClass?: "software-standard" | "software-trivial" | "software-bugfix" | null;
79
93
  /** Stages explicitly skipped for this track (empty for standard; populated for quick). */
80
94
  skippedStages: FlowStage[];
81
95
  /** Stages invalidated by rewind operations and awaiting explicit acknowledgement. */
@@ -34,7 +34,43 @@ interface InternalValidationReport {
34
34
  issues: string[];
35
35
  };
36
36
  }
37
+ /**
38
+ * Wave 24 entry point — auto-hydrate evidence for an auto-hydratable
39
+ * gate that the agent already included in --passed but for which they
40
+ * forgot to provide --evidence-json. Returns silently when no
41
+ * hydration is possible (no auto-hydratable gate, no artifact, no
42
+ * envelope, etc.).
43
+ *
44
+ * Wave 25 (v6.1.0) layered `tryAutoHydrateAndSelectReviewLoopGate` on
45
+ * top of this so the gate is also auto-included in selectedGateIds
46
+ * when the artifact yields a valid envelope. Together the two helpers
47
+ * remove the contradiction the user reported in Wave 24:
48
+ * - "omit this gate from --evidence-json so stage-complete can
49
+ * auto-hydrate it" → "missing --evidence-json entries for passed
50
+ * gates: design_diagram_freshness".
51
+ */
37
52
  export declare function hydrateReviewLoopEvidenceFromArtifact(projectRoot: string, stage: FlowStage, track: FlowState["track"], selectedGateIds: string[], evidenceByGate: Record<string, string>): Promise<void>;
53
+ /**
54
+ * Wave 25 (v6.1.0) — auto-include an auto-hydratable review-loop gate
55
+ * in `selectedGateIds` when:
56
+ * - The stage has an auto-hydratable gate registered via
57
+ * `AUTO_REVIEW_LOOP_GATE_BY_STAGE` (currently `design`).
58
+ * - The artifact yields a valid review-loop envelope via
59
+ * `extractReviewLoopEnvelopeFromArtifact`.
60
+ * - The gate is required for the active track.
61
+ * - The agent has NOT passed the gate yet (so we don't double-add).
62
+ *
63
+ * Returns the (possibly extended) array of selected gate IDs and
64
+ * mutates `evidenceByGate` to include the hydrated envelope.
65
+ *
66
+ * Together with `hydrateReviewLoopEvidenceFromArtifact` this makes the
67
+ * flow consistent: if the artifact contains the envelope, the agent
68
+ * neither has to include the gate in --passed nor pass --evidence-json
69
+ * for it. If the artifact does NOT contain the envelope, the agent
70
+ * gets a clear error pointing at the artifact section to add (via
71
+ * `reviewLoopArtifactFixHint`).
72
+ */
73
+ export declare function tryAutoHydrateAndSelectReviewLoopGate(projectRoot: string, stage: FlowStage, track: FlowState["track"], requiredGateIds: string[], selectedGateIds: string[], evidenceByGate: Record<string, string>): Promise<string[]>;
38
74
  export declare function buildValidationReport(projectRoot: string, flowState: FlowState, options?: {
39
75
  allowBlockedReviewRoute?: boolean;
40
76
  extraStageFlags?: string[];
@@ -10,7 +10,7 @@ import { readFlowState, writeFlowState } from "../../runs.js";
10
10
  import { stageSchema } from "../../content/stage-schema.js";
11
11
  import { extractReviewLoopEnvelopeFromArtifact } from "../../content/review-loop.js";
12
12
  import { unique } from "./helpers.js";
13
- import { AUTO_REVIEW_LOOP_GATE_BY_STAGE, reviewLoopArtifactFixHint, validateGateEvidenceShape } from "./review-loop.js";
13
+ import { AUTO_REVIEW_LOOP_GATE_BY_STAGE, reviewLoopArtifactFixHint, reviewLoopEnvelopeExample, validateGateEvidenceShape } from "./review-loop.js";
14
14
  import { ensureProactiveDelegationTrace } from "./verify.js";
15
15
  function resolveSuccessorTransition(stage, track, transitionTargets, satisfiedGuards, selectedTransitionGuards) {
16
16
  const natural = transitionTargets[0] ?? null;
@@ -57,6 +57,21 @@ function nextInteractionHints(flowState, args, successor) {
57
57
  }
58
58
  return hints;
59
59
  }
60
+ /**
61
+ * Wave 24 entry point — auto-hydrate evidence for an auto-hydratable
62
+ * gate that the agent already included in --passed but for which they
63
+ * forgot to provide --evidence-json. Returns silently when no
64
+ * hydration is possible (no auto-hydratable gate, no artifact, no
65
+ * envelope, etc.).
66
+ *
67
+ * Wave 25 (v6.1.0) layered `tryAutoHydrateAndSelectReviewLoopGate` on
68
+ * top of this so the gate is also auto-included in selectedGateIds
69
+ * when the artifact yields a valid envelope. Together the two helpers
70
+ * remove the contradiction the user reported in Wave 24:
71
+ * - "omit this gate from --evidence-json so stage-complete can
72
+ * auto-hydrate it" → "missing --evidence-json entries for passed
73
+ * gates: design_diagram_freshness".
74
+ */
60
75
  export async function hydrateReviewLoopEvidenceFromArtifact(projectRoot, stage, track, selectedGateIds, evidenceByGate) {
61
76
  const gateId = AUTO_REVIEW_LOOP_GATE_BY_STAGE[stage];
62
77
  if (!gateId)
@@ -87,8 +102,73 @@ export async function hydrateReviewLoopEvidenceFromArtifact(projectRoot, stage,
87
102
  return;
88
103
  evidenceByGate[gateId] = JSON.stringify(envelope);
89
104
  }
105
+ /**
106
+ * Wave 25 (v6.1.0) — auto-include an auto-hydratable review-loop gate
107
+ * in `selectedGateIds` when:
108
+ * - The stage has an auto-hydratable gate registered via
109
+ * `AUTO_REVIEW_LOOP_GATE_BY_STAGE` (currently `design`).
110
+ * - The artifact yields a valid review-loop envelope via
111
+ * `extractReviewLoopEnvelopeFromArtifact`.
112
+ * - The gate is required for the active track.
113
+ * - The agent has NOT passed the gate yet (so we don't double-add).
114
+ *
115
+ * Returns the (possibly extended) array of selected gate IDs and
116
+ * mutates `evidenceByGate` to include the hydrated envelope.
117
+ *
118
+ * Together with `hydrateReviewLoopEvidenceFromArtifact` this makes the
119
+ * flow consistent: if the artifact contains the envelope, the agent
120
+ * neither has to include the gate in --passed nor pass --evidence-json
121
+ * for it. If the artifact does NOT contain the envelope, the agent
122
+ * gets a clear error pointing at the artifact section to add (via
123
+ * `reviewLoopArtifactFixHint`).
124
+ */
125
+ export async function tryAutoHydrateAndSelectReviewLoopGate(projectRoot, stage, track, requiredGateIds, selectedGateIds, evidenceByGate) {
126
+ const gateId = AUTO_REVIEW_LOOP_GATE_BY_STAGE[stage];
127
+ if (!gateId)
128
+ return selectedGateIds;
129
+ const reviewStage = stage === "scope" || stage === "design" ? stage : null;
130
+ if (!reviewStage)
131
+ return selectedGateIds;
132
+ if (!requiredGateIds.includes(gateId))
133
+ return selectedGateIds;
134
+ if (selectedGateIds.includes(gateId)) {
135
+ // Already selected — fall through to the existing hydration helper
136
+ // for the manual --evidence-json path.
137
+ await hydrateReviewLoopEvidenceFromArtifact(projectRoot, stage, track, selectedGateIds, evidenceByGate);
138
+ return selectedGateIds;
139
+ }
140
+ const existing = evidenceByGate[gateId];
141
+ if (typeof existing === "string" && existing.trim().length > 0) {
142
+ return [...selectedGateIds, gateId];
143
+ }
144
+ const resolved = await resolveArtifactPath(stage, {
145
+ projectRoot,
146
+ track,
147
+ intent: "read"
148
+ });
149
+ let raw = "";
150
+ try {
151
+ raw = await fs.readFile(resolved.absPath, "utf8");
152
+ }
153
+ catch {
154
+ return selectedGateIds;
155
+ }
156
+ const envelope = extractReviewLoopEnvelopeFromArtifact(raw, reviewStage, resolved.relPath);
157
+ if (!envelope)
158
+ return selectedGateIds;
159
+ evidenceByGate[gateId] = JSON.stringify(envelope);
160
+ return [...selectedGateIds, gateId];
161
+ }
90
162
  export async function buildValidationReport(projectRoot, flowState, options = {}) {
91
- const delegation = await checkMandatoryDelegations(projectRoot, flowState.currentStage);
163
+ // Wave 24 follow-up (v6.1.1): forward `flowState.taskClass` so the
164
+ // bugfix-skip lights up via the `cclaw advance-stage` path. The
165
+ // delegation helper now has its own fallback (it reads `flowState`
166
+ // internally), but threading the value here keeps the call site
167
+ // self-documenting and survives any future refactor that drops the
168
+ // implicit fallback.
169
+ const delegation = await checkMandatoryDelegations(projectRoot, flowState.currentStage, {
170
+ taskClass: flowState.taskClass ?? undefined
171
+ });
92
172
  const gates = await verifyCurrentStageGateEvidence(projectRoot, flowState, {
93
173
  extraStageFlags: options.extraStageFlags
94
174
  });
@@ -229,9 +309,17 @@ export async function runAdvanceStage(projectRoot, args, io) {
229
309
  .flatMap((target) => getTransitionGuards(args.stage, target, flowState.track))
230
310
  .filter((guardId) => !allowedGateIds.has(guardId)));
231
311
  const selectableGateIds = new Set([...allowedGateIds, ...transitionGuardIds]);
232
- const selectedGateIds = args.passedGateIds.length > 0
312
+ let selectedGateIds = args.passedGateIds.length > 0
233
313
  ? args.passedGateIds.filter((gateId) => selectableGateIds.has(gateId))
234
314
  : requiredGateIds;
315
+ // Wave 25 (v6.1.0): if the active stage has an auto-hydratable
316
+ // review-loop gate (currently `design.design_architecture_locked`)
317
+ // and the artifact already contains a valid review-loop envelope,
318
+ // include the gate in selectedGateIds and hydrate evidence in one
319
+ // step. This removes the Wave 24 contradiction between "omit from
320
+ // --evidence-json so we can auto-hydrate" and "missing
321
+ // --evidence-json entries for passed gates".
322
+ selectedGateIds = await tryAutoHydrateAndSelectReviewLoopGate(projectRoot, args.stage, flowState.track, requiredGateIds, selectedGateIds, args.evidenceByGate);
235
323
  const selectedGateIdSet = new Set(selectedGateIds);
236
324
  const selectedTransitionGuards = selectedGateIds.filter((gateId) => transitionGuardIds.has(gateId));
237
325
  const blockedReviewRoute = args.stage === "review" && selectedGateIdSet.has("review_verdict_blocked");
@@ -240,7 +328,11 @@ export async function runAdvanceStage(projectRoot, args, io) {
240
328
  : requiredGateIds;
241
329
  const missingRequired = requiredForSelectedRoute.filter((gateId) => !selectedGateIdSet.has(gateId));
242
330
  if (missingRequired.length > 0) {
243
- io.stderr.write(`cclaw internal advance-stage: required gates not selected as passed: ${missingRequired.join(", ")}.\n`);
331
+ const autoHydrateGate = AUTO_REVIEW_LOOP_GATE_BY_STAGE[args.stage];
332
+ const autoHydrateHint = autoHydrateGate && missingRequired.includes(autoHydrateGate) && (args.stage === "scope" || args.stage === "design")
333
+ ? ` Auto-hydratable gate "${autoHydrateGate}" was NOT auto-included because the design artifact is missing the review-loop envelope. Add a \`## ${args.stage === "scope" ? "Scope Outside Voice Loop" : "Design Outside Voice Loop"}\` table (example envelope: ${reviewLoopEnvelopeExample(args.stage)}), or pass --evidence-json='{"${autoHydrateGate}": "<envelope-json>"}' alongside --passed=...,${autoHydrateGate}.`
334
+ : "";
335
+ io.stderr.write(`cclaw internal advance-stage: required gates not selected as passed: ${missingRequired.join(", ")}.${autoHydrateHint}\n`);
244
336
  return 1;
245
337
  }
246
338
  const mandatory = new Set(schema.mandatoryDelegations);
@@ -269,7 +361,10 @@ export async function runAdvanceStage(projectRoot, args, io) {
269
361
  });
270
362
  }
271
363
  }
272
- await hydrateReviewLoopEvidenceFromArtifact(projectRoot, args.stage, flowState.track, selectedGateIds, args.evidenceByGate);
364
+ // Wave 25 (v6.1.0): hydration + auto-select happens earlier via
365
+ // `tryAutoHydrateAndSelectReviewLoopGate`. The previous explicit
366
+ // call here was redundant (helper already covered both the
367
+ // already-selected and not-yet-selected paths).
273
368
  const catalog = flowState.stageGateCatalog[args.stage];
274
369
  const nextPassed = unique([...catalog.passed, ...selectedGateIds]).filter((gateId) => allowedGateIds.has(gateId));
275
370
  const nextPassedSet = new Set(nextPassed);
@@ -1,5 +1,14 @@
1
1
  import type { FlowStage } from "../../types.js";
2
2
  export declare const AUTO_REVIEW_LOOP_GATE_BY_STAGE: Partial<Record<FlowStage, string>>;
3
+ /**
4
+ * Wave 25 (v6.1.0) — exact JSON shape that gate-evidence validators
5
+ * accept for a review-loop envelope. The error messages emitted by
6
+ * `validateReviewLoopGateEvidence` always include this example so the
7
+ * agent never has to guess where `stage` lives (top-level of the
8
+ * envelope, NOT inside `payload`). Keep `stage`/`targetScore`/etc. in
9
+ * the order shown so a copy-paste from the error survives.
10
+ */
11
+ export declare function reviewLoopEnvelopeExample(stage: "scope" | "design"): string;
3
12
  export declare function pickReviewLoopEnvelope(value: unknown): Record<string, unknown> | null;
4
13
  export declare function validateReviewLoopGateEvidence(stage: "scope" | "design", evidence: string): string | null;
5
14
  export declare function validateUserApprovalEvidence(evidence: string): string | null;
@@ -11,6 +11,29 @@ const REVIEW_LOOP_STOP_REASONS = new Set([
11
11
  "max_iterations_reached",
12
12
  "user_opt_out"
13
13
  ]);
14
+ /**
15
+ * Wave 25 (v6.1.0) — exact JSON shape that gate-evidence validators
16
+ * accept for a review-loop envelope. The error messages emitted by
17
+ * `validateReviewLoopGateEvidence` always include this example so the
18
+ * agent never has to guess where `stage` lives (top-level of the
19
+ * envelope, NOT inside `payload`). Keep `stage`/`targetScore`/etc. in
20
+ * the order shown so a copy-paste from the error survives.
21
+ */
22
+ export function reviewLoopEnvelopeExample(stage) {
23
+ return JSON.stringify({
24
+ type: "review-loop",
25
+ stage,
26
+ targetScore: 0.8,
27
+ maxIterations: 3,
28
+ stopReason: "quality_threshold_met",
29
+ iterations: [{ iteration: 1, qualityScore: 0.8, findingsCount: 0 }]
30
+ });
31
+ }
32
+ function reviewLoopEnvelopeShapeHint(stage) {
33
+ return (`Expected envelope: ${reviewLoopEnvelopeExample(stage)}` +
34
+ " (top-level keys: type, stage, targetScore, maxIterations, stopReason, iterations[]). " +
35
+ "Stage MUST be at the top level — not inside payload.");
36
+ }
14
37
  export function pickReviewLoopEnvelope(value) {
15
38
  const direct = asRecord(value);
16
39
  if (!direct)
@@ -31,14 +54,17 @@ export function validateReviewLoopGateEvidence(stage, evidence) {
31
54
  parsed = JSON.parse(evidence);
32
55
  }
33
56
  catch {
34
- return "must be JSON containing a review-loop envelope (`type: \"review-loop\"`) in top-level, `payload`, or `reviewLoop`.";
57
+ return ("must be JSON containing a review-loop envelope (`type: \"review-loop\"`) in top-level, `payload`, or `reviewLoop`. " +
58
+ reviewLoopEnvelopeShapeHint(stage));
35
59
  }
36
60
  const envelope = pickReviewLoopEnvelope(parsed);
37
61
  if (!envelope) {
38
- return "must include a review-loop envelope (`type: \"review-loop\"`) in top-level, `payload`, or `reviewLoop`.";
62
+ return ("must include a review-loop envelope (`type: \"review-loop\"`) in top-level, `payload`, or `reviewLoop`. " +
63
+ reviewLoopEnvelopeShapeHint(stage));
39
64
  }
40
65
  if (envelope.stage !== stage) {
41
- return `review-loop envelope stage must be "${stage}".`;
66
+ return (`review-loop envelope stage must be "${stage}" at the top level of the envelope, not inside payload. ` +
67
+ reviewLoopEnvelopeShapeHint(stage));
42
68
  }
43
69
  const targetScore = envelope.targetScore;
44
70
  if (typeof targetScore !== "number" || Number.isNaN(targetScore) || targetScore < 0 || targetScore > 1) {
@@ -157,5 +183,17 @@ export async function validateGateEvidenceShape(projectRoot, stage, gateId, evid
157
183
  export function reviewLoopArtifactFixHint(stage, gateId) {
158
184
  if (AUTO_REVIEW_LOOP_GATE_BY_STAGE[stage] !== gateId)
159
185
  return "";
160
- return ` Add a \`## ${stage === "scope" ? "Scope Outside Voice Loop" : "Design Outside Voice Loop"}\` table to the artifact with rows like \`| 1 | 0.80 | 0 |\` plus \`- Stop reason: quality_threshold_met\`, \`- Target score: 0.80\`, and \`- Max iterations: 3\`; then omit this gate from manual evidence so stage-complete can auto-hydrate it.`;
186
+ // Wave 25 (v6.1.0): the consistent flow is "include the gate in
187
+ // --passed AND let stage-complete auto-hydrate evidence from the
188
+ // artifact". Wave 24's hint told agents to omit the gate from
189
+ // --evidence-json, but they then hit
190
+ // `missing --evidence-json entries for passed gates: <gateId>`
191
+ // because hydration only runs when --evidence-json is also present
192
+ // OR when an artifact section yields a parseable envelope. The new
193
+ // hint tells the agent to:
194
+ // 1. Add the artifact section (so hydration succeeds), AND
195
+ // 2. Include the gate in --passed.
196
+ // No --evidence-json entry is required in that case.
197
+ const stageReviewSection = stage === "scope" ? "Scope Outside Voice Loop" : "Design Outside Voice Loop";
198
+ return (` Fix in two steps: (1) Add a \`## ${stageReviewSection}\` table to the artifact with rows like \`| 1 | 0.80 | 0 |\` plus \`- Stop reason: quality_threshold_met\`, \`- Target score: 0.80\`, and \`- Max iterations: 3\`. (2) Re-run \`stage-complete ${stage} --passed=...,${gateId},...\` — stage-complete will auto-hydrate the envelope from the artifact, so you do NOT need to pass --evidence-json for ${gateId}.`);
161
199
  }
@@ -159,6 +159,29 @@ function sanitizeStageGateCatalog(value, fallback) {
159
159
  function coerceTrack(value) {
160
160
  return isFlowTrack(value) ? value : "standard";
161
161
  }
162
+ /**
163
+ * Wave 24 follow-up (v6.1.1) — preserve `flow-state.json#taskClass`
164
+ * across read/write round-trips. Before this audit fix the persistence
165
+ * layer silently dropped the field, which made the Wave 24 bugfix-skip
166
+ * (`mandatoryAgentsFor` short-circuit) and the Wave 25 artifact-validation
167
+ * demotion both dead in practice: the only entry point that classified
168
+ * a run was the unit-test harness passing `options.taskClass` directly
169
+ * to `checkMandatoryDelegations`. The accepted union mirrors
170
+ * `MandatoryDelegationTaskClass` plus `null` so callers can explicitly
171
+ * clear the classification without dropping the property.
172
+ */
173
+ function coerceTaskClass(value) {
174
+ if (value === undefined)
175
+ return undefined;
176
+ if (value === null)
177
+ return null;
178
+ if (value === "software-standard" ||
179
+ value === "software-trivial" ||
180
+ value === "software-bugfix") {
181
+ return value;
182
+ }
183
+ return undefined;
184
+ }
162
185
  function sanitizeSkippedStages(value, track) {
163
186
  const trackDefault = skippedStagesForTrack(track);
164
187
  if (!Array.isArray(value)) {
@@ -354,6 +377,7 @@ function coerceFlowState(parsed) {
354
377
  const activeRunId = typeof activeRunIdRaw === "string" && activeRunIdRaw.trim().length > 0
355
378
  ? activeRunIdRaw.trim()
356
379
  : next.activeRunId;
380
+ const taskClass = coerceTaskClass(parsed.taskClass);
357
381
  const state = {
358
382
  schemaVersion: FLOW_STATE_SCHEMA_VERSION,
359
383
  activeRunId,
@@ -362,6 +386,7 @@ function coerceFlowState(parsed) {
362
386
  guardEvidence: sanitizeGuardEvidence(parsed.guardEvidence),
363
387
  stageGateCatalog: sanitizeStageGateCatalog(parsed.stageGateCatalog, next.stageGateCatalog),
364
388
  track,
389
+ ...(taskClass !== undefined ? { taskClass } : {}),
365
390
  skippedStages: sanitizeSkippedStages(parsed.skippedStages, track),
366
391
  staleStages: sanitizeStaleStages(parsed.staleStages),
367
392
  rewinds: sanitizeRewinds(parsed.rewinds),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "6.0.0",
3
+ "version": "6.1.1",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {