cclaw-cli 4.0.0 → 6.0.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 (39) hide show
  1. package/dist/artifact-linter/brainstorm.js +40 -2
  2. package/dist/artifact-linter/design.js +2 -2
  3. package/dist/artifact-linter/review-army.d.ts +25 -0
  4. package/dist/artifact-linter/review-army.js +155 -0
  5. package/dist/artifact-linter/review.js +13 -0
  6. package/dist/artifact-linter/scope.js +9 -17
  7. package/dist/artifact-linter/shared.d.ts +93 -19
  8. package/dist/artifact-linter/shared.js +214 -96
  9. package/dist/artifact-linter.d.ts +1 -1
  10. package/dist/artifact-linter.js +1 -1
  11. package/dist/content/core-agents.js +6 -1
  12. package/dist/content/idea.js +14 -2
  13. package/dist/content/skills-elicitation.js +35 -18
  14. package/dist/content/skills.js +10 -4
  15. package/dist/content/stage-schema.d.ts +29 -0
  16. package/dist/content/stage-schema.js +17 -0
  17. package/dist/content/stages/_lint-metadata/index.js +1 -2
  18. package/dist/content/stages/brainstorm.js +5 -2
  19. package/dist/content/stages/design.js +13 -12
  20. package/dist/content/stages/review.js +21 -21
  21. package/dist/content/stages/scope.js +20 -18
  22. package/dist/content/stages/spec.js +3 -3
  23. package/dist/content/stages/tdd.js +1 -0
  24. package/dist/content/subagents.js +3 -1
  25. package/dist/content/templates.d.ts +2 -2
  26. package/dist/content/templates.js +52 -36
  27. package/dist/delegation.d.ts +16 -0
  28. package/dist/delegation.js +64 -3
  29. package/dist/flow-state.d.ts +12 -0
  30. package/dist/gate-evidence.d.ts +12 -0
  31. package/dist/gate-evidence.js +4 -1
  32. package/dist/harness-adapters.js +1 -1
  33. package/dist/internal/advance-stage/advance.d.ts +2 -0
  34. package/dist/internal/advance-stage/advance.js +2 -1
  35. package/dist/internal/advance-stage/parsers.d.ts +8 -0
  36. package/dist/internal/advance-stage/parsers.js +27 -1
  37. package/dist/internal/advance-stage/start-flow.js +13 -0
  38. package/dist/run-persistence.js +14 -2
  39. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import { SHIP_FINALIZATION_MODES } from "../constants.js";
2
2
  import { questionBudgetHint } from "../track-heuristics.js";
3
3
  import { FLOW_STAGES } from "../types.js";
4
+ import { stageSchema } from "../content/stage-schema.js";
4
5
  /**
5
6
  * Recognized stop-signal phrases that satisfy the Q&A floor escape hatch
6
7
  * when recorded as a Q&A Log row. Mirrors `Stop Signals (Natural Language)`
@@ -29,9 +30,9 @@ const QA_LOG_STOP_SIGNAL_PATTERNS = [
29
30
  /(?<![\p{L}\p{N}_])рухаємось\s+далі(?![\p{L}\p{N}_])/iu
30
31
  ];
31
32
  /**
32
- * Stages that run adaptive elicitation. The `qa_log_below_min` rule only
33
- * fires for these. Other stages may still record a Q&A Log but no floor is
34
- * enforced.
33
+ * Stages that run adaptive elicitation. The `qa_log_unconverged` rule
34
+ * only fires for these. Other stages may still record a Q&A Log but no
35
+ * convergence floor is enforced.
35
36
  */
36
37
  export const ELICITATION_STAGES = new Set([
37
38
  "brainstorm",
@@ -39,9 +40,27 @@ export const ELICITATION_STAGES = new Set([
39
40
  "design"
40
41
  ]);
41
42
  /**
42
- * Decide whether a Q&A Log row counts as a "substantive" entry for the floor.
43
- * Rows whose disposition column reads `skipped` / `waived` only do not
44
- * count toward the minimum.
43
+ * Phrases that mark a Q&A Log row as "no new decision" used by the
44
+ * Ralph-Loop convergence detector. When the last 2 substantive rows have
45
+ * a Decision impact tagged with one of these phrases, convergence has
46
+ * been reached even if not every forcing question was explicitly
47
+ * addressed.
48
+ */
49
+ const QA_LOG_NO_DECISION_TOKENS = [
50
+ /\bskip(?:ped)?\b/iu,
51
+ /\bcontinue\b/iu,
52
+ /\bno[-\s]?change\b/iu,
53
+ /\bno[-\s]?decision\b/iu,
54
+ /\bno[-\s]?op\b/iu,
55
+ /\bnoop\b/iu,
56
+ /\bdone\b/iu,
57
+ /\bsame\b/iu,
58
+ /\bok\b/iu
59
+ ];
60
+ /**
61
+ * Decide whether a Q&A Log row counts as a "substantive" entry. Rows
62
+ * whose decision_impact column reads `skipped` / `waived` only do not
63
+ * count.
45
64
  */
46
65
  function isSubstantiveQaRow(cells) {
47
66
  if (cells.length === 0)
@@ -53,8 +72,8 @@ function isSubstantiveQaRow(cells) {
53
72
  return true;
54
73
  }
55
74
  /**
56
- * Detect a stop-signal row in the Q&A Log. Pattern is matched across all
57
- * cells of any row so the user's quote can live in any column.
75
+ * Detect a stop-signal row in the Q&A Log. Pattern is matched across
76
+ * all cells of any row so the user's quote can live in any column.
58
77
  */
59
78
  function detectStopSignal(rows) {
60
79
  for (const row of rows) {
@@ -67,60 +86,211 @@ function detectStopSignal(rows) {
67
86
  return false;
68
87
  }
69
88
  /**
70
- * Evaluate the Q&A Log floor for a brainstorm / scope / design artifact.
71
- * Returns ok=true when the floor is satisfied or any escape hatch fires.
89
+ * Validate the kebab-case ASCII shape of a forcing-question topic ID.
90
+ * Wave 24 enforces that IDs are short, language-neutral identifiers
91
+ * authors can paste into a `[topic:<id>]` tag without typos.
92
+ */
93
+ const TOPIC_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/u;
94
+ function isValidTopicId(id) {
95
+ return TOPIC_ID_PATTERN.test(id);
96
+ }
97
+ /**
98
+ * Parse a single checklist row into the list of forcing-question topic
99
+ * descriptors it declares. Returns `null` when the row is not a
100
+ * forcing-questions header. Throws when the header is found but its
101
+ * body does not match the Wave 24 `id: topic; id: topic; ...` syntax
102
+ * — authors fix the stage definition rather than silently ship
103
+ * un-coverable topics.
104
+ *
105
+ * Exposed for unit tests that exercise the parser without depending on
106
+ * the live stage schema.
107
+ */
108
+ export function parseForcingQuestionsRow(row, context = "row") {
109
+ const headerMatch = /\*\*\s*[A-Za-z]+\s+forcing\s+questions\s*\([^)]*\)\s*\*\*\s*(?:[—\-–:]+)?\s*(.+)/iu.exec(row);
110
+ if (!headerMatch)
111
+ return null;
112
+ const body = (headerMatch[1] ?? "").trim();
113
+ if (body.length === 0)
114
+ return [];
115
+ // Take everything up to the first sentence-ending `.` followed by a
116
+ // space + capital letter. We split on `;` only; commas are part of
117
+ // human labels. Authors stop the list with `.` so the trailing
118
+ // prose ("Tag the matching ...") is excluded.
119
+ const listSection = body.split(/\.\s+(?=[A-Z])/u)[0] ?? body;
120
+ const segments = listSection
121
+ .split(/;\s*/u)
122
+ .map((segment) => segment.trim())
123
+ .filter((segment) => segment.length > 0);
124
+ const topics = [];
125
+ for (const segment of segments) {
126
+ const match = /^[`*_]?\s*([A-Za-z0-9][A-Za-z0-9-]*)\s*[`*_]?\s*:\s*(.+?)\s*$/u.exec(segment);
127
+ if (!match) {
128
+ throw new Error(`parseForcingQuestionsRow(${context}): segment "${segment}" does not match required \`id: topic\` syntax. Wave 24 (v6.0.0) requires \`id: topic; id: topic; ...\` form.`);
129
+ }
130
+ const id = (match[1] ?? "").toLowerCase();
131
+ const topic = (match[2] ?? "").replace(/[`*_]+$/u, "").trim();
132
+ if (!isValidTopicId(id)) {
133
+ throw new Error(`parseForcingQuestionsRow(${context}): invalid topic id "${id}" in segment "${segment}". IDs must match ${TOPIC_ID_PATTERN.source}.`);
134
+ }
135
+ if (topic.length === 0) {
136
+ throw new Error(`parseForcingQuestionsRow(${context}): empty topic label after id "${id}" in segment "${segment}".`);
137
+ }
138
+ topics.push({ id, topic });
139
+ }
140
+ return topics;
141
+ }
142
+ /**
143
+ * Extract forcing-question topics from a stage's checklist.
144
+ *
145
+ * Wave 24 (v6.0.0): only the new `id: topic; id: topic; ...` syntax is
146
+ * accepted. Throws when the syntax is malformed so authors fix the
147
+ * stage definition rather than silently shipping un-coverable topics.
72
148
  *
73
- * Escape hatches (any one is sufficient):
74
- * - Q&A Log contains a stop-signal row.
149
+ * Returns empty array when no forcing-questions row is present (caller
150
+ * treats absence as "no forcing requirement" — convergence falls back
151
+ * to the no-new-decisions / stop-signal detectors). Returning [] when
152
+ * the row exists but lists no segments is also legal.
153
+ */
154
+ export function extractForcingQuestions(stage) {
155
+ let checklist;
156
+ try {
157
+ checklist = stageSchema(stage).executionModel.checklist;
158
+ }
159
+ catch {
160
+ return [];
161
+ }
162
+ for (const row of checklist) {
163
+ const parsed = parseForcingQuestionsRow(row, `stage=${stage}`);
164
+ if (parsed === null)
165
+ continue;
166
+ return parsed;
167
+ }
168
+ return [];
169
+ }
170
+ /**
171
+ * Detect whether a Q&A Log row carries an explicit `[topic:<id>]` tag
172
+ * for the requested forcing-topic id. Matching is case-insensitive on
173
+ * the id, ASCII-only on the tag boundary. NO keyword fallback: the user
174
+ * must stamp the tag in any cell of the row.
175
+ */
176
+ function isTopicAddressed(id, rows) {
177
+ const needle = id.toLowerCase();
178
+ const tagPattern = /\[topic:\s*([A-Za-z0-9][A-Za-z0-9-]*)\s*\]/giu;
179
+ for (const row of rows) {
180
+ const haystack = row.join(" | ");
181
+ tagPattern.lastIndex = 0;
182
+ let match;
183
+ while ((match = tagPattern.exec(haystack)) !== null) {
184
+ const candidate = (match[1] ?? "").toLowerCase();
185
+ if (candidate === needle)
186
+ return true;
187
+ }
188
+ }
189
+ return false;
190
+ }
191
+ function lastTwoRowsAllNoDecision(substantiveRows) {
192
+ if (substantiveRows.length < 2)
193
+ return false;
194
+ const tail = substantiveRows.slice(-2);
195
+ for (const row of tail) {
196
+ const decisionImpact = (row[row.length - 1] ?? "").trim();
197
+ if (decisionImpact.length === 0)
198
+ return false;
199
+ const matched = QA_LOG_NO_DECISION_TOKENS.some((pattern) => pattern.test(decisionImpact));
200
+ if (!matched)
201
+ return false;
202
+ }
203
+ return true;
204
+ }
205
+ /**
206
+ * Evaluate the Q&A Log convergence floor for a brainstorm / scope /
207
+ * design artifact. Returns ok=true when convergence is reached or any
208
+ * escape hatch fires.
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).
75
218
  * - `--skip-questions` flag was persisted to the active stage flags
76
- * (passed via `options.skipQuestions=true`); finding downgrades to advisory.
77
- * - Track is `quick` (lite tier ~ lightweight complexity) AND substantive
78
- * count >= 1.
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.
223
+ *
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.
79
229
  */
80
230
  export function evaluateQaLogFloor(qaLogBody, track, stage, options = {}) {
81
- const hint = questionBudgetHint(track, stage);
82
- const min = hint.min;
83
231
  const rows = qaLogBody !== null ? getMarkdownTableRows(qaLogBody) : [];
84
232
  const substantiveRows = rows.filter(isSubstantiveQaRow);
85
233
  const count = substantiveRows.length;
86
234
  const hasStopSignal = detectStopSignal(rows);
87
- const liteShortCircuit = track === "quick" && count >= 1;
88
- // Emergency override (undocumented for users): set
89
- // `CCLAW_ELICITATION_FLOOR=advisory` to downgrade qa_log_below_min from
90
- // blocking to advisory globally. This is a safety net for incidents where
91
- // the floor mis-fires across an org; treat as `--skip-questions` semantics.
92
- const envOverride = (typeof process !== "undefined" ? process.env?.CCLAW_ELICITATION_FLOOR : undefined) === "advisory";
93
- const skipQuestionsAdvisory = options.skipQuestions === true || envOverride;
94
- const ok = count >= min || hasStopSignal || liteShortCircuit;
235
+ const skipQuestionsAdvisory = options.skipQuestions === true;
236
+ const forcingTopics = (options.forcingQuestions ?? extractForcingQuestions(stage)).map((entry) => (typeof entry === "string" ? { id: entry, topic: entry } : entry));
237
+ const forcingCovered = [];
238
+ const forcingPending = [];
239
+ for (const topic of forcingTopics) {
240
+ if (isTopicAddressed(topic.id, rows))
241
+ forcingCovered.push(topic.id);
242
+ else
243
+ forcingPending.push(topic.id);
244
+ }
245
+ const noNewDecisions = lastTwoRowsAllNoDecision(substantiveRows);
246
+ const allForcingCovered = forcingTopics.length > 0 ? forcingPending.length === 0 : count >= 1;
247
+ const ok = allForcingCovered || noNewDecisions || hasStopSignal;
248
+ const pendingIdsBracket = forcingPending.length > 0
249
+ ? `[${forcingPending.join(", ")}]`
250
+ : "[none]";
95
251
  let details;
96
252
  if (ok) {
97
- if (count >= min) {
98
- details = `Q&A Log has ${count} substantive entries (floor for ${track}/${stage}: ${min}).`;
253
+ if (allForcingCovered && forcingTopics.length > 0) {
254
+ details = `Q&A Log converged: all ${forcingTopics.length} forcing-question topic(s) addressed across ${count} substantive row(s).`;
255
+ }
256
+ else if (allForcingCovered) {
257
+ details = `Q&A Log converged: stage exposes no forcing-questions row and ${count} substantive entry recorded.`;
99
258
  }
100
- else if (hasStopSignal) {
101
- details = `Q&A Log has ${count} substantive entries with an explicit user stop-signal row recorded (floor: ${min}).`;
259
+ else if (noNewDecisions) {
260
+ 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
+ details = `Q&A Log converged via no-new-decisions detector at ${count} row(s).${remaining}`;
102
264
  }
103
265
  else {
104
- details = `Q&A Log has ${count} substantive entry under lightweight track short-circuit (default floor: ${min}).`;
266
+ details = `Q&A Log converged: explicit user stop-signal row recorded at ${count} row(s).`;
105
267
  }
106
268
  }
107
269
  else if (skipQuestionsAdvisory) {
108
- const reason = options.skipQuestions === true
109
- ? "--skip-questions flag was set"
110
- : "CCLAW_ELICITATION_FLOOR=advisory env override is active";
111
- details = `Q&A Log has ${count} substantive entries, minimum for ${track}/${stage} is ${min}; ${reason}, finding downgraded to advisory.`;
270
+ details = `Q&A Log unconverged at ${count} row(s); --skip-questions flag downgraded the finding to advisory. Forcing topic IDs pending: ${pendingIdsBracket}.`;
112
271
  }
113
272
  else {
114
- details = `Q&A Log has ${count} substantive entries, minimum for ${track}/${stage} is ${min}. Continue the elicitation loop or record an explicit user stop-signal row in Q&A Log.`;
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.`;
115
274
  }
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;
116
278
  return {
117
279
  ok,
118
280
  count,
119
- min,
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
+ min: 0,
120
285
  hasStopSignal,
121
- liteShortCircuit,
286
+ liteShortCircuit: false,
122
287
  skipQuestionsAdvisory,
123
- details
288
+ forcingCovered,
289
+ forcingPending,
290
+ noNewDecisions,
291
+ details: advisoryBudget > 0
292
+ ? `${details} (advisory budget for ${track}/${stage}: ~${advisoryBudget} Q&A turns)`
293
+ : details
124
294
  };
125
295
  }
126
296
  export function normalizeHeadingTitle(title) {
@@ -678,61 +848,12 @@ export function extractCanonicalScopeMode(body) {
678
848
  }
679
849
  return null;
680
850
  }
681
- export function validatePremiseChallenge(sectionBody) {
682
- // gstack-style premise challenge requires a real Q/A structure (table or
683
- // list), not free-form prose. The validation is *structural* only — we do
684
- // NOT keyword-grep for English phrases like "right problem"; authors may
685
- // write the questions in any language, and the answers carry the meaning.
686
- // The template ships with canonical question labels as scaffolding, but
687
- // the linter only enforces that the section actually compares premise
688
- // questions to answers.
689
- const tableRows = getMarkdownTableRows(sectionBody);
690
- const bulletRows = sectionBody
691
- .split(/\r?\n/u)
692
- .map((line) => line.trim())
693
- .filter((line) => /^(?:[-*]|\d+\.)\s+\S/u.test(line));
694
- const rowCount = Math.max(tableRows.length, bulletRows.length);
695
- if (rowCount < 3) {
696
- return {
697
- ok: false,
698
- details: `Premise Challenge needs at least 3 substantive rows in a table or bullet list. Found ${rowCount}.`
699
- };
700
- }
701
- // For tables, each data row must have at least 2 non-empty cells so the
702
- // section is genuinely a premise/answer comparison, not a list of headlines.
703
- // For bullet lists, each line must be substantive so we don't accept
704
- // placeholders like `- a`; punctuation style and natural language do not
705
- // matter.
706
- if (tableRows.length >= 3) {
707
- const sparseRows = tableRows.filter((row) => {
708
- const filledCells = row.filter((cell) => cell.replace(/[\s|]/gu, "").length >= 2);
709
- return filledCells.length < 2;
710
- });
711
- if (sparseRows.length > 0) {
712
- return {
713
- ok: false,
714
- details: "Premise Challenge table rows must populate at least the question and answer columns (no empty answers)."
715
- };
716
- }
717
- }
718
- else if (bulletRows.length >= 3) {
719
- const sparseBullets = bulletRows.filter((line) => {
720
- const cleaned = line.replace(/^[-*\d.\s]+/u, "").replace(/[`*_]/gu, "").trim();
721
- const meaningful = cleaned.match(/[\p{L}\p{N}]/gu)?.length ?? 0;
722
- return meaningful < 12;
723
- });
724
- if (sparseBullets.length > 0) {
725
- return {
726
- ok: false,
727
- details: "Premise Challenge bullet list must include at least 3 substantive rows, not placeholders."
728
- };
729
- }
730
- }
731
- return {
732
- ok: true,
733
- details: `Premise Challenge structures ${rowCount} Q/A rows.`
734
- };
735
- }
851
+ // `validatePremiseChallenge` was removed in Wave 23 (v5.0.0). Premise
852
+ // challenge is now owned solely by brainstorm (`## Premise Check`); scope
853
+ // only records `## Premise Drift` when scope-stage Q&A surfaces new
854
+ // evidence that materially changes the brainstorm answer. The drift
855
+ // section is optional and structural-only via the default `validateSectionBody`
856
+ // path (no specialized validator required).
736
857
  export function validateScopeSummary(sectionBody) {
737
858
  const meaningfulLines = sectionBody
738
859
  .split(/\r?\n/)
@@ -1551,9 +1672,6 @@ export function validateSectionBody(sectionBody, rule, sectionName) {
1551
1672
  if (sectionNameNormalized === "scope summary") {
1552
1673
  return validateScopeSummary(sectionBody);
1553
1674
  }
1554
- if (sectionNameNormalized === "premise challenge") {
1555
- return validatePremiseChallenge(sectionBody);
1556
- }
1557
1675
  if (sectionNameNormalized.startsWith("requirements")) {
1558
1676
  return validateRequirementsTaxonomy(sectionBody);
1559
1677
  }
@@ -1,6 +1,6 @@
1
1
  import type { FlowStage, FlowTrack } from "./types.js";
2
2
  import { type LintResult } from "./artifact-linter/shared.js";
3
- export { validateReviewArmy, checkReviewVerdictConsistency, checkReviewSecurityNoChangeAttestation, type ReviewVerdictConsistencyResult, type ReviewSecurityNoChangeAttestationResult } from "./artifact-linter/review-army.js";
3
+ export { validateReviewArmy, checkReviewVerdictConsistency, checkReviewSecurityNoChangeAttestation, checkReviewTddNoCrossArtifactDuplication, type ReviewVerdictConsistencyResult, type ReviewSecurityNoChangeAttestationResult, type ReviewTddDuplicationConflict, type ReviewTddDuplicationResult } from "./artifact-linter/review-army.js";
4
4
  export { type LintFinding, type LintResult, type LearningEntryType, type LearningConfidence, type LearningSeverity, type LearningSource, type LearningSeedEntry, type LearningsParseResult, extractMarkdownSectionBody, parseLearningsSection } from "./artifact-linter/shared.js";
5
5
  export interface LintArtifactOptions {
6
6
  /**
@@ -12,7 +12,7 @@ import { lintSpecStage } from "./artifact-linter/spec.js";
12
12
  import { lintTddStage } from "./artifact-linter/tdd.js";
13
13
  import { lintReviewStage } from "./artifact-linter/review.js";
14
14
  import { lintShipStage } from "./artifact-linter/ship.js";
15
- export { validateReviewArmy, checkReviewVerdictConsistency, checkReviewSecurityNoChangeAttestation } from "./artifact-linter/review-army.js";
15
+ export { validateReviewArmy, checkReviewVerdictConsistency, checkReviewSecurityNoChangeAttestation, checkReviewTddNoCrossArtifactDuplication } from "./artifact-linter/review-army.js";
16
16
  export { extractMarkdownSectionBody, parseLearningsSection } from "./artifact-linter/shared.js";
17
17
  const FRONTMATTER_REQUIRED_KEYS = [
18
18
  "stage",
@@ -392,7 +392,12 @@ export const CCLAW_AGENTS = [
392
392
  "Compatibility: NO_IMPACT / FOUND_<n>",
393
393
  "Observability: NO_IMPACT / FOUND_<n>",
394
394
  "Security: routed to security-reviewer (always separate)",
395
- "For unusually large/high-risk diffs, optional deep-dive context skills may be loaded: `review-perf-lens`, `review-compat-lens`, `review-observability-lens`.",
395
+ "",
396
+ "### Companion lens skills (load on-demand, never all-at-once)",
397
+ "- **review-perf-lens** — load when reviewing code touching hot paths, loops over large data, network/disk I/O, render hot paths, or sub-100ms latency budgets.",
398
+ "- **review-compat-lens** — load when reviewing code that runs on multiple OS/runtime/browser targets, modifies shared library APIs, or changes serialized payload shapes.",
399
+ "- **review-observability-lens** — load when reviewing code that adds/removes logging, metrics, traces, error reporting, or audit/compliance signals.",
400
+ "If none of those triggers apply, do NOT load the lens skills — they are deep-dive context, not default reading.",
396
401
  "",
397
402
  "For each finding include:",
398
403
  "- Severity: `Critical` | `Important` | `Suggestion`",
@@ -247,7 +247,12 @@ ${frameBullets}
247
247
  8. **Write the artifact** at
248
248
  \`${IDEA_ARTIFACT_PATTERN}\` using the schema in the skill.
249
249
  9. **Present the handoff prompt** with four concrete options - not A/B/C
250
- letters. Default = "Start /cc on the top recommendation".
250
+ letters. Default = "Start /cc on the top recommendation". When the user
251
+ picks the start option, plumb the chosen candidate forward via
252
+ \`start-flow --from-idea-artifact=<path> --from-idea-candidate=I-<n>\`
253
+ (Wave 23 / v5.0.0) so brainstorm reuses the idea's divergent + critique +
254
+ rank work via \`interactionHints.brainstorm.fromIdeaArtifact\`; do NOT
255
+ ask brainstorm to regenerate it.
251
256
 
252
257
  ## Headless mode (CI/automation only)
253
258
 
@@ -390,7 +395,14 @@ Required options, in this order:
390
395
  ### Phase 6 - Execute the choice
391
396
 
392
397
  - Start /cc: load \`${RUNTIME_ROOT}/skills/using-cclaw/SKILL.md\` and run
393
- \`/cc <phrase>\`.
398
+ \`/cc <phrase>\`. **Wave 23 (v5.0.0) handoff carry-forward (mandatory when starting from /cc-idea):**
399
+ the harness shim that turns \`/cc <phrase>\` into a \`start-flow\` invocation
400
+ MUST forward the originating idea artifact and chosen candidate so brainstorm
401
+ reuses divergent + critique + rank work instead of redoing it. Equivalent CLI
402
+ call (used by automation; harness handles this transparently in interactive mode):
403
+ \`npx cclaw-cli internal start-flow --track=<track> --prompt='<phrase>' --from-idea-artifact=${IDEA_ARTIFACT_PATTERN} --from-idea-candidate=I-<n>\`.
404
+ The hint lands in \`flow-state.interactionHints.brainstorm\` and brainstorm's
405
+ \`Idea-evidence carry-forward\` checklist row picks it up.
394
406
  - Save and close: reply with artifact path and stop.
395
407
  - Discard: delete the artifact and stop.
396
408
 
@@ -47,7 +47,7 @@ These behaviors are the exact reason this skill exists. The linter will block yo
47
47
  - Ask exactly one question per turn and wait for the answer before asking the next one.
48
48
  - Use harness-native question tools first; prose fallback is allowed only when the tool is unavailable.
49
49
  - Keep a running Q&A trace in the active artifact under \`## Q&A Log\` in \`${RUNTIME_ROOT}/artifacts/\` as append-only rows.
50
- - **Hard floor**: do NOT advance the stage (do NOT call \`stage-complete.mjs\`) until \`## Q&A Log\` contains at least \`min(track, stage)\` substantive entries OR an explicit user stop-signal is recorded as a row. The linter rule \`qa_log_below_min\` enforces this; \`stage-complete\` will fail otherwise.
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.
51
51
  - **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
52
  - **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
53
 
@@ -103,16 +103,19 @@ Each grill question follows the same Core Protocol: ask one, wait, log, self-eva
103
103
 
104
104
  Do not ask extra questions "for theater" on simple low-risk work.
105
105
 
106
- ## Question Budget Hint (linter-enforced floor)
106
+ ## Question Budget Hint (advisory only — Wave 23 dropped the count floor)
107
107
 
108
- Source of truth: \`questionBudgetHint(track, stage)\`. The \`Min\` column is enforced by \`qa_log_below_min\` linter rule — \`stage-complete\` fails when below.
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.
109
112
 
110
113
  ${budgetTable}
111
114
 
112
115
  Track mapping note: \`quick\` ~= lightweight, \`medium\` ~= standard, \`standard\` ~= deep.
113
116
 
114
117
  How to use the columns:
115
- - \`Min\` — hard floor. Below this, \`stage-complete\` is blocked unless escape hatch is recorded.
118
+ - \`Min\` — soft minimum to surface forcing questions; not a blocking gate.
116
119
  - \`Recommended\` — target for normal flows.
117
120
  - \`Hard cap warning\` — point at which to stop or compress remaining forcing questions into one final batched ask. Not skip.
118
121
 
@@ -124,24 +127,38 @@ How to use the columns:
124
127
  - \`skipped (already covered: turn N)\` — answered implicitly by an earlier reply; cite the turn.
125
128
  - \`waived (user override)\` — user explicitly waived this question.
126
129
 
127
- Stage forcing question lists:
130
+ ### Topic tagging (MANDATORY for forcing-question rows)
131
+
132
+ Each forcing question has a stable topic id (kebab-case ASCII, e.g. \`pain\`, \`do-nothing\`, \`data-flow\`). Tag the matching Q&A Log row's \`Decision impact\` cell with \`[topic:<id>]\` so the linter can verify coverage in any natural language. This is a **HARD requirement** in Wave 24 (v6.0.0): the linter no longer keyword-matches English question prose, so an un-tagged row does NOT count toward coverage even if the answer fully addresses the topic.
133
+
134
+ RU example (after asking \`pain\` in Russian):
135
+
136
+ \`\`\`
137
+ | Turn | Question | User answer (1-line) | Decision impact |
138
+ |---|---|---|---|
139
+ | 1 | Какую боль мы решаем? | Регистрация занимает 30 минут. | scope-shaping [topic:pain] |
140
+ \`\`\`
141
+
142
+ Multiple tags in one row are allowed when one answer covers several topics: \`[topic:pain] [topic:do-nothing]\`. Stop-signal rows do NOT need a tag.
143
+
144
+ Stage forcing question lists (id → topic):
128
145
 
129
146
  - **Brainstorm**:
130
- - What pain are we solving?
131
- - What is the most direct path?
132
- - What happens if we do nothing?
133
- - Who is the operator/user impacted first?
134
- - What are non-negotiable no-go boundaries?
147
+ - \`pain\` — What pain are we solving?
148
+ - \`direct-path\` — What is the most direct path?
149
+ - \`do-nothing\` — What happens if we do nothing?
150
+ - \`operator\` — Who is the operator/user impacted first?
151
+ - \`no-go\` — What are non-negotiable no-go boundaries?
135
152
  - **Scope**:
136
- - What is definitely in and definitely out?
137
- - Which decisions are already locked upstream?
138
- - What is the rollback path if this fails?
139
- - What are the top failure modes we must design for?
153
+ - \`in-out\` — What is definitely in and definitely out?
154
+ - \`locked-upstream\` — Which decisions are already locked upstream?
155
+ - \`rollback\` — What is the rollback path if this fails?
156
+ - \`failure-modes\` — What are the top failure modes we must design for?
140
157
  - **Design**:
141
- - What is the data flow end-to-end?
142
- - Where are the seams/interfaces and ownership boundaries?
143
- - Which invariants must always hold?
144
- - What will we explicitly NOT refactor now?
158
+ - \`data-flow\` — What is the data flow end-to-end?
159
+ - \`seams\` — Where are the seams/interfaces and ownership boundaries?
160
+ - \`invariants\` — Which invariants must always hold?
161
+ - \`not-refactor\` — What will we explicitly NOT refactor now?
145
162
 
146
163
  ## One-Way Override (Irreversible Decisions)
147
164
 
@@ -360,10 +360,16 @@ function mergedAntiPatterns(philosophy, execution) {
360
360
  }
361
361
  function completionParametersBlock(schema, track) {
362
362
  const gateList = schema.executionModel.requiredGates.map((g) => `\`${g.id}\``).join(", ");
363
- const mandatoryAgents = schema.reviewLens.mandatoryDelegations;
364
- const mandatory = schema.reviewLens.mandatoryDelegations.length > 0
365
- ? schema.reviewLens.mandatoryDelegations.map((a) => `\`${a}\``).join(", ")
366
- : "none";
363
+ // Wave 24 (v6.0.0): mandatory agents are dropped on `quick` track. Surface
364
+ // the empty list so the rendered SKILL.md doesn't tell quick-track runs to
365
+ // dispatch agents the linter is going to skip.
366
+ const trackAwareMandatoryAgents = track === "quick" ? [] : schema.reviewLens.mandatoryDelegations;
367
+ const mandatoryAgents = trackAwareMandatoryAgents;
368
+ const mandatory = trackAwareMandatoryAgents.length > 0
369
+ ? trackAwareMandatoryAgents.map((a) => `\`${a}\``).join(", ")
370
+ : track === "quick" && schema.reviewLens.mandatoryDelegations.length > 0
371
+ ? "none (skipped: quick track — Wave 24)"
372
+ : "none";
367
373
  const resolvedNextStage = nextStageForTrack(schema.stage, track);
368
374
  const nextStage = resolvedNextStage ?? "done";
369
375
  const nextDescription = nextStage === "done"
@@ -55,6 +55,35 @@ export declare function validateSkillEnvelope(value: unknown): SkillEnvelopeVali
55
55
  export declare function parseSkillEnvelope(raw: string): SkillEnvelope | null;
56
56
  /** Transition guard: agents with `mode: "mandatory"` in auto-subagent dispatch for this stage. */
57
57
  export declare function mandatoryDelegationsForStage(stage: FlowStage, complexityTier?: StageComplexityTier): string[];
58
+ /**
59
+ * Wave 24 (v6.0.0) — track-aware mandatory delegation lookup.
60
+ *
61
+ * Returns `[]` (skip the gate entirely) when the run is on a small-fix
62
+ * track or classified as a software bugfix:
63
+ *
64
+ * - `track === "quick"` — the quick track is for trivial single-purpose
65
+ * fixes (landing-page copy, doc edits, config tweaks). Mandatory
66
+ * subagent dispatch is theatre on that surface area.
67
+ * - `taskClass === "software-bugfix"` — bugfixes carry a RED-first
68
+ * repro contract; the test author + reviewer in the tdd/review
69
+ * stages already cover the safety surface, so mandatory upstream
70
+ * delegation only burns tokens.
71
+ *
72
+ * Otherwise returns the registered mandatory list for the stage at the
73
+ * given tier. Callers (gate-evidence, advance-stage validator,
74
+ * subagents.ts table generator) MUST go through this helper instead of
75
+ * `mandatoryDelegationsForStage` so the track-aware drop applies
76
+ * uniformly.
77
+ *
78
+ * NOTE: the user query also calls this `lite/quick`. There is no `lite`
79
+ * FlowTrack — the closest concept in cclaw is the `quick` track plus the
80
+ * brainstorm `lightweight` complexity tier. We key on the FlowTrack
81
+ * because the run-level decision is what matters at gate time;
82
+ * complexity tier is a per-stage knob that doesn't survive the dispatch
83
+ * boundary.
84
+ */
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[];
58
87
  export declare function stageSchema(stage: FlowStage, track?: FlowTrack): StageSchema;
59
88
  export declare function orderedStageSchemas(track?: FlowTrack): StageSchema[];
60
89
  export declare function stageGateIds(stage: FlowStage, track?: FlowTrack): string[];
@@ -439,6 +439,16 @@ const STAGE_SCHEMA_MAP = {
439
439
  review: REVIEW,
440
440
  ship: SHIP
441
441
  };
442
+ /**
443
+ * Stage-level subagent dispatch matrix.
444
+ *
445
+ * NOTE on `fixer`: the `fixer` agent is intentionally NOT listed in any stage
446
+ * row. It is dispatched on-demand by the SDD `subagent-dev` skill (and by
447
+ * reviewer flows) when a review surfaces a concrete failing criterion that
448
+ * needs a fresh worker. Adding `fixer` to the static matrix would create
449
+ * proactive-waiver theatre because it can only run after a specific review
450
+ * finding exists. See `core-agents.ts` `fixer` definition for the contract.
451
+ */
442
452
  const STAGE_AUTO_SUBAGENT_DISPATCH = {
443
453
  brainstorm: [
444
454
  {
@@ -801,6 +811,13 @@ export function mandatoryDelegationsForStage(stage, complexityTier = "standard")
801
811
  .find((row) => row.stage === stage);
802
812
  return summary ? summary.mandatoryAgents : [];
803
813
  }
814
+ export function mandatoryAgentsFor(stage, track, taskClass, complexityTier = "standard") {
815
+ if (track === "quick")
816
+ return [];
817
+ if (taskClass === "software-bugfix")
818
+ return [];
819
+ return mandatoryDelegationsForStage(stage, complexityTier);
820
+ }
804
821
  export function stageSchema(stage, track = "standard") {
805
822
  const rawInput = stage === "tdd" ? tddStageForTrack(track) : STAGE_SCHEMA_MAP[stage];
806
823
  const base = normalizeStageSchemaInput(rawInput);
@@ -18,8 +18,7 @@ const STAGE_POLICY_NEEDLES = {
18
18
  "In Scope",
19
19
  "Out of Scope",
20
20
  "Discretion Areas",
21
- "NOT in scope",
22
- "Premise Challenge",
21
+ "Premise Drift",
23
22
  "Locked Decisions",
24
23
  "Victory Detector",
25
24
  "Critic Pass"