cclaw-cli 3.0.0 → 5.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 (41) hide show
  1. package/dist/artifact-linter/brainstorm.js +51 -2
  2. package/dist/artifact-linter/design.js +14 -3
  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 +27 -48
  7. package/dist/artifact-linter/shared.d.ts +98 -11
  8. package/dist/artifact-linter/shared.js +280 -113
  9. package/dist/artifact-linter.d.ts +12 -2
  10. package/dist/artifact-linter.js +29 -13
  11. package/dist/content/core-agents.js +6 -1
  12. package/dist/content/examples.js +8 -0
  13. package/dist/content/hooks.js +2 -1
  14. package/dist/content/idea.js +14 -2
  15. package/dist/content/review-prompts.js +3 -3
  16. package/dist/content/skills-elicitation.js +61 -20
  17. package/dist/content/skills.js +19 -6
  18. package/dist/content/stage-schema.js +46 -18
  19. package/dist/content/stages/_lint-metadata/index.js +1 -2
  20. package/dist/content/stages/brainstorm.js +6 -3
  21. package/dist/content/stages/design.js +13 -12
  22. package/dist/content/stages/plan.js +1 -1
  23. package/dist/content/stages/review.js +21 -21
  24. package/dist/content/stages/schema-types.d.ts +9 -0
  25. package/dist/content/stages/scope.js +22 -20
  26. package/dist/content/stages/spec.js +3 -3
  27. package/dist/content/stages/tdd.js +1 -0
  28. package/dist/content/templates.d.ts +8 -1
  29. package/dist/content/templates.js +115 -43
  30. package/dist/flow-state.d.ts +12 -0
  31. package/dist/gate-evidence.d.ts +37 -1
  32. package/dist/gate-evidence.js +37 -3
  33. package/dist/harness-adapters.js +8 -0
  34. package/dist/install.js +22 -11
  35. package/dist/internal/advance-stage/advance.d.ts +1 -0
  36. package/dist/internal/advance-stage/advance.js +5 -2
  37. package/dist/internal/advance-stage/parsers.d.ts +8 -0
  38. package/dist/internal/advance-stage/parsers.js +27 -1
  39. package/dist/internal/advance-stage/start-flow.js +13 -0
  40. package/dist/run-persistence.js +14 -2
  41. package/package.json +1 -1
@@ -1,6 +1,272 @@
1
- import { createHash } from "node:crypto";
2
1
  import { SHIP_FINALIZATION_MODES } from "../constants.js";
2
+ import { questionBudgetHint } from "../track-heuristics.js";
3
3
  import { FLOW_STAGES } from "../types.js";
4
+ import { stageSchema } from "../content/stage-schema.js";
5
+ /**
6
+ * Recognized stop-signal phrases that satisfy the Q&A floor escape hatch
7
+ * when recorded as a Q&A Log row. Mirrors `Stop Signals (Natural Language)`
8
+ * in `adaptive-elicitation/SKILL.md`.
9
+ */
10
+ /**
11
+ * Stop-signal phrases. ASCII tokens use `\b` word boundaries; non-ASCII
12
+ * (RU/UA) tokens use Unicode-aware boundaries built from `\p{L}` so cyrillic
13
+ * characters around the phrase prevent partial matches without breaking on
14
+ * `\b`'s ASCII-only boundary semantics.
15
+ */
16
+ const QA_LOG_STOP_SIGNAL_PATTERNS = [
17
+ /\bstop[-\s]?signal\b/iu,
18
+ /\bachieved\s+enough\b/iu,
19
+ /\benough\b/iu,
20
+ /\bskip\b/iu,
21
+ /\bjust\s+draft\s+it\b/iu,
22
+ /\bstop\s+asking\b/iu,
23
+ /\bmove\s+on\b/iu,
24
+ /\bno\s+more\s+questions\b/iu,
25
+ /(?<![\p{L}\p{N}_])достаточно(?![\p{L}\p{N}_])/iu,
26
+ /(?<![\p{L}\p{N}_])хватит(?![\p{L}\p{N}_])/iu,
27
+ /(?<![\p{L}\p{N}_])давай\s+драфт(?![\p{L}\p{N}_])/iu,
28
+ /(?<![\p{L}\p{N}_])досить(?![\p{L}\p{N}_])/iu,
29
+ /(?<![\p{L}\p{N}_])вистачить(?![\p{L}\p{N}_])/iu,
30
+ /(?<![\p{L}\p{N}_])рухаємось\s+далі(?![\p{L}\p{N}_])/iu
31
+ ];
32
+ /**
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.
36
+ */
37
+ export const ELICITATION_STAGES = new Set([
38
+ "brainstorm",
39
+ "scope",
40
+ "design"
41
+ ]);
42
+ /**
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.
64
+ */
65
+ function isSubstantiveQaRow(cells) {
66
+ if (cells.length === 0)
67
+ return false;
68
+ const last = cells[cells.length - 1] ?? "";
69
+ const normalized = last.toLowerCase();
70
+ if (/^\s*(?:skipped|waived)\b/u.test(normalized))
71
+ return false;
72
+ return true;
73
+ }
74
+ /**
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.
77
+ */
78
+ function detectStopSignal(rows) {
79
+ for (const row of rows) {
80
+ const joined = row.join(" | ");
81
+ for (const pattern of QA_LOG_STOP_SIGNAL_PATTERNS) {
82
+ if (pattern.test(joined))
83
+ return true;
84
+ }
85
+ }
86
+ return false;
87
+ }
88
+ /**
89
+ * Extract forcing-question topics from a stage's checklist. Looks for
90
+ * the canonical `**<Stage> forcing questions (must be covered or
91
+ * explicitly waived)** — <topic1>, <topic2>, ...` row and tokenizes the
92
+ * comma-separated topic list. Returns trimmed topic strings stripped of
93
+ * leading question words (`what`/`who`/`where`/`which`/`how`/`is`/`do`/`does`).
94
+ *
95
+ * Returns empty array when no forcing-questions row is present (caller
96
+ * should treat absence as "no forcing requirement" — convergence falls
97
+ * back to the no-new-decisions / stop-signal detectors).
98
+ */
99
+ export function extractForcingQuestions(stage) {
100
+ let checklist;
101
+ try {
102
+ checklist = stageSchema(stage).executionModel.checklist;
103
+ }
104
+ catch {
105
+ return [];
106
+ }
107
+ for (const row of checklist) {
108
+ const headerMatch = /\*\*\s*[A-Za-z]+\s+forcing\s+questions\s*\([^)]*\)\s*\*\*\s*(?:[—\-–:]+)?\s*(.+)/iu.exec(row);
109
+ if (!headerMatch)
110
+ continue;
111
+ const body = (headerMatch[1] ?? "")
112
+ .replace(/\.$/u, "")
113
+ .trim();
114
+ if (body.length === 0)
115
+ return [];
116
+ return body
117
+ .split(/,\s*(?:and\s+)?|\s+and\s+/iu)
118
+ .map((topic) => topic.trim())
119
+ .filter((topic) => topic.length > 0)
120
+ .map((topic) => topic
121
+ .replace(/^[*_`]+|[*_`]+$/gu, "")
122
+ .replace(/^(?:what|who|where|which|how|is|are|do|does|did|can|will|would|could|should|may|might)\s+/iu, "")
123
+ .replace(/\?+$/u, "")
124
+ .trim())
125
+ .filter((topic) => topic.length > 0);
126
+ }
127
+ return [];
128
+ }
129
+ /**
130
+ * Build a salient-keyword set for a forcing-question topic. Splits on
131
+ * whitespace, drops short/stop words, lowercases. Used for fuzzy
132
+ * substring match against Q&A Log row content.
133
+ */
134
+ function topicKeywords(topic) {
135
+ const STOP_WORDS = new Set([
136
+ "the", "a", "an", "is", "are", "was", "were", "be", "to", "of", "in", "on", "at",
137
+ "for", "and", "or", "but", "if", "then", "else", "with", "without", "by", "as",
138
+ "we", "us", "our", "they", "them", "their", "you", "your", "i", "me", "my",
139
+ "this", "that", "these", "those", "it", "its", "do", "does", "did", "can",
140
+ "will", "would", "should", "could", "may", "might", "any", "some", "no", "not",
141
+ "from", "into", "onto", "upon", "than", "very", "much", "many", "more", "most",
142
+ "must", "have", "has", "had", "been", "being", "where", "when", "while",
143
+ "what", "which", "who", "whose", "whom", "why", "how", "non"
144
+ ]);
145
+ return topic
146
+ .toLowerCase()
147
+ .split(/[\s\-/.,;:()\[\]{}'"`*_]+/u)
148
+ .map((token) => token.replace(/[^\p{L}\p{N}-]/gu, ""))
149
+ .filter((token) => token.length >= 3 && !STOP_WORDS.has(token));
150
+ }
151
+ function isTopicAddressed(topic, rows) {
152
+ const keywords = topicKeywords(topic);
153
+ if (keywords.length === 0)
154
+ return true;
155
+ const minHits = keywords.length === 1 ? 1 : Math.min(2, keywords.length);
156
+ for (const row of rows) {
157
+ const haystack = row.join(" | ").toLowerCase();
158
+ let hits = 0;
159
+ for (const keyword of keywords) {
160
+ if (haystack.includes(keyword))
161
+ hits += 1;
162
+ if (hits >= minHits)
163
+ return true;
164
+ }
165
+ }
166
+ return false;
167
+ }
168
+ function lastTwoRowsAllNoDecision(substantiveRows) {
169
+ if (substantiveRows.length < 2)
170
+ return false;
171
+ const tail = substantiveRows.slice(-2);
172
+ for (const row of tail) {
173
+ const decisionImpact = (row[row.length - 1] ?? "").trim();
174
+ if (decisionImpact.length === 0)
175
+ return false;
176
+ const matched = QA_LOG_NO_DECISION_TOKENS.some((pattern) => pattern.test(decisionImpact));
177
+ if (!matched)
178
+ return false;
179
+ }
180
+ return true;
181
+ }
182
+ /**
183
+ * Evaluate the Q&A Log convergence floor for a brainstorm / scope /
184
+ * design artifact. Returns ok=true when convergence is reached or any
185
+ * escape hatch fires.
186
+ *
187
+ * Convergence sources (any one is sufficient):
188
+ * - All forcing-question topics from the stage checklist appear addressed
189
+ * in `## Q&A Log` (substring keyword match in question/answer columns).
190
+ * - The Ralph-Loop convergence detector reports the last 2 substantive
191
+ * rows have decision_impact marking `skip`/`continue`/`no-change`/`done`
192
+ * (i.e. the dialogue is no longer producing decision-changing rows).
193
+ * - Q&A Log contains a stop-signal row (existing
194
+ * `QA_LOG_STOP_SIGNAL_PATTERNS` keep working).
195
+ * - `--skip-questions` flag was persisted to the active stage flags
196
+ * (`options.skipQuestions=true`); finding downgrades to advisory.
197
+ * - The stage checklist exposes no forcing-questions row (e.g. simple
198
+ * refactor) AND the artifact has at least one substantive row — treat
199
+ * as converged because there is nothing left to force.
200
+ *
201
+ * Wave 23 (v5.0.0) replaces the count-based `qa_log_below_min` rule with
202
+ * `qa_log_unconverged`. The fixed count constant (10 for standard) and
203
+ * the `CCLAW_ELICITATION_FLOOR=advisory` env override were removed. The
204
+ * `min` and `liteShortCircuit` fields on the result are retained for
205
+ * harness UI compatibility but are always 0/false.
206
+ */
207
+ export function evaluateQaLogFloor(qaLogBody, track, stage, options = {}) {
208
+ const rows = qaLogBody !== null ? getMarkdownTableRows(qaLogBody) : [];
209
+ const substantiveRows = rows.filter(isSubstantiveQaRow);
210
+ const count = substantiveRows.length;
211
+ const hasStopSignal = detectStopSignal(rows);
212
+ const skipQuestionsAdvisory = options.skipQuestions === true;
213
+ const forcingTopics = options.forcingQuestions ?? extractForcingQuestions(stage);
214
+ const forcingCovered = [];
215
+ const forcingPending = [];
216
+ for (const topic of forcingTopics) {
217
+ if (isTopicAddressed(topic, rows))
218
+ forcingCovered.push(topic);
219
+ else
220
+ forcingPending.push(topic);
221
+ }
222
+ const noNewDecisions = lastTwoRowsAllNoDecision(substantiveRows);
223
+ const allForcingCovered = forcingTopics.length > 0 ? forcingPending.length === 0 : count >= 1;
224
+ const ok = allForcingCovered || noNewDecisions || hasStopSignal;
225
+ let details;
226
+ if (ok) {
227
+ if (allForcingCovered && forcingTopics.length > 0) {
228
+ details = `Q&A Log converged: all ${forcingTopics.length} forcing-question topic(s) addressed across ${count} substantive row(s).`;
229
+ }
230
+ else if (allForcingCovered) {
231
+ details = `Q&A Log converged: stage exposes no forcing-questions row and ${count} substantive entry recorded.`;
232
+ }
233
+ else if (noNewDecisions) {
234
+ const remaining = forcingPending.length > 0
235
+ ? ` ${forcingPending.length} forcing topic(s) still pending but last 2 rows produced no decision changes (Ralph-Loop convergence).`
236
+ : " Ralph-Loop convergence detector says no new decision-changing rows in the last 2 turns.";
237
+ details = `Q&A Log converged via no-new-decisions detector at ${count} row(s).${remaining}`;
238
+ }
239
+ else {
240
+ details = `Q&A Log converged: explicit user stop-signal row recorded at ${count} row(s).`;
241
+ }
242
+ }
243
+ else if (skipQuestionsAdvisory) {
244
+ details = `Q&A Log unconverged at ${count} row(s); --skip-questions flag downgraded the finding to advisory. Pending forcing topic(s): ${forcingPending.length > 0 ? forcingPending.join("; ") : "(none extracted)"}.`;
245
+ }
246
+ else {
247
+ details = `Q&A Log unconverged at ${count} row(s). Continue the elicitation loop until forcing-question topics are addressed (${forcingPending.length > 0 ? forcingPending.join("; ") : "no forcing topics extracted"}), the last 2 rows record no-decision impact, or an explicit user stop-signal row is appended.`;
248
+ }
249
+ // Surface advisory budget hint for harness UI without re-introducing a
250
+ // blocking count. `recommended` is the soft budget per track/stage.
251
+ const advisoryBudget = questionBudgetHint(track, stage).recommended;
252
+ return {
253
+ ok,
254
+ count,
255
+ // Wave 23: floor no longer enforces a count. Surfacing 0 keeps the
256
+ // QaLogFloorSignal shape stable for harness consumers; harness UIs
257
+ // may show `recommended` from `questionBudgetHint` separately.
258
+ min: 0,
259
+ hasStopSignal,
260
+ liteShortCircuit: false,
261
+ skipQuestionsAdvisory,
262
+ forcingCovered,
263
+ forcingPending,
264
+ noNewDecisions,
265
+ details: advisoryBudget > 0
266
+ ? `${details} (advisory budget for ${track}/${stage}: ~${advisoryBudget} Q&A turns)`
267
+ : details
268
+ };
269
+ }
4
270
  export function normalizeHeadingTitle(title) {
5
271
  return title.trim().replace(/\s+/g, " ");
6
272
  }
@@ -556,61 +822,12 @@ export function extractCanonicalScopeMode(body) {
556
822
  }
557
823
  return null;
558
824
  }
559
- export function validatePremiseChallenge(sectionBody) {
560
- // gstack-style premise challenge requires a real Q/A structure (table or
561
- // list), not free-form prose. The validation is *structural* only — we do
562
- // NOT keyword-grep for English phrases like "right problem"; authors may
563
- // write the questions in any language, and the answers carry the meaning.
564
- // The template ships with canonical question labels as scaffolding, but
565
- // the linter only enforces that the section actually compares premise
566
- // questions to answers.
567
- const tableRows = getMarkdownTableRows(sectionBody);
568
- const bulletRows = sectionBody
569
- .split(/\r?\n/u)
570
- .map((line) => line.trim())
571
- .filter((line) => /^(?:[-*]|\d+\.)\s+\S/u.test(line));
572
- const rowCount = Math.max(tableRows.length, bulletRows.length);
573
- if (rowCount < 3) {
574
- return {
575
- ok: false,
576
- details: `Premise Challenge needs at least 3 substantive rows in a table or bullet list. Found ${rowCount}.`
577
- };
578
- }
579
- // For tables, each data row must have at least 2 non-empty cells so the
580
- // section is genuinely a premise/answer comparison, not a list of headlines.
581
- // For bullet lists, each line must be substantive so we don't accept
582
- // placeholders like `- a`; punctuation style and natural language do not
583
- // matter.
584
- if (tableRows.length >= 3) {
585
- const sparseRows = tableRows.filter((row) => {
586
- const filledCells = row.filter((cell) => cell.replace(/[\s|]/gu, "").length >= 2);
587
- return filledCells.length < 2;
588
- });
589
- if (sparseRows.length > 0) {
590
- return {
591
- ok: false,
592
- details: "Premise Challenge table rows must populate at least the question and answer columns (no empty answers)."
593
- };
594
- }
595
- }
596
- else if (bulletRows.length >= 3) {
597
- const sparseBullets = bulletRows.filter((line) => {
598
- const cleaned = line.replace(/^[-*\d.\s]+/u, "").replace(/[`*_]/gu, "").trim();
599
- const meaningful = cleaned.match(/[\p{L}\p{N}]/gu)?.length ?? 0;
600
- return meaningful < 12;
601
- });
602
- if (sparseBullets.length > 0) {
603
- return {
604
- ok: false,
605
- details: "Premise Challenge bullet list must include at least 3 substantive rows, not placeholders."
606
- };
607
- }
608
- }
609
- return {
610
- ok: true,
611
- details: `Premise Challenge structures ${rowCount} Q/A rows.`
612
- };
613
- }
825
+ // `validatePremiseChallenge` was removed in Wave 23 (v5.0.0). Premise
826
+ // challenge is now owned solely by brainstorm (`## Premise Check`); scope
827
+ // only records `## Premise Drift` when scope-stage Q&A surfaces new
828
+ // evidence that materially changes the brainstorm answer. The drift
829
+ // section is optional and structural-only via the default `validateSectionBody`
830
+ // path (no specialized validator required).
614
831
  export function validateScopeSummary(sectionBody) {
615
832
  const meaningfulLines = sectionBody
616
833
  .split(/\r?\n/)
@@ -786,52 +1003,6 @@ export function validateRequirementsTaxonomy(sectionBody) {
786
1003
  details: "Requirements table uses canonical Priority values."
787
1004
  };
788
1005
  }
789
- export function validateLockedDecisionAnchors(sectionBody) {
790
- const rows = getMarkdownTableRows(sectionBody);
791
- const lines = sectionBody
792
- .split(/\r?\n/u)
793
- .map((line) => line.trim())
794
- .filter((line) => /^[-*]\s+\S/u.test(line));
795
- const anchors = [];
796
- const issues = [];
797
- for (const [index, row] of rows.entries()) {
798
- const anchor = (row[0] ?? "").trim().toLowerCase();
799
- const decisionText = (row[1] ?? "").trim();
800
- if (!/^ld#[0-9a-f]{8}$/u.test(anchor)) {
801
- issues.push(`row ${index + 1} has invalid anchor "${row[0] ?? ""}"`);
802
- continue;
803
- }
804
- anchors.push(anchor);
805
- if (decisionText.length > 0) {
806
- const expected = lockedDecisionHash(decisionText).toLowerCase();
807
- if (anchor !== expected) {
808
- issues.push(`row ${index + 1} anchor should be ${expected} for its Decision text`);
809
- }
810
- }
811
- }
812
- for (const [index, line] of lines.entries()) {
813
- const anchor = /\bLD#[0-9a-f]{8}\b/iu.exec(line)?.[0]?.toLowerCase();
814
- if (!anchor) {
815
- issues.push(`bullet ${index + 1} is missing an LD#<sha8> anchor`);
816
- continue;
817
- }
818
- anchors.push(anchor);
819
- }
820
- const duplicateAnchors = [...new Set(anchors.filter((anchor, index) => anchors.indexOf(anchor) !== index))];
821
- if (duplicateAnchors.length > 0) {
822
- issues.push(`duplicate anchors: ${duplicateAnchors.join(", ")}`);
823
- }
824
- if (anchors.length === 0 && (rows.length > 0 || lines.length > 0)) {
825
- issues.push("no LD#<sha8> anchors found");
826
- }
827
- return {
828
- ok: issues.length === 0,
829
- anchors: [...new Set(anchors)],
830
- details: issues.length === 0
831
- ? `${anchors.length} LD#hash anchor(s) recorded with no duplicates.`
832
- : issues.join("; ")
833
- };
834
- }
835
1006
  export const INTERACTION_EDGE_CASE_REQUIREMENTS = [
836
1007
  { label: "double-click", pattern: /\bdouble[\s-]?click\b/iu },
837
1008
  {
@@ -1356,14 +1527,13 @@ export function extractRequirementIdsFromMarkdown(text) {
1356
1527
  const ids = text.match(/\bR\d+\b/gu) ?? [];
1357
1528
  return [...new Set(ids)];
1358
1529
  }
1359
- export function extractLockedDecisionAnchors(text) {
1360
- const ids = text.match(/\bLD#[0-9a-f]{8}\b/giu) ?? [];
1361
- return [...new Set(ids.map((id) => id.replace(/^LD#/iu, "LD#").toLowerCase()))];
1362
- }
1363
- export function lockedDecisionHash(value) {
1364
- const normalized = value.replace(/\s+/gu, " ").trim().toLowerCase();
1365
- return `LD#${createHash("sha256").update(normalized).digest("hex").slice(0, 8)}`;
1366
- }
1530
+ // `extractLockedDecisionAnchors` was removed in Wave 22 (v4.0.0) along
1531
+ // with the LD#<sha8> contract. Cross-stage decision traceability now uses
1532
+ // stable D-XX IDs.
1533
+ // `lockedDecisionHash` was removed in Wave 22 (v4.0.0) along with the
1534
+ // `Locked Decisions Hash Integrity` linter rule. Decision identity now
1535
+ // relies on stable D-XX IDs which the agent can edit safely without
1536
+ // recomputing content hashes.
1367
1537
  export function collectPatternHits(text, patterns) {
1368
1538
  const hits = [];
1369
1539
  for (const pattern of patterns) {
@@ -1476,9 +1646,6 @@ export function validateSectionBody(sectionBody, rule, sectionName) {
1476
1646
  if (sectionNameNormalized === "scope summary") {
1477
1647
  return validateScopeSummary(sectionBody);
1478
1648
  }
1479
- if (sectionNameNormalized === "premise challenge") {
1480
- return validatePremiseChallenge(sectionBody);
1481
- }
1482
1649
  if (sectionNameNormalized.startsWith("requirements")) {
1483
1650
  return validateRequirementsTaxonomy(sectionBody);
1484
1651
  }
@@ -1,5 +1,15 @@
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
- export declare function lintArtifact(projectRoot: string, stage: FlowStage, track?: FlowTrack): Promise<LintResult>;
5
+ export interface LintArtifactOptions {
6
+ /**
7
+ * Stage-level flags supplied by the caller (typically `advance-stage`)
8
+ * that augment whatever flow-state.json says. Used so the linter sees
9
+ * `--skip-questions` even before flow-state is updated for the current
10
+ * stage (advance-stage applies the hint to the successor stage only,
11
+ * but the linter must respect the current-call intent).
12
+ */
13
+ extraStageFlags?: string[];
14
+ }
15
+ export declare function lintArtifact(projectRoot: string, stage: FlowStage, track?: FlowTrack, options?: LintArtifactOptions): Promise<LintResult>;
@@ -2,7 +2,8 @@ import fs from "node:fs/promises";
2
2
  import { resolveArtifactPath as resolveStageArtifactPath } from "./artifact-paths.js";
3
3
  import { exists } from "./fs-utils.js";
4
4
  import { stageSchema } from "./content/stage-schema.js";
5
- import { duplicateH2Headings, extractH2Sections, extractLockedDecisionAnchors, extractRequirementIdsFromMarkdown, isShortCircuitActivated, normalizeHeadingTitle, parseFrontmatter, parseLearningsSection, sectionBodyByAnyName, sectionBodyByHeadingPrefix, sectionBodyByName, validateSectionBody } from "./artifact-linter/shared.js";
5
+ import { readFlowState } from "./run-persistence.js";
6
+ import { duplicateH2Headings, extractH2Sections, extractRequirementIdsFromMarkdown, isShortCircuitActivated, normalizeHeadingTitle, parseFrontmatter, parseLearningsSection, sectionBodyByAnyName, sectionBodyByHeadingPrefix, sectionBodyByName, validateSectionBody } from "./artifact-linter/shared.js";
6
7
  import { lintBrainstormStage } from "./artifact-linter/brainstorm.js";
7
8
  import { lintDesignStage } from "./artifact-linter/design.js";
8
9
  import { lintPlanStage } from "./artifact-linter/plan.js";
@@ -11,7 +12,7 @@ import { lintSpecStage } from "./artifact-linter/spec.js";
11
12
  import { lintTddStage } from "./artifact-linter/tdd.js";
12
13
  import { lintReviewStage } from "./artifact-linter/review.js";
13
14
  import { lintShipStage } from "./artifact-linter/ship.js";
14
- export { validateReviewArmy, checkReviewVerdictConsistency, checkReviewSecurityNoChangeAttestation } from "./artifact-linter/review-army.js";
15
+ export { validateReviewArmy, checkReviewVerdictConsistency, checkReviewSecurityNoChangeAttestation, checkReviewTddNoCrossArtifactDuplication } from "./artifact-linter/review-army.js";
15
16
  export { extractMarkdownSectionBody, parseLearningsSection } from "./artifact-linter/shared.js";
16
17
  const FRONTMATTER_REQUIRED_KEYS = [
17
18
  "stage",
@@ -20,7 +21,7 @@ const FRONTMATTER_REQUIRED_KEYS = [
20
21
  "locked_decisions",
21
22
  "inputs_hash"
22
23
  ];
23
- export async function lintArtifact(projectRoot, stage, track = "standard") {
24
+ export async function lintArtifact(projectRoot, stage, track = "standard", options = {}) {
24
25
  const schema = stageSchema(stage, track);
25
26
  const { absPath: absFile, relPath: relFile } = await resolveStageArtifactPath(stage, {
26
27
  projectRoot,
@@ -157,6 +158,21 @@ export async function lintArtifact(projectRoot, stage, track = "standard") {
157
158
  details: `${learnings.details}${meaningfulStageNoneWarning}`
158
159
  });
159
160
  }
161
+ let activeStageFlags = [];
162
+ try {
163
+ const flowState = await readFlowState(projectRoot);
164
+ const hint = flowState.interactionHints?.[stage];
165
+ if (hint?.skipQuestions === true)
166
+ activeStageFlags.push("--skip-questions");
167
+ }
168
+ catch {
169
+ activeStageFlags = [];
170
+ }
171
+ for (const extra of options.extraStageFlags ?? []) {
172
+ if (typeof extra === "string" && extra.length > 0 && !activeStageFlags.includes(extra)) {
173
+ activeStageFlags.push(extra);
174
+ }
175
+ }
160
176
  const stageContext = {
161
177
  projectRoot,
162
178
  stage,
@@ -171,7 +187,8 @@ export async function lintArtifact(projectRoot, stage, track = "standard") {
171
187
  scopePreAuditEnabled,
172
188
  staleDiagramAuditEnabled,
173
189
  isTrivialOverride,
174
- overrideSet
190
+ overrideSet,
191
+ activeStageFlags
175
192
  };
176
193
  switch (stage) {
177
194
  case "brainstorm":
@@ -213,10 +230,9 @@ export async function lintArtifact(projectRoot, stage, track = "standard") {
213
230
  const requirementsBody = sectionBodyByHeadingPrefix(scopeSections, "Requirements") ?? "";
214
231
  const lockedDecisionsBody = sectionBodyByHeadingPrefix(scopeSections, "Locked Decisions") ?? "";
215
232
  const requirementIds = extractRequirementIdsFromMarkdown(requirementsBody);
216
- const lockedDecisionAnchors = extractLockedDecisionAnchors(lockedDecisionsBody);
233
+ const decisionIds = Array.from(new Set((lockedDecisionsBody.match(/\bD-\d+\b/giu) ?? []).map((id) => id.toUpperCase())));
217
234
  const missingRequirementRefs = requirementIds.filter((id) => !raw.includes(id));
218
- const normalizedCurrentRaw = raw.toLowerCase();
219
- const missingDecisionRefs = lockedDecisionAnchors.filter((id) => !normalizedCurrentRaw.includes(id));
235
+ const missingDecisionRefs = decisionIds.filter((id) => !raw.toUpperCase().includes(id));
220
236
  findings.push({
221
237
  section: "Scope Requirement Reference Integrity",
222
238
  required: requirementIds.length > 0,
@@ -229,14 +245,14 @@ export async function lintArtifact(projectRoot, stage, track = "standard") {
229
245
  : `Missing scope requirement reference(s): ${missingRequirementRefs.join(", ")}.`
230
246
  });
231
247
  findings.push({
232
- section: "Locked Decision Hash Reference Integrity",
233
- required: lockedDecisionAnchors.length > 0,
234
- rule: "Every LD#hash locked decision anchor from scope must be referenced by downstream artifacts.",
248
+ section: "Locked Decision Reference Integrity",
249
+ required: decisionIds.length > 0,
250
+ rule: "Every D-XX locked decision ID from scope must be referenced by downstream artifacts.",
235
251
  found: missingDecisionRefs.length === 0,
236
- details: lockedDecisionAnchors.length === 0
237
- ? "No LD#hash anchors found in scope artifact; reference check skipped."
252
+ details: decisionIds.length === 0
253
+ ? "No D-XX decision IDs found in scope artifact; reference check skipped."
238
254
  : missingDecisionRefs.length === 0
239
- ? `All ${lockedDecisionAnchors.length} locked decision anchor(s) are referenced.`
255
+ ? `All ${decisionIds.length} locked decision ID(s) are referenced.`
240
256
  : `Missing locked decision reference(s): ${missingDecisionRefs.join(", ")}.`
241
257
  });
242
258
  }
@@ -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`",
@@ -4,6 +4,14 @@ const STAGE_EXAMPLES = {
4
4
  - Project state: release checks exist but CI/local behavior drifts.
5
5
  - Existing anchors: \`scripts/pre-publish.sh\`, \`src/release/\`, incident notes.
6
6
 
7
+ ## Q&A Log
8
+
9
+ | Turn | Question | User answer (1-line) | Decision impact |
10
+ | --- | --- | --- | --- |
11
+ | 1 | Block invalid releases or only warn? | Block. | Validation is a hard gate. |
12
+ | 2 | Shared module or script-only patch? | Shared module. | Reuse in CI/local. |
13
+ | 3 | (stop-signal) | "достаточно, давай драфт" | stop-and-draft |
14
+
7
15
  ## Problem Decision Record
8
16
 
9
17
  - Depth: standard
@@ -193,7 +193,8 @@ export function cancelRunScript() {
193
193
  export function stageCompleteScript() {
194
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]", {
195
195
  positionalArgName: "stage",
196
- positionalArgRequired: true
196
+ positionalArgRequired: true,
197
+ defaultQuietEnvVar: "CCLAW_STAGE_COMPLETE_QUIET"
197
198
  });
198
199
  }
199
200
  export function delegationRecordScript() {
@@ -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
 
@@ -53,7 +53,7 @@ value. Do not nitpick wording.
53
53
  | 10-star delta | Is there a better high-leverage scope move worth cherry-picking? |
54
54
  | Boundary | Are accepted, deferred, and excluded items unambiguous? |
55
55
  | Mode fit | Does the selected mode match the evidence: SCOPE EXPANSION, SELECTIVE EXPANSION, HOLD SCOPE, or SCOPE REDUCTION? |
56
- | Downstream refs | Are R-IDs and LD#hash anchors ready for design/spec/plan? |
56
+ | Downstream refs | Are R-IDs and D-XX decision IDs ready for design/spec/plan? |
57
57
 
58
58
  ## Output
59
59
 
@@ -82,7 +82,7 @@ rework, missing failure behavior, or unverifiable acceptance criteria.
82
82
  | Architecture | Are component boundaries concrete and aligned with scope? |
83
83
  | Data flow | Are inputs, outputs, persistence, and async/sync edges explicit? |
84
84
  | Failure modes | Does every meaningful failure have detection, rescue, and user-visible behavior? |
85
- | Traceability | Do design decisions reference relevant R-IDs and LD#hash anchors? |
85
+ | Traceability | Do design decisions reference relevant R-IDs and D-XX decision IDs? |
86
86
  | Verification | Is each risky choice testable by spec/plan/TDD? |
87
87
  | Overbuild | Is any architecture stronger than the locked scope actually needs? |
88
88
 
@@ -95,7 +95,7 @@ Record findings in the design artifact's review section:
95
95
  **Status:** Approved | Issues Found
96
96
 
97
97
  **Issues:**
98
- - [R#/LD#hash]: <specific issue> — <why it matters>
98
+ - [R#/D-XX]: <specific issue> — <why it matters>
99
99
 
100
100
  **Recommendations:**
101
101
  - <advisory item or None>