cclaw-cli 6.1.1 → 6.3.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 (40) hide show
  1. package/README.md +2 -2
  2. package/dist/artifact-linter/brainstorm.js +13 -13
  3. package/dist/artifact-linter/design.js +5 -5
  4. package/dist/artifact-linter/scope.js +3 -3
  5. package/dist/artifact-linter/shared.d.ts +18 -19
  6. package/dist/artifact-linter/shared.js +34 -31
  7. package/dist/artifact-linter.js +4 -0
  8. package/dist/content/hooks.js +154 -2
  9. package/dist/content/skills-elicitation.js +8 -19
  10. package/dist/content/skills.js +1 -0
  11. package/dist/content/stage-schema.d.ts +3 -3
  12. package/dist/content/stage-schema.js +31 -6
  13. package/dist/content/stages/brainstorm.js +5 -5
  14. package/dist/content/stages/design.js +1 -1
  15. package/dist/content/stages/schema-types.d.ts +6 -0
  16. package/dist/content/stages/scope.js +2 -2
  17. package/dist/content/start-command.d.ts +2 -2
  18. package/dist/content/start-command.js +23 -18
  19. package/dist/content/subagents.js +1 -1
  20. package/dist/content/templates.d.ts +1 -1
  21. package/dist/content/templates.js +1 -0
  22. package/dist/delegation.js +2 -2
  23. package/dist/flow-state.d.ts +14 -1
  24. package/dist/flow-state.js +6 -1
  25. package/dist/gate-evidence.js +4 -3
  26. package/dist/internal/advance-stage/advance.js +20 -4
  27. package/dist/internal/advance-stage/parsers.d.ts +2 -1
  28. package/dist/internal/advance-stage/parsers.js +12 -1
  29. package/dist/internal/advance-stage/proactive-delegation-trace.d.ts +21 -0
  30. package/dist/internal/advance-stage/proactive-delegation-trace.js +60 -0
  31. package/dist/internal/advance-stage/start-flow.d.ts +3 -1
  32. package/dist/internal/advance-stage/start-flow.js +81 -2
  33. package/dist/internal/advance-stage/verify.d.ts +0 -8
  34. package/dist/internal/advance-stage/verify.js +2 -30
  35. package/dist/run-persistence.js +37 -2
  36. package/dist/track-heuristics.d.ts +2 -2
  37. package/dist/track-heuristics.js +11 -6
  38. package/dist/types.d.ts +2 -0
  39. package/dist/types.js +1 -0
  40. package/package.json +1 -1
package/README.md CHANGED
@@ -68,7 +68,7 @@ Legacy `.cclaw/runs/` directories are only auto-removed when empty. If the direc
68
68
 
69
69
  That gives you:
70
70
 
71
- - **One path** from idea to ship, with `quick`, `medium`, and `standard` tracks.
71
+ - **One path** from idea to ship, with one user-chosen discovery mode (`lean`, `guided`, `deep`) and internal `quick` / `medium` / `standard` tracks.
72
72
  - **Real gates** for evidence, tests, review, delegation, stale-stage recovery, and closeout.
73
73
  - **Subagents with accountability**: controller owns state, workers do bounded tasks, overseers validate, evidence lands in `delegation-log.json`.
74
74
  - **Recovery instead of confusion**: `npx cclaw-cli sync` tells you blockers and next fixes.
@@ -101,7 +101,7 @@ medium brainstorm -> spec -> plan -> tdd -> review -> ship
101
101
  standard brainstorm -> scope -> design -> spec -> plan -> tdd -> review -> ship
102
102
  ```
103
103
 
104
- Track selection is **model-guided and advisory** during `/cc`. Runtime enforcement begins after state is written: subsequent `/cc` turns follow the selected track, required gates, delegation rules, stale-stage markers, and `closeout.shipSubstate`.
104
+ At `/cc <idea>`, the user picks **one discovery mode** (`lean`, `guided`, `deep`) for upstream shaping. Track selection remains **model-guided and advisory** during start-up; runtime enforcement begins after state is written: subsequent `/cc` turns follow the selected internal track, persisted `discoveryMode`, required gates, delegation rules, stale-stage markers, and `closeout.shipSubstate`.
105
105
 
106
106
  ## When Blocked
107
107
 
@@ -20,7 +20,7 @@ export async function lintBrainstormStage(ctx) {
20
20
  });
21
21
  if (!brainstormShortCircuitActivated) {
22
22
  const skipQuestions = ctx.activeStageFlags.includes("--skip-questions");
23
- const floor = evaluateQaLogFloor(qaLogBody, track, "brainstorm", { skipQuestions });
23
+ const floor = evaluateQaLogFloor(qaLogBody, track, "brainstorm", { discoveryMode: ctx.discoveryMode, skipQuestions });
24
24
  findings.push({
25
25
  section: "qa_log_unconverged",
26
26
  required: !floor.skipQuestionsAdvisory,
@@ -53,7 +53,7 @@ export async function lintBrainstormStage(ctx) {
53
53
  const ok = hasDecisionLine;
54
54
  findings.push({
55
55
  section: "Approach Tier Classification",
56
- required: true,
56
+ required: false,
57
57
  rule: "Approach Tier must explicitly classify depth as one of `lite` (a.k.a. `Lightweight`), `Standard`, or `Deep`.",
58
58
  found: ok,
59
59
  details: ok
@@ -77,14 +77,14 @@ export async function lintBrainstormStage(ctx) {
77
77
  });
78
78
  findings.push({
79
79
  section: "Approaches Role/Upside Taxonomy",
80
- required: true,
80
+ required: false,
81
81
  rule: "Approaches table must use canonical Role and Upside enum values.",
82
82
  found: approachesTaxonomy.roleUpsideOk,
83
83
  details: approachesTaxonomy.details
84
84
  });
85
85
  findings.push({
86
86
  section: "Challenger Alternative Enforcement",
87
- required: true,
87
+ required: false,
88
88
  rule: "Approaches must include one challenger option with explicit high/higher upside.",
89
89
  found: approachesTaxonomy.challengerOk,
90
90
  details: approachesTaxonomy.details
@@ -96,7 +96,7 @@ export async function lintBrainstormStage(ctx) {
96
96
  const orderOk = reactionIndex >= 0 && reactionIndex < directionIndex;
97
97
  findings.push({
98
98
  section: "Approach Reaction Ordering",
99
- required: true,
99
+ required: false,
100
100
  rule: "Approach Reaction must appear before Selected Direction (propose -> react -> recommend).",
101
101
  found: orderOk,
102
102
  details: orderOk
@@ -151,7 +151,7 @@ export async function lintBrainstormStage(ctx) {
151
151
  const hasStatus = statusValue.length > 0;
152
152
  findings.push({
153
153
  section: "Short-Circuit Status",
154
- required: true,
154
+ required: false,
155
155
  rule: "Short-Circuit Decision must include a `Status:` line (`activated` or `bypassed`).",
156
156
  found: hasStatus,
157
157
  details: hasStatus
@@ -187,7 +187,7 @@ export async function lintBrainstormStage(ctx) {
187
187
  const selfReview = validateCalibratedSelfReview(selfReviewBody);
188
188
  findings.push({
189
189
  section: "Calibrated Self-Review Format",
190
- required: true,
190
+ required: false,
191
191
  rule: "When Self-Review Notes are present, they must use the calibrated review prompt output shape.",
192
192
  found: selfReview.ok,
193
193
  details: selfReview.details
@@ -197,7 +197,7 @@ export async function lintBrainstormStage(ctx) {
197
197
  if (criticPredictions !== null) {
198
198
  findings.push({
199
199
  section: "critic.predictions_missing",
200
- required: true,
200
+ required: false,
201
201
  rule: "[P2] critic.predictions_missing — pre-commitment predictions block missing or empty",
202
202
  found: criticPredictions.found,
203
203
  details: criticPredictions.details
@@ -227,7 +227,7 @@ export async function lintBrainstormStage(ctx) {
227
227
  const ok = tokenMatches.size === 1 && !isPlaceholder;
228
228
  findings.push({
229
229
  section: "Mode Block Token",
230
- required: true,
230
+ required: false,
231
231
  rule: "Mode Block must declare exactly one mode token: STARTUP, BUILDER, ENGINEERING, OPS, or RESEARCH.",
232
232
  found: ok,
233
233
  details: ok
@@ -246,7 +246,7 @@ export async function lintBrainstormStage(ctx) {
246
246
  /^RECOMMENDATION:/imu.test(raw)) {
247
247
  findings.push({
248
248
  section: "Approach Detail Cards",
249
- required: true,
249
+ required: false,
250
250
  rule: "Approach Detail Cards must include ≥2 `#### APPROACH <letter>` blocks each with Summary/Effort/Risk/Pros/Cons/Reuses.",
251
251
  found: cardCount >= 2,
252
252
  details: cardCount >= 2
@@ -257,7 +257,7 @@ export async function lintBrainstormStage(ctx) {
257
257
  const hasRecommendation = recommendationLine !== null && recommendationLine[1] !== undefined && recommendationLine[1].trim().length > 0;
258
258
  findings.push({
259
259
  section: "Approach Recommendation Marker",
260
- required: true,
260
+ required: false,
261
261
  rule: "Approach Detail Cards must conclude with a single `RECOMMENDATION:` line citing the chosen letter and rationale.",
262
262
  found: hasRecommendation,
263
263
  details: hasRecommendation
@@ -272,7 +272,7 @@ export async function lintBrainstormStage(ctx) {
272
272
  const optedOut = /\bnot used\b|\bn\/a\b|\bnone\b/iu.test(outsideVoiceBody);
273
273
  findings.push({
274
274
  section: "Outside Voice Slot Shape",
275
- required: true,
275
+ required: false,
276
276
  rule: "Outside Voice section must either declare opt-out (`not used`/`none`) or include `source:`, `prompt:`, `tension:`, `resolution:`.",
277
277
  found: optedOut || missing.length === 0,
278
278
  details: optedOut || missing.length === 0
@@ -337,7 +337,7 @@ export async function lintBrainstormStage(ctx) {
337
337
  const waveDriftAddressed = hasCarryForwardSection && hasCarryForwardContent && hasDriftAuditMarkers;
338
338
  findings.push({
339
339
  section: "wave.drift_unaddressed",
340
- required: true,
340
+ required: false,
341
341
  rule: "[P1] wave.drift_unaddressed — when `.cclaw/wave-plans/` has >=2 entries, brainstorm must include `## Wave Carry-forward` with carry-forward and drift audit markers.",
342
342
  found: waveDriftAddressed,
343
343
  details: waveDriftAddressed
@@ -284,7 +284,7 @@ export async function lintDesignStage(ctx) {
284
284
  });
285
285
  {
286
286
  const skipQuestions = activeStageFlags.includes("--skip-questions");
287
- const floor = evaluateQaLogFloor(qaLogBody, track, "design", { skipQuestions });
287
+ const floor = evaluateQaLogFloor(qaLogBody, track, "design", { discoveryMode: ctx.discoveryMode, skipQuestions });
288
288
  findings.push({
289
289
  section: "qa_log_unconverged",
290
290
  required: !floor.skipQuestionsAdvisory,
@@ -297,7 +297,7 @@ export async function lintDesignStage(ctx) {
297
297
  if (criticPredictions !== null) {
298
298
  findings.push({
299
299
  section: "critic.predictions_missing",
300
- required: true,
300
+ required: false,
301
301
  rule: "[P2] critic.predictions_missing — pre-commitment predictions block missing or empty",
302
302
  found: criticPredictions.found,
303
303
  details: criticPredictions.details
@@ -390,7 +390,7 @@ export async function lintDesignStage(ctx) {
390
390
  const ack = markdownFieldRegex("Iron rule acknowledged", "yes|true|y").test(regressionBody);
391
391
  findings.push({
392
392
  section: "Regression Iron Rule Acknowledgement",
393
- required: true,
393
+ required: false,
394
394
  rule: "Regression Iron Rule section must affirm `Iron rule acknowledged: yes`.",
395
395
  found: ack,
396
396
  details: ack
@@ -409,7 +409,7 @@ export async function lintDesignStage(ctx) {
409
409
  const ok = isEmpty || validRows.length >= 1;
410
410
  findings.push({
411
411
  section: "Calibrated Finding Format",
412
- required: true,
412
+ required: false,
413
413
  rule: "Calibrated Findings must either declare `None this stage` or contain at least one finding in the form `[P1|P2|P3] (confidence: <n>/10) <path>[:<line>] — <description>`.",
414
414
  found: ok,
415
415
  details: isEmpty
@@ -423,7 +423,7 @@ export async function lintDesignStage(ctx) {
423
423
  if (layeredDocumentReview !== null) {
424
424
  findings.push({
425
425
  section: "Document Reviewer Structured Findings",
426
- required: true,
426
+ required: false,
427
427
  rule: "When Layered review references coherence-reviewer/scope-guardian-reviewer/feasibility-reviewer, include explicit reviewer status plus calibrated finding lines.",
428
428
  found: layeredDocumentReview.missingStructured.length === 0,
429
429
  details: layeredDocumentReview.missingStructured.length === 0
@@ -23,7 +23,7 @@ export async function lintScopeStage(ctx) {
23
23
  });
24
24
  {
25
25
  const skipQuestions = activeStageFlags.includes("--skip-questions");
26
- const floor = evaluateQaLogFloor(qaLogBody, track, "scope", { skipQuestions });
26
+ const floor = evaluateQaLogFloor(qaLogBody, track, "scope", { discoveryMode: ctx.discoveryMode, skipQuestions });
27
27
  findings.push({
28
28
  section: "qa_log_unconverged",
29
29
  required: !floor.skipQuestionsAdvisory,
@@ -94,7 +94,7 @@ export async function lintScopeStage(ctx) {
94
94
  if (criticPredictions !== null) {
95
95
  findings.push({
96
96
  section: "critic.predictions_missing",
97
- required: true,
97
+ required: false,
98
98
  rule: "[P2] critic.predictions_missing — pre-commitment predictions block missing or empty",
99
99
  found: criticPredictions.found,
100
100
  details: criticPredictions.details
@@ -139,7 +139,7 @@ export async function lintScopeStage(ctx) {
139
139
  }
140
140
  findings.push({
141
141
  section: "Locked Decisions ID Integrity",
142
- required: true,
142
+ required: false,
143
143
  rule: "Locked Decisions section must list each decision with a unique stable D-XX ID. (D-XX IDs replaced the legacy LD#<sha8> hash anchors in Wave 22.)",
144
144
  found: issues.length === 0,
145
145
  details: issues.length === 0
@@ -1,4 +1,4 @@
1
- import { type FlowStage, type FlowTrack } from "../types.js";
1
+ import { type DiscoveryMode, type FlowStage, type FlowTrack } from "../types.js";
2
2
  /**
3
3
  * Stages that run adaptive elicitation. The `qa_log_unconverged` rule
4
4
  * only fires for these. Other stages may still record a Q&A Log but no
@@ -20,6 +20,7 @@ export interface ForcingQuestionTopic {
20
20
  topic: string;
21
21
  }
22
22
  export interface QaLogFloorOptions {
23
+ discoveryMode?: DiscoveryMode;
23
24
  /**
24
25
  * When true, downgrades the finding to advisory (`required: false`).
25
26
  * Set when `--skip-questions` was persisted to the active stage flags.
@@ -97,25 +98,22 @@ export declare function extractForcingQuestions(stage: FlowStage): ForcingQuesti
97
98
  * design artifact. Returns ok=true when convergence is reached or any
98
99
  * escape hatch fires.
99
100
  *
100
- * Convergence sources (any one is sufficient):
101
- * - All forcing-question topics from the stage checklist appear addressed
102
- * in `## Q&A Log` (substring keyword match in question/answer columns).
103
- * - The Ralph-Loop convergence detector reports the last 2 substantive
104
- * rows have decision_impact marking `skip`/`continue`/`no-change`/`done`
105
- * (i.e. the dialogue is no longer producing decision-changing rows).
106
- * - Q&A Log contains a stop-signal row (existing
107
- * `QA_LOG_STOP_SIGNAL_PATTERNS` keep working).
108
- * - `--skip-questions` flag was persisted to the active stage flags
109
- * (`options.skipQuestions=true`); finding downgrades to advisory.
110
- * - The stage checklist exposes no forcing-questions row (e.g. simple
111
- * refactor) AND the artifact has at least one substantive row — treat
112
- * as converged because there is nothing left to force.
101
+ * Convergence sources (any one can set ok=true — see also
102
+ * `adaptiveElicitationSkillMarkdown`):
103
+ * - Every forcing-question topic id from the stage checklist is tagged
104
+ * `[topic:<id>]` on at least one `## Q&A Log` row.
105
+ * - Ralph-Loop path: last 2 substantive rows read as no-new-decisions,
106
+ * substantive count max(2, questionBudgetHint(discoveryMode, stage).min),
107
+ * and not (guided/deep discovery with pending forcing-topic ids).
108
+ * - Stop-signal row (`QA_LOG_STOP_SIGNAL_PATTERNS`).
109
+ * - `--skip-questions` (`options.skipQuestions`): ok remains false but
110
+ * `skipQuestionsAdvisory` is true (linter treats as non-blocking).
111
+ * - No forcing-questions row in the checklist and ≥1 substantive row.
113
112
  *
114
- * Wave 23 (v5.0.0) replaces the count-based `qa_log_below_min` rule with
115
- * `qa_log_unconverged`. The fixed count constant (10 for standard) and
116
- * the `CCLAW_ELICITATION_FLOOR=advisory` env override were removed. The
117
- * `min` and `liteShortCircuit` fields on the result are retained for
118
- * harness UI compatibility but are always 0/false.
113
+ * Wave 23 retired the fixed English-only count floor; Wave 24 made
114
+ * `[topic:<id>]` mandatory for topic coverage. The `min` and
115
+ * `liteShortCircuit` fields stay for harness compatibility (min is always 0;
116
+ * liteShortCircuit false).
119
117
  */
120
118
  export declare function evaluateQaLogFloor(qaLogBody: string | null, track: FlowTrack, stage: FlowStage, options?: QaLogFloorOptions): QaLogFloorResult;
121
119
  export interface LintFinding {
@@ -448,6 +446,7 @@ export interface StageLintContext {
448
446
  projectRoot: string;
449
447
  stage: FlowStage;
450
448
  track: FlowTrack;
449
+ discoveryMode: DiscoveryMode;
451
450
  raw: string;
452
451
  absFile: string;
453
452
  sections: H2SectionMap;
@@ -207,25 +207,22 @@ function lastTwoRowsAllNoDecision(substantiveRows) {
207
207
  * design artifact. Returns ok=true when convergence is reached or any
208
208
  * escape hatch fires.
209
209
  *
210
- * Convergence sources (any one is sufficient):
211
- * - All forcing-question topics from the stage checklist appear addressed
212
- * in `## Q&A Log` (substring keyword match in question/answer columns).
213
- * - The Ralph-Loop convergence detector reports the last 2 substantive
214
- * rows have decision_impact marking `skip`/`continue`/`no-change`/`done`
215
- * (i.e. the dialogue is no longer producing decision-changing rows).
216
- * - Q&A Log contains a stop-signal row (existing
217
- * `QA_LOG_STOP_SIGNAL_PATTERNS` keep working).
218
- * - `--skip-questions` flag was persisted to the active stage flags
219
- * (`options.skipQuestions=true`); finding downgrades to advisory.
220
- * - The stage checklist exposes no forcing-questions row (e.g. simple
221
- * refactor) AND the artifact has at least one substantive row — treat
222
- * as converged because there is nothing left to force.
210
+ * Convergence sources (any one can set ok=true — see also
211
+ * `adaptiveElicitationSkillMarkdown`):
212
+ * - Every forcing-question topic id from the stage checklist is tagged
213
+ * `[topic:<id>]` on at least one `## Q&A Log` row.
214
+ * - Ralph-Loop path: last 2 substantive rows read as no-new-decisions,
215
+ * substantive count max(2, questionBudgetHint(discoveryMode, stage).min),
216
+ * and not (guided/deep discovery with pending forcing-topic ids).
217
+ * - Stop-signal row (`QA_LOG_STOP_SIGNAL_PATTERNS`).
218
+ * - `--skip-questions` (`options.skipQuestions`): ok remains false but
219
+ * `skipQuestionsAdvisory` is true (linter treats as non-blocking).
220
+ * - No forcing-questions row in the checklist and ≥1 substantive row.
223
221
  *
224
- * Wave 23 (v5.0.0) replaces the count-based `qa_log_below_min` rule with
225
- * `qa_log_unconverged`. The fixed count constant (10 for standard) and
226
- * the `CCLAW_ELICITATION_FLOOR=advisory` env override were removed. The
227
- * `min` and `liteShortCircuit` fields on the result are retained for
228
- * harness UI compatibility but are always 0/false.
222
+ * Wave 23 retired the fixed English-only count floor; Wave 24 made
223
+ * `[topic:<id>]` mandatory for topic coverage. The `min` and
224
+ * `liteShortCircuit` fields stay for harness compatibility (min is always 0;
225
+ * liteShortCircuit false).
229
226
  */
230
227
  export function evaluateQaLogFloor(qaLogBody, track, stage, options = {}) {
231
228
  const rows = qaLogBody !== null ? getMarkdownTableRows(qaLogBody) : [];
@@ -233,6 +230,7 @@ export function evaluateQaLogFloor(qaLogBody, track, stage, options = {}) {
233
230
  const count = substantiveRows.length;
234
231
  const hasStopSignal = detectStopSignal(rows);
235
232
  const skipQuestionsAdvisory = options.skipQuestions === true;
233
+ const discoveryMode = options.discoveryMode ?? (track === "quick" ? "lean" : "guided");
236
234
  const forcingTopics = (options.forcingQuestions ?? extractForcingQuestions(stage)).map((entry) => (typeof entry === "string" ? { id: entry, topic: entry } : entry));
237
235
  const forcingCovered = [];
238
236
  const forcingPending = [];
@@ -242,9 +240,13 @@ export function evaluateQaLogFloor(qaLogBody, track, stage, options = {}) {
242
240
  else
243
241
  forcingPending.push(topic.id);
244
242
  }
243
+ const budget = questionBudgetHint(discoveryMode, stage);
245
244
  const noNewDecisions = lastTwoRowsAllNoDecision(substantiveRows);
246
245
  const allForcingCovered = forcingTopics.length > 0 ? forcingPending.length === 0 : count >= 1;
247
- const ok = allForcingCovered || noNewDecisions || hasStopSignal;
246
+ const minimumRowsReached = count >= Math.max(2, budget.min);
247
+ const riskEscalationNeeded = forcingPending.length > 0 && /^(guided|deep)$/u.test(discoveryMode);
248
+ const noNewDecisionConverged = noNewDecisions && minimumRowsReached && !riskEscalationNeeded;
249
+ const ok = allForcingCovered || noNewDecisionConverged || hasStopSignal;
248
250
  const pendingIdsBracket = forcingPending.length > 0
249
251
  ? `[${forcingPending.join(", ")}]`
250
252
  : "[none]";
@@ -256,10 +258,10 @@ export function evaluateQaLogFloor(qaLogBody, track, stage, options = {}) {
256
258
  else if (allForcingCovered) {
257
259
  details = `Q&A Log converged: stage exposes no forcing-questions row and ${count} substantive entry recorded.`;
258
260
  }
259
- else if (noNewDecisions) {
261
+ else if (noNewDecisionConverged) {
260
262
  const remaining = forcingPending.length > 0
261
- ? ` ${forcingPending.length} forcing topic IDs still pending: ${pendingIdsBracket} (Ralph-Loop convergence overrode coverage).`
262
- : " Ralph-Loop convergence detector says no new decision-changing rows in the last 2 turns.";
263
+ ? ` ${forcingPending.length} forcing topic IDs still pending: ${pendingIdsBracket} after the minimum ${budget.min}-row discovery pass.`
264
+ : ` Ralph-Loop convergence detector says no new decision-changing rows in the last 2 turns after the minimum ${budget.min}-row discovery pass.`;
263
265
  details = `Q&A Log converged via no-new-decisions detector at ${count} row(s).${remaining}`;
264
266
  }
265
267
  else {
@@ -269,27 +271,28 @@ export function evaluateQaLogFloor(qaLogBody, track, stage, options = {}) {
269
271
  else if (skipQuestionsAdvisory) {
270
272
  details = `Q&A Log unconverged at ${count} row(s); --skip-questions flag downgraded the finding to advisory. Forcing topic IDs pending: ${pendingIdsBracket}.`;
271
273
  }
274
+ else if (noNewDecisions && !minimumRowsReached) {
275
+ details = `Q&A Log still below the minimum ${budget.min}-row ${discoveryMode} discovery pass (${count} substantive row(s)). Forcing topic IDs pending: ${pendingIdsBracket}. Continue asking decision-changing questions before drafting.`;
276
+ }
277
+ else if (riskEscalationNeeded && noNewDecisions) {
278
+ details = `Q&A Log cannot converge via Ralph-Loop yet because ${discoveryMode} mode keeps pending forcing topic IDs blocking: ${pendingIdsBracket}. Cover the remaining topics or record an explicit stop-signal row.`;
279
+ }
272
280
  else {
273
- details = `Q&A Log unconverged at ${count} row(s). Forcing topic IDs pending: ${pendingIdsBracket}. Tag each Q&A row with \`[topic:<id>]\` to mark coverage, append a no-new-decisions pair, or record an explicit user stop-signal row.`;
281
+ details = `Q&A Log unconverged at ${count} row(s). Forcing topic IDs pending: ${pendingIdsBracket}. Tag each Q&A row with \`[topic:<id>]\` to mark coverage, complete the minimum ${budget.min}-row ${discoveryMode} discovery pass, or record an explicit user stop-signal row.`;
274
282
  }
275
- // Surface advisory budget hint for harness UI without re-introducing a
276
- // blocking count. `recommended` is the soft budget per track/stage.
277
- const advisoryBudget = questionBudgetHint(track, stage).recommended;
283
+ const advisoryBudget = budget.recommended;
278
284
  return {
279
285
  ok,
280
286
  count,
281
- // Wave 23: floor no longer enforces a count. Surfacing 0 keeps the
282
- // QaLogFloorSignal shape stable for harness consumers; harness UIs
283
- // may show `recommended` from `questionBudgetHint` separately.
284
287
  min: 0,
285
288
  hasStopSignal,
286
289
  liteShortCircuit: false,
287
290
  skipQuestionsAdvisory,
288
291
  forcingCovered,
289
292
  forcingPending,
290
- noNewDecisions,
293
+ noNewDecisions: noNewDecisionConverged,
291
294
  details: advisoryBudget > 0
292
- ? `${details} (advisory budget for ${track}/${stage}: ~${advisoryBudget} Q&A turns)`
295
+ ? `${details} (advisory budget for ${discoveryMode}/${stage}: ~${advisoryBudget} Q&A turns)`
293
296
  : details
294
297
  };
295
298
  }
@@ -114,6 +114,7 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
114
114
  // Same flow-state read powers the post-loop demotion + audit log
115
115
  // below; we cache the result here to avoid two disk reads.
116
116
  let activeStageFlags = [];
117
+ let discoveryMode = "guided";
117
118
  let taskClass = null;
118
119
  let activeRunId = null;
119
120
  try {
@@ -121,11 +122,13 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
121
122
  const hint = flowState.interactionHints?.[stage];
122
123
  if (hint?.skipQuestions === true)
123
124
  activeStageFlags.push("--skip-questions");
125
+ discoveryMode = flowState.discoveryMode ?? "guided";
124
126
  taskClass = flowState.taskClass ?? null;
125
127
  activeRunId = flowState.activeRunId ?? null;
126
128
  }
127
129
  catch {
128
130
  activeStageFlags = [];
131
+ discoveryMode = "guided";
129
132
  taskClass = null;
130
133
  activeRunId = null;
131
134
  }
@@ -195,6 +198,7 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
195
198
  projectRoot,
196
199
  stage,
197
200
  track,
201
+ discoveryMode,
198
202
  raw,
199
203
  absFile,
200
204
  sections,
@@ -185,13 +185,13 @@ void main();
185
185
  `;
186
186
  }
187
187
  export function startFlowScript() {
188
- return internalHelperScript("start-flow", "start-flow", "Usage: node " + RUNTIME_ROOT + "/hooks/start-flow.mjs --track=<standard|medium|quick> [--class=...] [--prompt=...] [--stack=...] [--reason=...] [--reclassify] [--force-reset]", { defaultQuietEnvVar: "CCLAW_START_FLOW_QUIET" });
188
+ return internalHelperScript("start-flow", "start-flow", "Usage: node " + RUNTIME_ROOT + "/hooks/start-flow.mjs --track=<standard|medium|quick> [--discovery-mode=<lean|guided|deep>] [--class=...] [--prompt=...] [--stack=...] [--reason=...] [--reclassify] [--force-reset]", { defaultQuietEnvVar: "CCLAW_START_FLOW_QUIET" });
189
189
  }
190
190
  export function cancelRunScript() {
191
191
  return internalHelperScript("cancel-run", "cancel-run", "Usage: node " + RUNTIME_ROOT + "/hooks/cancel-run.mjs --reason=<text> [--disposition=<cancelled|abandoned>] [--name=<slug>]");
192
192
  }
193
193
  export function stageCompleteScript() {
194
- return internalHelperScript("stage-complete", "advance-stage", "Usage: node " + RUNTIME_ROOT + "/hooks/stage-complete.mjs <stage> [--passed=...] [--evidence-json=...] [--waive-delegation=...] [--waiver-reason=...] [--accept-proactive-waiver] [--accept-proactive-waiver-reason=...] [--skip-questions] [--json]", {
194
+ return internalHelperScript("stage-complete", "advance-stage", "Usage: node " + RUNTIME_ROOT + "/hooks/stage-complete.mjs <stage> [--passed=...] [--evidence-json=...] [--waive-delegation=...] [--waiver-reason=...] [--accept-proactive-waiver] [--accept-proactive-waiver-reason=\"<why safe>\"] [--skip-questions] [--json]", {
195
195
  positionalArgName: "stage",
196
196
  positionalArgRequired: true,
197
197
  defaultQuietEnvVar: "CCLAW_STAGE_COMPLETE_QUIET"
@@ -296,6 +296,7 @@ function usage() {
296
296
  "Usage:",
297
297
  " node .cclaw/hooks/delegation-record.mjs --stage=<stage> --agent=<agent> --mode=<mandatory|proactive> --status=<scheduled|launched|acknowledged|completed|failed|waived|stale> --span-id=<id> [--dispatch-id=<id>] [--worker-run-id=<id>] [--dispatch-surface=<surface>] [--agent-definition-path=<path>] [--ack-ts=<iso>] [--launched-ts=<iso>] [--completed-ts=<iso>] [--evidence-ref=<ref>] [--waiver-reason=<text>] [--json]",
298
298
  " node .cclaw/hooks/delegation-record.mjs --rerecord --span-id=<id> --dispatch-id=<id> --dispatch-surface=<surface> --agent-definition-path=<path> [--ack-ts=<iso>] [--completed-ts=<iso>] [--evidence-ref=<ref>] [--json]",
299
+ " node .cclaw/hooks/delegation-record.mjs --repair --span-id=<id> --repair-reason=\"<why>\" [--json]",
299
300
  "",
300
301
  "Allowed --dispatch-surface values:",
301
302
  " " + VALID_DISPATCH_SURFACES.join(", "),
@@ -496,10 +497,161 @@ async function runRerecord(args, json) {
496
497
  process.stdout.write(JSON.stringify({ ok: true, event, rerecord: true }, null, 2) + "\\n");
497
498
  }
498
499
 
500
+ const LIFECYCLE_PHASES = ["scheduled", "launched", "acknowledged", "completed"];
501
+
502
+ function mergeSpanTemplate(spanEvents) {
503
+ const base = {};
504
+ const keys = [
505
+ "stage",
506
+ "agent",
507
+ "mode",
508
+ "runId",
509
+ "dispatchId",
510
+ "dispatchSurface",
511
+ "agentDefinitionPath",
512
+ "workerRunId",
513
+ "fulfillmentMode",
514
+ "schemaVersion",
515
+ "parentSpanId",
516
+ "evidenceRefs",
517
+ "waiverReason"
518
+ ];
519
+ for (const e of spanEvents) {
520
+ if (!e || typeof e !== "object") continue;
521
+ for (const k of keys) {
522
+ if (base[k] === undefined && e[k] !== undefined) {
523
+ base[k] = e[k];
524
+ }
525
+ }
526
+ }
527
+ return base;
528
+ }
529
+
530
+ function repairFulfillmentMode(base) {
531
+ if (base.fulfillmentMode) return base.fulfillmentMode;
532
+ if (base.dispatchSurface === "role-switch") return "role-switch";
533
+ if (base.dispatchSurface === "cursor-task" || base.dispatchSurface === "generic-task") {
534
+ return "generic-dispatch";
535
+ }
536
+ return "isolated";
537
+ }
538
+
539
+ async function runRepair(args, json) {
540
+ const problems = [];
541
+ if (!args["span-id"]) problems.push("repair mode requires --span-id");
542
+ if (!args["repair-reason"] || String(args["repair-reason"]).trim().length === 0) {
543
+ problems.push("repair mode requires --repair-reason=<text>");
544
+ }
545
+ if (problems.length > 0) {
546
+ emitProblems(problems, json, 2);
547
+ return;
548
+ }
549
+ const spanId = args["span-id"];
550
+ const repairedReason = String(args["repair-reason"]).trim();
551
+ const root = await detectRoot();
552
+ const events = await readDelegationEvents(root);
553
+ const spanEvents = events.filter(
554
+ (e) => e && e.spanId === spanId && typeof e.event === "string" && LIFECYCLE_PHASES.includes(e.event)
555
+ );
556
+ if (spanEvents.length === 0) {
557
+ emitProblems(
558
+ ["repair refused: no lifecycle delegation-events.jsonl rows found for --span-id=" + spanId],
559
+ json,
560
+ 2
561
+ );
562
+ return;
563
+ }
564
+ const present = new Set(spanEvents.map((e) => e.event));
565
+ const base = mergeSpanTemplate(spanEvents);
566
+ if (!base.stage || !base.agent || !base.mode) {
567
+ emitProblems(["repair refused: span events missing stage/agent/mode to clone"], json, 2);
568
+ return;
569
+ }
570
+ const runId =
571
+ typeof base.runId === "string" && base.runId.length > 0 ? base.runId : await readRunId(root);
572
+ const fulfillmentMode = repairFulfillmentMode(base);
573
+ const schemaVersion =
574
+ typeof base.schemaVersion === "number" && base.schemaVersion > 0
575
+ ? base.schemaVersion
576
+ : LEDGER_SCHEMA_VERSION;
577
+ const evidenceRefs = Array.isArray(base.evidenceRefs)
578
+ ? base.evidenceRefs.filter((r) => typeof r === "string" && r.trim().length > 0)
579
+ : [];
580
+ const now = new Date().toISOString();
581
+ const appended = [];
582
+
583
+ for (const status of LIFECYCLE_PHASES) {
584
+ if (present.has(status)) continue;
585
+ if (status === "completed" && base.dispatchSurface !== "role-switch") {
586
+ if (!base.dispatchId || !base.dispatchSurface || !base.agentDefinitionPath) {
587
+ emitProblems(
588
+ [
589
+ "repair refused: cannot synthesize completed row without dispatchId, dispatchSurface, and agentDefinitionPath on span " +
590
+ spanId
591
+ ],
592
+ json,
593
+ 2
594
+ );
595
+ return;
596
+ }
597
+ }
598
+ if (status === "completed" && base.dispatchSurface === "role-switch" && evidenceRefs.length === 0) {
599
+ emitProblems(
600
+ ["repair refused: role-switch completed synthesis requires evidenceRefs on span " + spanId],
601
+ json,
602
+ 2
603
+ );
604
+ return;
605
+ }
606
+ const launchedTs =
607
+ status === "launched" || status === "acknowledged" || status === "completed" ? now : undefined;
608
+ const ackTs = status === "acknowledged" || status === "completed" ? now : undefined;
609
+ const completedTs = status === "completed" ? now : undefined;
610
+ const endTs = status === "completed" ? now : undefined;
611
+ const row = {
612
+ stage: base.stage,
613
+ agent: base.agent,
614
+ mode: base.mode,
615
+ status,
616
+ spanId,
617
+ dispatchId: base.dispatchId,
618
+ workerRunId: base.workerRunId,
619
+ dispatchSurface: base.dispatchSurface,
620
+ agentDefinitionPath: base.agentDefinitionPath,
621
+ fulfillmentMode,
622
+ evidenceRefs,
623
+ runId,
624
+ startTs: now,
625
+ ts: now,
626
+ launchedTs,
627
+ ackTs,
628
+ completedTs,
629
+ endTs,
630
+ schemaVersion
631
+ };
632
+ const clean = Object.fromEntries(Object.entries(row).filter(([, value]) => value !== undefined));
633
+ const event = { ...clean, event: status, eventTs: now, repairedAt: now, repairedReason };
634
+ await persistEntry(root, runId, clean, event);
635
+ present.add(status);
636
+ appended.push(status);
637
+ }
638
+
639
+ if (json) {
640
+ process.stdout.write(
641
+ JSON.stringify({ ok: true, repair: true, spanId, appended, repairedAt: now, repairedReason }, null, 2) + "\\n"
642
+ );
643
+ }
644
+ }
645
+
499
646
  async function main() {
500
647
  const args = parseArgs(process.argv.slice(2));
501
648
  const json = args.json !== undefined;
502
649
 
650
+ if (args.repair) {
651
+ await runRepair(args, json);
652
+ return;
653
+ }
654
+
503
655
  if (args.rerecord) {
504
656
  await runRerecord(args, json);
505
657
  return;