cclaw-cli 5.0.0 → 6.1.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.
@@ -7,7 +7,7 @@ import { readConfig } from "./config.js";
7
7
  import { exists, withDirectoryLock, writeFileSafe } from "./fs-utils.js";
8
8
  import { HARNESS_ADAPTERS } from "./harness-adapters.js";
9
9
  import { readFlowState } from "./runs.js";
10
- import { stageSchema } from "./content/stage-schema.js";
10
+ import { mandatoryAgentsFor, stageSchema } from "./content/stage-schema.js";
11
11
  const execFileAsync = promisify(execFile);
12
12
  const TERMINAL_DELEGATION_STATUSES = new Set(["completed", "failed", "waived", "stale"]);
13
13
  export const DELEGATION_DISPATCH_SURFACES = [
@@ -320,6 +320,23 @@ export async function readDelegationLedger(projectRoot) {
320
320
  return { runId: activeRunId, entries: [] };
321
321
  }
322
322
  }
323
+ /**
324
+ * Wave 24 (v6.0.0) audit-only event types that live in
325
+ * `delegation-events.jsonl` but do NOT carry a delegation lifecycle
326
+ * payload (no agent/spanId). The parser must accept them so they
327
+ * don't show up as corrupt lines.
328
+ */
329
+ const NON_DELEGATION_AUDIT_EVENTS = new Set([
330
+ "mandatory_delegations_skipped_by_track",
331
+ "artifact_validation_demoted_by_track",
332
+ "expansion_strategist_skipped_by_track"
333
+ ]);
334
+ function isAuditEventLine(parsed) {
335
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
336
+ return false;
337
+ const evt = parsed.event;
338
+ return typeof evt === "string" && NON_DELEGATION_AUDIT_EVENTS.has(evt);
339
+ }
323
340
  export async function readDelegationEvents(projectRoot) {
324
341
  const filePath = delegationEventsPath(projectRoot);
325
342
  if (!(await exists(filePath))) {
@@ -338,6 +355,11 @@ export async function readDelegationEvents(projectRoot) {
338
355
  if (isDelegationEvent(parsed)) {
339
356
  events.push(parsed);
340
357
  }
358
+ else if (isAuditEventLine(parsed)) {
359
+ // Wave 24 audit-only row (e.g. mandatory_delegations_skipped_by_track).
360
+ // Not a delegation lifecycle event but valid audit content.
361
+ continue;
362
+ }
341
363
  else {
342
364
  corruptLines.push(index + 1);
343
365
  }
@@ -451,7 +473,17 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
451
473
  const flowState = await readFlowState(projectRoot, {
452
474
  repairFeatureSystem: options.repairFeatureSystem
453
475
  });
454
- const mandatory = stageSchema(stage, flowState.track).mandatoryDelegations;
476
+ const mandatory = mandatoryAgentsFor(stage, flowState.track, options.taskClass ?? null);
477
+ const skippedByTrack = mandatory.length === 0 &&
478
+ stageSchema(stage, flowState.track).mandatoryDelegations.length > 0;
479
+ if (skippedByTrack) {
480
+ await recordMandatorySkippedByTrack(projectRoot, {
481
+ stage,
482
+ track: flowState.track,
483
+ taskClass: options.taskClass ?? null,
484
+ runId: flowState.activeRunId
485
+ });
486
+ }
455
487
  const { activeRunId } = flowState;
456
488
  const ledger = await readDelegationLedger(projectRoot);
457
489
  const events = await readDelegationEvents(projectRoot);
@@ -553,6 +585,99 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
553
585
  legacyInferredCompletions,
554
586
  corruptEventLines: events.corruptLines,
555
587
  staleWorkers,
556
- expectedMode
588
+ expectedMode,
589
+ skippedByTrack
590
+ };
591
+ }
592
+ /**
593
+ * Wave 24 (v6.0.0) — append a non-delegation audit event to
594
+ * `delegation-events.jsonl` recording that the mandatory delegation
595
+ * gate was skipped because of the active track / task class. Plays the
596
+ * same audit role as a `waived` row but does NOT carry an agent —
597
+ * downstream tooling treats `event === "mandatory_delegations_skipped_by_track"`
598
+ * lines as informational.
599
+ *
600
+ * Failures are swallowed: the audit log is best-effort. Missing the
601
+ * event must never block stage advance because the gate skip itself is
602
+ * authoritative.
603
+ */
604
+ async function recordMandatorySkippedByTrack(projectRoot, params) {
605
+ const eventsPath = delegationEventsPath(projectRoot);
606
+ const payload = {
607
+ event: "mandatory_delegations_skipped_by_track",
608
+ stage: params.stage,
609
+ track: params.track,
610
+ taskClass: params.taskClass,
611
+ runId: params.runId,
612
+ ts: new Date().toISOString()
557
613
  };
614
+ try {
615
+ await fs.mkdir(path.dirname(eventsPath), { recursive: true });
616
+ await fs.appendFile(eventsPath, `${JSON.stringify(payload)}\n`, "utf8");
617
+ }
618
+ catch {
619
+ // best-effort audit; never block stage advance.
620
+ }
621
+ }
622
+ /**
623
+ * Wave 25 (v6.1.0) — append a non-delegation audit event recording
624
+ * that one or more required artifact-validation findings were
625
+ * demoted from blocking to advisory because the active run is on a
626
+ * small-fix lane (`track === "quick"` or `taskClass === "software-bugfix"`).
627
+ *
628
+ * The event mirrors the Wave 24 `mandatory_delegations_skipped_by_track`
629
+ * audit pattern: best-effort write to `delegation-events.jsonl`, no
630
+ * agent payload, recognized by `readDelegationEvents` so it does not
631
+ * corrupt downstream parsers. Failures are swallowed.
632
+ */
633
+ export async function recordArtifactValidationDemotedByTrack(projectRoot, params) {
634
+ if (params.sections.length === 0)
635
+ return;
636
+ const eventsPath = delegationEventsPath(projectRoot);
637
+ const payload = {
638
+ event: "artifact_validation_demoted_by_track",
639
+ stage: params.stage,
640
+ track: params.track,
641
+ taskClass: params.taskClass,
642
+ runId: params.runId,
643
+ sections: params.sections,
644
+ ts: new Date().toISOString()
645
+ };
646
+ try {
647
+ await fs.mkdir(path.dirname(eventsPath), { recursive: true });
648
+ await fs.appendFile(eventsPath, `${JSON.stringify(payload)}\n`, "utf8");
649
+ }
650
+ catch {
651
+ // best-effort audit; never block stage advance.
652
+ }
653
+ }
654
+ /**
655
+ * Wave 25 (v6.1.0) — append a non-delegation audit event recording
656
+ * that the scope-stage Expansion Strategist (`product-discovery`)
657
+ * delegation requirement was skipped because the active run is on a
658
+ * small-fix lane (`track === "quick"` or `taskClass === "software-bugfix"`).
659
+ *
660
+ * Mirrors the Wave 24 `mandatory_delegations_skipped_by_track`
661
+ * audit pattern: best-effort write to `delegation-events.jsonl`, no
662
+ * agent payload, recognized by `readDelegationEvents` so it does not
663
+ * corrupt downstream parsers. Failures are swallowed.
664
+ */
665
+ export async function recordExpansionStrategistSkippedByTrack(projectRoot, params) {
666
+ const eventsPath = delegationEventsPath(projectRoot);
667
+ const payload = {
668
+ event: "expansion_strategist_skipped_by_track",
669
+ stage: "scope",
670
+ track: params.track,
671
+ taskClass: params.taskClass,
672
+ runId: params.runId,
673
+ selectedScopeMode: params.selectedScopeMode,
674
+ ts: new Date().toISOString()
675
+ };
676
+ try {
677
+ await fs.mkdir(path.dirname(eventsPath), { recursive: true });
678
+ await fs.appendFile(eventsPath, `${JSON.stringify(payload)}\n`, "utf8");
679
+ }
680
+ catch {
681
+ // best-effort audit; never block stage advance.
682
+ }
558
683
  }
@@ -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. */
@@ -349,7 +349,7 @@ Before responding to a coding request:
349
349
 
350
350
  Three rules apply to every cclaw stage in this project, regardless of which skills loaded:
351
351
 
352
- 1. **Q&A convergence before drafting** — for brainstorm / scope / design, walk the stage forcing questions one at a time via the harness-native question tool (Claude \`AskUserQuestion\`, Cursor \`AskQuestion\`, Codex \`request_user_input\`, Gemini \`ask_user\`). The \`qa_log_unconverged\` linter rule will block \`stage-complete\` when convergence has not been reached. Convergence is satisfied when ANY of: (a) all forcing-question topics are addressed in \`## Q&A Log\`, (b) the last 2 substantive rows produce no decision-changing impact (Ralph-Loop), or (c) an explicit user stop-signal row is recorded. The fixed count floor (10 for standard) was removed in Wave 23.
352
+ 1. **Q&A convergence before drafting** — for brainstorm / scope / design, walk the stage forcing questions one at a time via the harness-native question tool (Claude \`AskUserQuestion\`, Cursor \`AskQuestion\`, Codex \`request_user_input\`, Gemini \`ask_user\`). The \`qa_log_unconverged\` linter rule will block \`stage-complete\` when convergence has not been reached. Convergence is satisfied when ANY of: (a) every forcing-question topic id is tagged \`[topic:<id>]\` in at least one \`## Q&A Log\` row, (b) the last 2 substantive rows produce no decision-changing impact (Ralph-Loop), or (c) an explicit user stop-signal row is recorded. The fixed count floor (10 for standard) was removed in Wave 23. Wave 24 (v6.0.0) made \`[topic:<id>]\` tagging mandatory (no English keyword fallback) so the gate works in any natural language.
353
353
  2. **Subagents run after Q&A approval** — mandatory subagents in brainstorm / scope / design (\`product-discovery\`, \`critic\`, \`planner\`, \`architect\`, \`test-author\`) run only AFTER the user approves the elicitation outcome. See each stage's "Run Phase: post-elicitation" rows in the materialized Automatic Subagent Dispatch table.
354
354
  3. **No command-line echo to chat** — the user does not run cclaw helpers manually. Never paste \`node .cclaw/hooks/...\` invocations, \`--evidence-json '{...}'\` payloads, or shell hash commands (\`shasum\`, \`sha256sum\`, \`Get-FileHash\`, \`certutil\`, etc.) into chat. Run helpers via the tool layer; report only the resulting summary.
355
355
 
@@ -19,6 +19,8 @@ interface InternalValidationReport {
19
19
  corruptEventLines: number[];
20
20
  staleWorkers: string[];
21
21
  expectedMode: string;
22
+ /** Wave 24: true when mandatoryAgentsFor returned [] for the run's track / taskClass. */
23
+ skippedByTrack: boolean;
22
24
  };
23
25
  gates: {
24
26
  ok: boolean;
@@ -32,7 +34,43 @@ interface InternalValidationReport {
32
34
  issues: string[];
33
35
  };
34
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
+ */
35
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[]>;
36
74
  export declare function buildValidationReport(projectRoot: string, flowState: FlowState, options?: {
37
75
  allowBlockedReviewRoute?: boolean;
38
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,6 +102,63 @@ 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
163
  const delegation = await checkMandatoryDelegations(projectRoot, flowState.currentStage);
92
164
  const gates = await verifyCurrentStageGateEvidence(projectRoot, flowState, {
@@ -111,7 +183,8 @@ export async function buildValidationReport(projectRoot, flowState, options = {}
111
183
  legacyInferredCompletions: delegation.legacyInferredCompletions,
112
184
  corruptEventLines: delegation.corruptEventLines,
113
185
  staleWorkers: delegation.staleWorkers,
114
- expectedMode: delegation.expectedMode
186
+ expectedMode: delegation.expectedMode,
187
+ skippedByTrack: delegation.skippedByTrack
115
188
  },
116
189
  gates: {
117
190
  ok: gates.ok,
@@ -228,9 +301,17 @@ export async function runAdvanceStage(projectRoot, args, io) {
228
301
  .flatMap((target) => getTransitionGuards(args.stage, target, flowState.track))
229
302
  .filter((guardId) => !allowedGateIds.has(guardId)));
230
303
  const selectableGateIds = new Set([...allowedGateIds, ...transitionGuardIds]);
231
- const selectedGateIds = args.passedGateIds.length > 0
304
+ let selectedGateIds = args.passedGateIds.length > 0
232
305
  ? args.passedGateIds.filter((gateId) => selectableGateIds.has(gateId))
233
306
  : requiredGateIds;
307
+ // Wave 25 (v6.1.0): if the active stage has an auto-hydratable
308
+ // review-loop gate (currently `design.design_architecture_locked`)
309
+ // and the artifact already contains a valid review-loop envelope,
310
+ // include the gate in selectedGateIds and hydrate evidence in one
311
+ // step. This removes the Wave 24 contradiction between "omit from
312
+ // --evidence-json so we can auto-hydrate" and "missing
313
+ // --evidence-json entries for passed gates".
314
+ selectedGateIds = await tryAutoHydrateAndSelectReviewLoopGate(projectRoot, args.stage, flowState.track, requiredGateIds, selectedGateIds, args.evidenceByGate);
234
315
  const selectedGateIdSet = new Set(selectedGateIds);
235
316
  const selectedTransitionGuards = selectedGateIds.filter((gateId) => transitionGuardIds.has(gateId));
236
317
  const blockedReviewRoute = args.stage === "review" && selectedGateIdSet.has("review_verdict_blocked");
@@ -239,7 +320,11 @@ export async function runAdvanceStage(projectRoot, args, io) {
239
320
  : requiredGateIds;
240
321
  const missingRequired = requiredForSelectedRoute.filter((gateId) => !selectedGateIdSet.has(gateId));
241
322
  if (missingRequired.length > 0) {
242
- io.stderr.write(`cclaw internal advance-stage: required gates not selected as passed: ${missingRequired.join(", ")}.\n`);
323
+ const autoHydrateGate = AUTO_REVIEW_LOOP_GATE_BY_STAGE[args.stage];
324
+ const autoHydrateHint = autoHydrateGate && missingRequired.includes(autoHydrateGate) && (args.stage === "scope" || args.stage === "design")
325
+ ? ` 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}.`
326
+ : "";
327
+ io.stderr.write(`cclaw internal advance-stage: required gates not selected as passed: ${missingRequired.join(", ")}.${autoHydrateHint}\n`);
243
328
  return 1;
244
329
  }
245
330
  const mandatory = new Set(schema.mandatoryDelegations);
@@ -268,7 +353,10 @@ export async function runAdvanceStage(projectRoot, args, io) {
268
353
  });
269
354
  }
270
355
  }
271
- await hydrateReviewLoopEvidenceFromArtifact(projectRoot, args.stage, flowState.track, selectedGateIds, args.evidenceByGate);
356
+ // Wave 25 (v6.1.0): hydration + auto-select happens earlier via
357
+ // `tryAutoHydrateAndSelectReviewLoopGate`. The previous explicit
358
+ // call here was redundant (helper already covered both the
359
+ // already-selected and not-yet-selected paths).
272
360
  const catalog = flowState.stageGateCatalog[args.stage];
273
361
  const nextPassed = unique([...catalog.passed, ...selectedGateIds]).filter((gateId) => allowedGateIds.has(gateId));
274
362
  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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "5.0.0",
3
+ "version": "6.1.0",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {