cclaw-cli 6.1.1 → 6.2.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 (35) 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 +2 -2
  9. package/dist/content/skills-elicitation.js +8 -19
  10. package/dist/content/stage-schema.d.ts +3 -3
  11. package/dist/content/stage-schema.js +27 -4
  12. package/dist/content/stages/brainstorm.js +5 -5
  13. package/dist/content/stages/design.js +1 -1
  14. package/dist/content/stages/scope.js +2 -2
  15. package/dist/content/start-command.d.ts +2 -2
  16. package/dist/content/start-command.js +18 -17
  17. package/dist/content/subagents.js +1 -1
  18. package/dist/delegation.js +2 -2
  19. package/dist/flow-state.d.ts +5 -1
  20. package/dist/flow-state.js +6 -1
  21. package/dist/gate-evidence.js +4 -3
  22. package/dist/internal/advance-stage/advance.js +4 -3
  23. package/dist/internal/advance-stage/parsers.d.ts +2 -1
  24. package/dist/internal/advance-stage/parsers.js +11 -1
  25. package/dist/internal/advance-stage/proactive-delegation-trace.d.ts +19 -0
  26. package/dist/internal/advance-stage/proactive-delegation-trace.js +44 -0
  27. package/dist/internal/advance-stage/start-flow.js +17 -2
  28. package/dist/internal/advance-stage/verify.d.ts +0 -8
  29. package/dist/internal/advance-stage/verify.js +2 -30
  30. package/dist/run-persistence.js +10 -2
  31. package/dist/track-heuristics.d.ts +2 -2
  32. package/dist/track-heuristics.js +11 -6
  33. package/dist/types.d.ts +2 -0
  34. package/dist/types.js +1 -0
  35. 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=...] [--skip-questions] [--json]", {
195
195
  positionalArgName: "stage",
196
196
  positionalArgRequired: true,
197
197
  defaultQuietEnvVar: "CCLAW_STAGE_COMPLETE_QUIET"
@@ -1,16 +1,15 @@
1
1
  import { RUNTIME_ROOT } from "../constants.js";
2
2
  import { questionBudgetHint } from "../track-heuristics.js";
3
- import { FLOW_TRACKS } from "../types.js";
4
3
  const ELICITATION_STAGES = ["brainstorm", "scope", "design"];
5
4
  function renderQuestionBudgetHintTable() {
6
5
  const rows = [];
7
- for (const track of FLOW_TRACKS) {
6
+ for (const mode of ["lean", "guided", "deep"]) {
8
7
  for (const stage of ELICITATION_STAGES) {
9
- const hint = questionBudgetHint(track, stage);
10
- rows.push(`| \`${track}\` | \`${stage}\` | ${hint.min} | ${hint.recommended} | ${hint.hardCapWarning} |`);
8
+ const hint = questionBudgetHint(mode, stage);
9
+ rows.push(`| \`${mode}\` | \`${stage}\` | ${hint.min} | ${hint.recommended} | ${hint.hardCapWarning} |`);
11
10
  }
12
11
  }
13
- return `| Track | Stage | Min | Recommended | Hard cap warning |
12
+ return `| Discovery mode | Stage | Min | Recommended | Hard cap warning |
14
13
  |---|---|---|---|---|
15
14
  ${rows.join("\n")}`;
16
15
  }
@@ -47,7 +46,7 @@ These behaviors are the exact reason this skill exists. The linter will block yo
47
46
  - Ask exactly one question per turn and wait for the answer before asking the next one.
48
47
  - Use harness-native question tools first; prose fallback is allowed only when the tool is unavailable.
49
48
  - Keep a running Q&A trace in the active artifact under \`## Q&A Log\` in \`${RUNTIME_ROOT}/artifacts/\` as append-only rows.
50
- - **Convergence floor**: do NOT advance the stage (do NOT call \`stage-complete.mjs\`) until Q&A converges. Convergence is reached when ANY of: (a) every forcing-question topic id is tagged \`[topic:<id>]\` on at least one \`## Q&A Log\` row, (b) the last 2 substantive rows produce no decision-changing impact (\`skip\`/\`continue\`/\`no-change\`/\`done\`), or (c) an explicit user stop-signal row is recorded. The linter rule \`qa_log_unconverged\` enforces this; \`stage-complete\` will fail otherwise. Wave 24 (v6.0.0) made the topic tag MANDATORY (no English keyword fallback) so the gate works in any natural language.
49
+ - **Convergence floor**: do NOT advance the stage (do NOT call \`stage-complete.mjs\`) until Q&A converges. The machine contract matches \`evaluateQaLogFloor\` in \`src/artifact-linter/shared.ts\` (rule \`qa_log_unconverged\`). Pass when ANY holds: (a) every forcing-question topic id is tagged \`[topic:<id>]\` on at least one \`## Q&A Log\` row; (b) the Ralph-Loop detector fires (last 2 substantive rows are non-decision-changing: \`skip\`/\`continue\`/\`no-change\`/\`done\`/etc.) **and** the log has at least \`max(2, questionBudgetHint(discoveryMode, stage).min)\` substantive rows — **unless** \`discoveryMode\` is \`guided\` or \`deep\` with pending forcing-topic ids (then Ralph-Loop alone cannot pass until topics are tagged, a stop-signal is recorded, or \`--skip-questions\` downgrades the finding to advisory); (c) an explicit user stop-signal row; or (d) \`--skip-questions\` was persisted (unconverged is advisory only). Wave 24 (v6.0.0) made \`[topic:<id>]\` mandatory (no English keyword fallback).
51
50
  - **NEVER run shell hash commands** (\`shasum\`, \`sha256sum\`, \`md5sum\`, \`Get-FileHash\`, \`certutil\`, etc.) to compute artifact hashes. If a linter ever asks you for a hash, that is a linter bug — report failure and stop, do not auto-fix in bash.
52
51
  - **NEVER paste cclaw command lines into chat** (e.g. \`node .cclaw/hooks/stage-complete.mjs ... --evidence-json '{...}'\`). Run them via the tool layer; report only the resulting summary. The user does not run cclaw manually and seeing the command line is noise.
53
52
 
@@ -103,23 +102,13 @@ Each grill question follows the same Core Protocol: ask one, wait, log, self-eva
103
102
 
104
103
  Do not ask extra questions "for theater" on simple low-risk work.
105
104
 
106
- ## Question Budget Hint (advisory only Wave 23 dropped the count floor)
105
+ ## Question Budget Hint (\`questionBudgetHint\`min rows feed the convergence floor)
107
106
 
108
- Source of truth: \`questionBudgetHint(track, stage)\`. The numbers below are
109
- **soft hints** for harness UI and elicitation pacing; gate blocking is done
110
- by the \`qa_log_unconverged\` rule (Ralph-Loop convergence detector), NOT by
111
- a fixed count.
107
+ Source of truth: \`questionBudgetHint(discoveryMode, stage)\`. The \`Min\` column is **not advisory** for the Ralph-Loop exit: \`evaluateQaLogFloor\` requires at least \`max(2, Min)\` substantive rows before the no-new-decisions path can converge (other exits — full topic coverage, stop-signal, \`--skip-questions\` advisory — ignore that minimum). \`Recommended\` and \`Hard cap warning\` remain pacing hints for the harness.
112
108
 
113
109
  ${budgetTable}
114
110
 
115
- Track mapping note: \`quick\` ~= lightweight, \`medium\` ~= standard, \`standard\` ~= deep.
116
-
117
- How to use the columns:
118
- - \`Min\` — soft minimum to surface forcing questions; not a blocking gate.
119
- - \`Recommended\` — target for normal flows.
120
- - \`Hard cap warning\` — point at which to stop or compress remaining forcing questions into one final batched ask. Not skip.
121
-
122
- ## Stage Forcing Questions (walk in order, one per turn)
111
+ Default mapping note: \`lean\` maps to a lightweight specialist tier on early stages, \`guided\` to standard, \`deep\` to deep; risk signals can escalate further.
123
112
 
124
113
  **Walk the forcing questions list one-by-one in order, asking each as a separate turn.** Do NOT batch. Do NOT pick favorites — go in order. For each question record one of:
125
114
  - \`asked\` — question was asked and answered.
@@ -1,4 +1,4 @@
1
- import type { FlowStage, FlowTrack, TransitionRule } from "../types.js";
1
+ import type { DiscoveryMode, FlowStage, FlowTrack, TransitionRule } from "../types.js";
2
2
  import type { StageComplexityTier, StageAutoSubagentDispatch, StageSchema } from "./stages/schema-types.js";
3
3
  export type { ArtifactValidation, CrossStageTrace, ReviewSection, StageComplexityTier, StageExecutionModel, StagePhilosophy, StageArtifactRules, StageReviewLoop, StageReviewLens, StageAutoSubagentDispatch, StageGate, StageSchemaLegacyInput, StageSchema, StageSchemaInput, StageSchemaV2Input } from "./stages/schema-types.js";
4
4
  export declare const SKILL_ENVELOPE_KINDS: readonly ["stage-output", "gate-result", "delegation-record"];
@@ -83,7 +83,7 @@ export declare function mandatoryDelegationsForStage(stage: FlowStage, complexit
83
83
  * boundary.
84
84
  */
85
85
  export type MandatoryDelegationTaskClass = "software-standard" | "software-trivial" | "software-bugfix";
86
- export declare function mandatoryAgentsFor(stage: FlowStage, track: FlowTrack, taskClass?: MandatoryDelegationTaskClass | null, complexityTier?: StageComplexityTier): string[];
86
+ export declare function mandatoryAgentsFor(stage: FlowStage, track: FlowTrack, taskClass?: MandatoryDelegationTaskClass | null, complexityTier?: StageComplexityTier, discoveryMode?: DiscoveryMode): string[];
87
87
  /**
88
88
  * Wave 25 (v6.1.0) — track-aware artifact validation demotion.
89
89
  *
@@ -107,7 +107,7 @@ export declare function mandatoryAgentsFor(stage: FlowStage, track: FlowTrack, t
107
107
  * `delegation-events.jsonl` once per stage advance for traceability.
108
108
  */
109
109
  export declare function shouldDemoteArtifactValidationByTrack(track: FlowTrack, taskClass?: MandatoryDelegationTaskClass | null): boolean;
110
- export declare function stageSchema(stage: FlowStage, track?: FlowTrack): StageSchema;
110
+ export declare function stageSchema(stage: FlowStage, track?: FlowTrack, discoveryMode?: DiscoveryMode, taskClass?: MandatoryDelegationTaskClass | null): StageSchema;
111
111
  export declare function orderedStageSchemas(track?: FlowTrack): StageSchema[];
112
112
  export declare function stageGateIds(stage: FlowStage, track?: FlowTrack): string[];
113
113
  export declare function stageRecommendedGateIds(stage: FlowStage, track?: FlowTrack): string[];
@@ -81,6 +81,20 @@ function dedupeAgentsInOrder(agents) {
81
81
  }
82
82
  return out;
83
83
  }
84
+ function discoveryModeTier(mode) {
85
+ if (mode === "lean")
86
+ return "lightweight";
87
+ if (mode === "deep")
88
+ return "deep";
89
+ return "standard";
90
+ }
91
+ function resolvedStageComplexityTier(params) {
92
+ const base = params.defaultTier ?? "standard";
93
+ const earlyStage = params.stage === "brainstorm" || params.stage === "scope" || params.stage === "design";
94
+ if (!earlyStage || params.discoveryMode === undefined)
95
+ return base;
96
+ return discoveryModeTier(params.discoveryMode);
97
+ }
84
98
  function defaultReturnSchemaForAgent(agent) {
85
99
  switch (agent) {
86
100
  case "researcher":
@@ -811,12 +825,17 @@ export function mandatoryDelegationsForStage(stage, complexityTier = "standard")
811
825
  .find((row) => row.stage === stage);
812
826
  return summary ? summary.mandatoryAgents : [];
813
827
  }
814
- export function mandatoryAgentsFor(stage, track, taskClass, complexityTier = "standard") {
828
+ export function mandatoryAgentsFor(stage, track, taskClass, complexityTier = "standard", discoveryMode) {
815
829
  if (track === "quick")
816
830
  return [];
817
831
  if (taskClass === "software-bugfix")
818
832
  return [];
819
- return mandatoryDelegationsForStage(stage, complexityTier);
833
+ const effectiveTier = resolvedStageComplexityTier({
834
+ stage,
835
+ defaultTier: complexityTier,
836
+ discoveryMode
837
+ });
838
+ return mandatoryDelegationsForStage(stage, effectiveTier);
820
839
  }
821
840
  /**
822
841
  * Wave 25 (v6.1.0) — track-aware artifact validation demotion.
@@ -847,7 +866,7 @@ export function shouldDemoteArtifactValidationByTrack(track, taskClass) {
847
866
  return true;
848
867
  return false;
849
868
  }
850
- export function stageSchema(stage, track = "standard") {
869
+ export function stageSchema(stage, track = "standard", discoveryMode, taskClass) {
851
870
  const rawInput = stage === "tdd" ? tddStageForTrack(track) : STAGE_SCHEMA_MAP[stage];
852
871
  const base = normalizeStageSchemaInput(rawInput);
853
872
  const tieredGates = tieredStageGates(stage, base.requiredGates, track);
@@ -856,7 +875,11 @@ export function stageSchema(stage, track = "standard") {
856
875
  ...base.crossStageTrace,
857
876
  readsFrom: readsFromForTrack(base.crossStageTrace.readsFrom, track)
858
877
  };
859
- const complexityTier = base.complexityTier ?? "standard";
878
+ const complexityTier = resolvedStageComplexityTier({
879
+ stage,
880
+ defaultTier: base.complexityTier ?? "standard",
881
+ discoveryMode
882
+ });
860
883
  const mandatoryDelegations = mandatoryDelegationsForStage(stage, complexityTier);
861
884
  const philosophy = {
862
885
  hardGate: base.hardGate,