cclaw-cli 3.0.0 → 4.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.
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { checkCriticPredictionsContract, sectionBodyByName, validateApproachesTaxonomy, headingLineIndex, meaningfulLineCount, getMarkdownTableRows, parseShortCircuitStatus, validateCalibratedSelfReview, markdownFieldRegex } from "./shared.js";
3
+ import { checkCriticPredictionsContract, evaluateQaLogFloor, sectionBodyByName, validateApproachesTaxonomy, headingLineIndex, meaningfulLineCount, getMarkdownTableRows, parseShortCircuitStatus, validateCalibratedSelfReview, markdownFieldRegex } from "./shared.js";
4
4
  export async function lintBrainstormStage(ctx) {
5
5
  const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride } = ctx;
6
6
  const qaLogBody = sectionBodyByName(sections, "Q&A Log");
@@ -9,7 +9,7 @@ export async function lintBrainstormStage(ctx) {
9
9
  findings.push({
10
10
  section: "qa_log_missing",
11
11
  required: false,
12
- rule: "[P3] qa_log_missing — Q&A Log empty — confirm you actually had a dialogue with the user, not a draft from memory.",
12
+ rule: "[P2] qa_log_missing — Q&A Log empty — confirm you actually had a dialogue with the user, not a draft from memory.",
13
13
  found: qaLogOk,
14
14
  details: qaLogOk
15
15
  ? `Q&A Log contains ${qaLogRows.length} data row(s).`
@@ -17,6 +17,17 @@ export async function lintBrainstormStage(ctx) {
17
17
  ? "Missing `## Q&A Log` section."
18
18
  : "Q&A Log is present but has zero data rows."
19
19
  });
20
+ if (!brainstormShortCircuitActivated) {
21
+ const skipQuestions = ctx.activeStageFlags.includes("--skip-questions");
22
+ const floor = evaluateQaLogFloor(qaLogBody, track, "brainstorm", { skipQuestions });
23
+ findings.push({
24
+ section: "qa_log_below_min",
25
+ required: !floor.skipQuestionsAdvisory,
26
+ rule: "[P1] qa_log_below_min — Q&A Log below the adaptive elicitation floor for this track. Continue the loop or record an explicit user stop-signal row.",
27
+ found: floor.ok,
28
+ details: floor.details
29
+ });
30
+ }
20
31
  // Brainstorm Iron Law: "NO ARTIFACT IS COMPLETE WITHOUT AN EXPLICITLY
21
32
  // APPROVED DIRECTION — SILENCE IS NOT APPROVAL." Previously this was
22
33
  // prose-only — nothing failed when the Selected Direction section
@@ -3,7 +3,7 @@ import path from "node:path";
3
3
  import { resolveArtifactPath as resolveStageArtifactPath } from "../artifact-paths.js";
4
4
  import { exists } from "../fs-utils.js";
5
5
  import { CONFIDENCE_FINDING_REGEX_SOURCE } from "../content/skills.js";
6
- import { checkCriticPredictionsContract, evaluateLayeredDocumentReviewStatus, extractMarkdownSectionBody, getMarkdownTableRows, meaningfulLineCount, sectionBodyByName, markdownFieldRegex } from "./shared.js";
6
+ import { checkCriticPredictionsContract, evaluateLayeredDocumentReviewStatus, evaluateQaLogFloor, extractMarkdownSectionBody, getMarkdownTableRows, meaningfulLineCount, sectionBodyByName, markdownFieldRegex } from "./shared.js";
7
7
  const DESIGN_DIAGRAM_REQUIREMENTS = {
8
8
  lightweight: [
9
9
  {
@@ -203,14 +203,14 @@ async function runStaleDiagramAudit(projectRoot, artifactPath, artifactRaw, code
203
203
  };
204
204
  }
205
205
  export async function lintDesignStage(ctx) {
206
- const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride } = ctx;
206
+ const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride, activeStageFlags } = ctx;
207
207
  const qaLogBody = sectionBodyByName(sections, "Q&A Log");
208
208
  const qaLogRows = qaLogBody ? getMarkdownTableRows(qaLogBody) : [];
209
209
  const qaLogOk = qaLogBody !== null && qaLogRows.length > 0;
210
210
  findings.push({
211
211
  section: "qa_log_missing",
212
212
  required: false,
213
- rule: "[P3] qa_log_missing — Q&A Log empty — confirm you actually had a dialogue with the user, not a draft from memory.",
213
+ rule: "[P2] qa_log_missing — Q&A Log empty — confirm you actually had a dialogue with the user, not a draft from memory.",
214
214
  found: qaLogOk,
215
215
  details: qaLogOk
216
216
  ? `Q&A Log contains ${qaLogRows.length} data row(s).`
@@ -218,6 +218,17 @@ export async function lintDesignStage(ctx) {
218
218
  ? "Missing `## Q&A Log` section."
219
219
  : "Q&A Log is present but has zero data rows."
220
220
  });
221
+ {
222
+ const skipQuestions = activeStageFlags.includes("--skip-questions");
223
+ const floor = evaluateQaLogFloor(qaLogBody, track, "design", { skipQuestions });
224
+ findings.push({
225
+ section: "qa_log_below_min",
226
+ required: !floor.skipQuestionsAdvisory,
227
+ rule: "[P1] qa_log_below_min — Q&A Log below the adaptive elicitation floor for this track. Continue the loop or record an explicit user stop-signal row.",
228
+ found: floor.ok,
229
+ details: floor.details
230
+ });
231
+ }
221
232
  const criticPredictions = checkCriticPredictionsContract(sections);
222
233
  if (criticPredictions !== null) {
223
234
  findings.push({
@@ -1,24 +1,17 @@
1
- import { checkCriticPredictionsContract, sectionBodyByHeadingPrefix, sectionBodyByName, extractCanonicalScopeMode, sectionBodyByAnyName, collectPatternHits, SCOPE_REDUCTION_PATTERNS, validateLockedDecisionAnchors, getMarkdownTableRows } from "./shared.js";
1
+ import { checkCriticPredictionsContract, evaluateQaLogFloor, sectionBodyByHeadingPrefix, sectionBodyByName, extractCanonicalScopeMode, getMarkdownTableRows } from "./shared.js";
2
2
  import { readDelegationLedger } from "../delegation.js";
3
3
  export async function lintScopeStage(ctx) {
4
- const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride } = ctx;
4
+ const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride, activeStageFlags } = ctx;
5
5
  const lockedDecisionsBody = sectionBodyByHeadingPrefix(sections, "Locked Decisions") ?? "";
6
6
  const scopeSummaryBody = sectionBodyByName(sections, "Scope Summary") ?? "";
7
7
  const selectedScopeMode = extractCanonicalScopeMode(scopeSummaryBody);
8
- const strictScopeGuards = parsedFrontmatter.hasFrontmatter ||
9
- sectionBodyByHeadingPrefix(sections, "Locked Decisions") !== null;
10
- const scopeSections = [
11
- sectionBodyByAnyName(sections, ["In Scope / Out of Scope", "In Scope", "Out of Scope"]) ?? "",
12
- sectionBodyByName(sections, "Scope Summary") ?? "",
13
- lockedDecisionsBody
14
- ].join("\n");
15
8
  const qaLogBody = sectionBodyByName(sections, "Q&A Log");
16
9
  const qaLogRows = qaLogBody ? getMarkdownTableRows(qaLogBody) : [];
17
10
  const qaLogOk = qaLogBody !== null && qaLogRows.length > 0;
18
11
  findings.push({
19
12
  section: "qa_log_missing",
20
13
  required: false,
21
- rule: "[P3] qa_log_missing — Q&A Log empty — confirm you actually had a dialogue with the user, not a draft from memory.",
14
+ rule: "[P2] qa_log_missing — Q&A Log empty — confirm you actually had a dialogue with the user, not a draft from memory.",
22
15
  found: qaLogOk,
23
16
  details: qaLogOk
24
17
  ? `Q&A Log contains ${qaLogRows.length} data row(s).`
@@ -26,6 +19,17 @@ export async function lintScopeStage(ctx) {
26
19
  ? "Missing `## Q&A Log` section."
27
20
  : "Q&A Log is present but has zero data rows."
28
21
  });
22
+ {
23
+ const skipQuestions = activeStageFlags.includes("--skip-questions");
24
+ const floor = evaluateQaLogFloor(qaLogBody, track, "scope", { skipQuestions });
25
+ findings.push({
26
+ section: "qa_log_below_min",
27
+ required: !floor.skipQuestionsAdvisory,
28
+ rule: "[P1] qa_log_below_min — Q&A Log below the adaptive elicitation floor for this track. Continue the loop or record an explicit user stop-signal row.",
29
+ found: floor.ok,
30
+ details: floor.details
31
+ });
32
+ }
29
33
  const strategistRequired = selectedScopeMode === "SCOPE EXPANSION" || selectedScopeMode === "SELECTIVE EXPANSION";
30
34
  if (strategistRequired) {
31
35
  const delegationLedger = await readDelegationLedger(projectRoot);
@@ -57,28 +61,11 @@ export async function lintScopeStage(ctx) {
57
61
  details: criticPredictions.details
58
62
  });
59
63
  }
60
- const reductionHits = collectPatternHits(scopeSections, SCOPE_REDUCTION_PATTERNS);
61
- findings.push({
62
- section: "No Scope Reduction Language",
63
- required: strictScopeGuards,
64
- rule: "Scope boundary sections must not use reduction placeholders (`v1`, `for now`, `later`, `temporary`, `placeholder`).",
65
- found: reductionHits.length === 0,
66
- details: reductionHits.length === 0
67
- ? "No scope-reduction phrases detected in scope boundary sections."
68
- : `Detected scope-reduction phrase(s): ${reductionHits.join(", ")}.`
69
- });
70
64
  if (sectionBodyByHeadingPrefix(sections, "Locked Decisions") !== null) {
71
- const anchorValidation = validateLockedDecisionAnchors(lockedDecisionsBody);
72
- findings.push({
73
- section: "Locked Decisions Hash Integrity",
74
- required: true,
75
- rule: "Locked Decisions section must list unique LD#<sha8> content-derived anchors.",
76
- found: anchorValidation.ok,
77
- details: anchorValidation.details
78
- });
79
- // Legacy D-XX rows remain advisory for older artifacts, but new templates
80
- // use LD#hash anchors. This check keeps D-XX duplicates visible without
81
- // making old artifacts the primary contract.
65
+ // D-XX IDs are the stable contract. The legacy LD#<sha8> hash anchor
66
+ // check was removed in Wave 22 (v4.0.0) — it caused agents to spam
67
+ // shell hash commands when shifting decision rows around, and provided
68
+ // no signal beyond the D-XX uniqueness check below.
82
69
  const listDecisionLines = lockedDecisionsBody
83
70
  .split(/\r?\n/u)
84
71
  .map((line) => line.trim())
@@ -113,8 +100,8 @@ export async function lintScopeStage(ctx) {
113
100
  }
114
101
  findings.push({
115
102
  section: "Locked Decisions ID Integrity",
116
- required: false,
117
- rule: "Locked Decisions section must list each decision with a unique stable D-XX ID.",
103
+ required: true,
104
+ 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.)",
118
105
  found: issues.length === 0,
119
106
  details: issues.length === 0
120
107
  ? `${rowDecisionIds.length} decision ID(s) recorded with no duplicates.`
@@ -1,4 +1,45 @@
1
1
  import { type FlowStage, type FlowTrack } from "../types.js";
2
+ /**
3
+ * Stages that run adaptive elicitation. The `qa_log_below_min` rule only
4
+ * fires for these. Other stages may still record a Q&A Log but no floor is
5
+ * enforced.
6
+ */
7
+ export declare const ELICITATION_STAGES: ReadonlySet<FlowStage>;
8
+ export interface QaLogFloorOptions {
9
+ /**
10
+ * When true, downgrades a below-floor finding to advisory (`required: false`).
11
+ * Set when `--skip-questions` was persisted to the active stage flags.
12
+ */
13
+ skipQuestions?: boolean;
14
+ }
15
+ export interface QaLogFloorResult {
16
+ /** Whether the floor is satisfied (passes the gate). */
17
+ ok: boolean;
18
+ /** Substantive Q&A Log row count (excludes `skipped`/`waived` only rows). */
19
+ count: number;
20
+ /** Required minimum count from `questionBudgetHint(track, stage).min`. */
21
+ min: number;
22
+ /** Whether a stop-signal row was detected. */
23
+ hasStopSignal: boolean;
24
+ /** Whether the lite-tier short-circuit applies (lite track + count >= 1). */
25
+ liteShortCircuit: boolean;
26
+ /** Whether `--skip-questions` flag downgraded the finding to advisory. */
27
+ skipQuestionsAdvisory: boolean;
28
+ /** Human-readable details for the linter finding. */
29
+ details: string;
30
+ }
31
+ /**
32
+ * Evaluate the Q&A Log floor for a brainstorm / scope / design artifact.
33
+ * Returns ok=true when the floor is satisfied or any escape hatch fires.
34
+ *
35
+ * Escape hatches (any one is sufficient):
36
+ * - Q&A Log contains a stop-signal row.
37
+ * - `--skip-questions` flag was persisted to the active stage flags
38
+ * (passed via `options.skipQuestions=true`); finding downgrades to advisory.
39
+ * - Track is `quick` (lite tier ~ lightweight complexity) AND substantive
40
+ * count >= 1.
41
+ */
42
+ export declare function evaluateQaLogFloor(qaLogBody: string | null, track: FlowTrack, stage: FlowStage, options?: QaLogFloorOptions): QaLogFloorResult;
2
43
  export interface LintFinding {
3
44
  section: string;
4
45
  required: boolean;
@@ -116,11 +157,6 @@ export declare function validateRequirementsTaxonomy(sectionBody: string): {
116
157
  ok: boolean;
117
158
  details: string;
118
159
  };
119
- export declare function validateLockedDecisionAnchors(sectionBody: string): {
120
- ok: boolean;
121
- anchors: string[];
122
- details: string;
123
- };
124
160
  export interface InteractionEdgeCaseRequirement {
125
161
  label: string;
126
162
  pattern: RegExp;
@@ -220,8 +256,6 @@ export declare const SCOPE_REDUCTION_PATTERNS: Array<{
220
256
  export declare function parseFrontmatter(markdown: string): ParsedFrontmatter;
221
257
  export declare function extractDecisionIds(text: string): string[];
222
258
  export declare function extractRequirementIdsFromMarkdown(text: string): string[];
223
- export declare function extractLockedDecisionAnchors(text: string): string[];
224
- export declare function lockedDecisionHash(value: string): string;
225
259
  export declare function collectPatternHits(text: string, patterns: Array<{
226
260
  label: string;
227
261
  regex: RegExp;
@@ -245,4 +279,11 @@ export interface StageLintContext {
245
279
  staleDiagramAuditEnabled: boolean;
246
280
  isTrivialOverride: boolean;
247
281
  overrideSet: Set<string> | null;
282
+ /**
283
+ * Stage-level flags persisted to flow-state.json `activeRun.currentStage.flags`
284
+ * (or equivalent). Used as escape-hatch signal for the Q&A floor rule
285
+ * (e.g. `--skip-questions` downgrades `qa_log_below_min` to advisory).
286
+ * When orchestrator cannot read flow-state, defaults to an empty array.
287
+ */
288
+ activeStageFlags: string[];
248
289
  }
@@ -1,6 +1,128 @@
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
+ /**
5
+ * Recognized stop-signal phrases that satisfy the Q&A floor escape hatch
6
+ * when recorded as a Q&A Log row. Mirrors `Stop Signals (Natural Language)`
7
+ * in `adaptive-elicitation/SKILL.md`.
8
+ */
9
+ /**
10
+ * Stop-signal phrases. ASCII tokens use `\b` word boundaries; non-ASCII
11
+ * (RU/UA) tokens use Unicode-aware boundaries built from `\p{L}` so cyrillic
12
+ * characters around the phrase prevent partial matches without breaking on
13
+ * `\b`'s ASCII-only boundary semantics.
14
+ */
15
+ const QA_LOG_STOP_SIGNAL_PATTERNS = [
16
+ /\bstop[-\s]?signal\b/iu,
17
+ /\bachieved\s+enough\b/iu,
18
+ /\benough\b/iu,
19
+ /\bskip\b/iu,
20
+ /\bjust\s+draft\s+it\b/iu,
21
+ /\bstop\s+asking\b/iu,
22
+ /\bmove\s+on\b/iu,
23
+ /\bno\s+more\s+questions\b/iu,
24
+ /(?<![\p{L}\p{N}_])достаточно(?![\p{L}\p{N}_])/iu,
25
+ /(?<![\p{L}\p{N}_])хватит(?![\p{L}\p{N}_])/iu,
26
+ /(?<![\p{L}\p{N}_])давай\s+драфт(?![\p{L}\p{N}_])/iu,
27
+ /(?<![\p{L}\p{N}_])досить(?![\p{L}\p{N}_])/iu,
28
+ /(?<![\p{L}\p{N}_])вистачить(?![\p{L}\p{N}_])/iu,
29
+ /(?<![\p{L}\p{N}_])рухаємось\s+далі(?![\p{L}\p{N}_])/iu
30
+ ];
31
+ /**
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.
35
+ */
36
+ export const ELICITATION_STAGES = new Set([
37
+ "brainstorm",
38
+ "scope",
39
+ "design"
40
+ ]);
41
+ /**
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.
45
+ */
46
+ function isSubstantiveQaRow(cells) {
47
+ if (cells.length === 0)
48
+ return false;
49
+ const last = cells[cells.length - 1] ?? "";
50
+ const normalized = last.toLowerCase();
51
+ if (/^\s*(?:skipped|waived)\b/u.test(normalized))
52
+ return false;
53
+ return true;
54
+ }
55
+ /**
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.
58
+ */
59
+ function detectStopSignal(rows) {
60
+ for (const row of rows) {
61
+ const joined = row.join(" | ");
62
+ for (const pattern of QA_LOG_STOP_SIGNAL_PATTERNS) {
63
+ if (pattern.test(joined))
64
+ return true;
65
+ }
66
+ }
67
+ return false;
68
+ }
69
+ /**
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.
72
+ *
73
+ * Escape hatches (any one is sufficient):
74
+ * - Q&A Log contains a stop-signal row.
75
+ * - `--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.
79
+ */
80
+ export function evaluateQaLogFloor(qaLogBody, track, stage, options = {}) {
81
+ const hint = questionBudgetHint(track, stage);
82
+ const min = hint.min;
83
+ const rows = qaLogBody !== null ? getMarkdownTableRows(qaLogBody) : [];
84
+ const substantiveRows = rows.filter(isSubstantiveQaRow);
85
+ const count = substantiveRows.length;
86
+ 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;
95
+ let details;
96
+ if (ok) {
97
+ if (count >= min) {
98
+ details = `Q&A Log has ${count} substantive entries (floor for ${track}/${stage}: ${min}).`;
99
+ }
100
+ else if (hasStopSignal) {
101
+ details = `Q&A Log has ${count} substantive entries with an explicit user stop-signal row recorded (floor: ${min}).`;
102
+ }
103
+ else {
104
+ details = `Q&A Log has ${count} substantive entry under lightweight track short-circuit (default floor: ${min}).`;
105
+ }
106
+ }
107
+ 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.`;
112
+ }
113
+ 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.`;
115
+ }
116
+ return {
117
+ ok,
118
+ count,
119
+ min,
120
+ hasStopSignal,
121
+ liteShortCircuit,
122
+ skipQuestionsAdvisory,
123
+ details
124
+ };
125
+ }
4
126
  export function normalizeHeadingTitle(title) {
5
127
  return title.trim().replace(/\s+/g, " ");
6
128
  }
@@ -786,52 +908,6 @@ export function validateRequirementsTaxonomy(sectionBody) {
786
908
  details: "Requirements table uses canonical Priority values."
787
909
  };
788
910
  }
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
911
  export const INTERACTION_EDGE_CASE_REQUIREMENTS = [
836
912
  { label: "double-click", pattern: /\bdouble[\s-]?click\b/iu },
837
913
  {
@@ -1356,14 +1432,13 @@ export function extractRequirementIdsFromMarkdown(text) {
1356
1432
  const ids = text.match(/\bR\d+\b/gu) ?? [];
1357
1433
  return [...new Set(ids)];
1358
1434
  }
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
- }
1435
+ // `extractLockedDecisionAnchors` was removed in Wave 22 (v4.0.0) along
1436
+ // with the LD#<sha8> contract. Cross-stage decision traceability now uses
1437
+ // stable D-XX IDs.
1438
+ // `lockedDecisionHash` was removed in Wave 22 (v4.0.0) along with the
1439
+ // `Locked Decisions Hash Integrity` linter rule. Decision identity now
1440
+ // relies on stable D-XX IDs which the agent can edit safely without
1441
+ // recomputing content hashes.
1367
1442
  export function collectPatternHits(text, patterns) {
1368
1443
  const hits = [];
1369
1444
  for (const pattern of patterns) {
@@ -2,4 +2,14 @@ import type { FlowStage, FlowTrack } from "./types.js";
2
2
  import { type LintResult } from "./artifact-linter/shared.js";
3
3
  export { validateReviewArmy, checkReviewVerdictConsistency, checkReviewSecurityNoChangeAttestation, type ReviewVerdictConsistencyResult, type ReviewSecurityNoChangeAttestationResult } 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";
@@ -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
  }
@@ -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() {
@@ -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>