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.
- package/dist/artifact-linter/brainstorm.js +51 -2
- package/dist/artifact-linter/design.js +14 -3
- package/dist/artifact-linter/review-army.d.ts +25 -0
- package/dist/artifact-linter/review-army.js +155 -0
- package/dist/artifact-linter/review.js +13 -0
- package/dist/artifact-linter/scope.js +27 -48
- package/dist/artifact-linter/shared.d.ts +98 -11
- package/dist/artifact-linter/shared.js +280 -113
- package/dist/artifact-linter.d.ts +12 -2
- package/dist/artifact-linter.js +29 -13
- package/dist/content/core-agents.js +6 -1
- package/dist/content/examples.js +8 -0
- package/dist/content/hooks.js +2 -1
- package/dist/content/idea.js +14 -2
- package/dist/content/review-prompts.js +3 -3
- package/dist/content/skills-elicitation.js +61 -20
- package/dist/content/skills.js +19 -6
- package/dist/content/stage-schema.js +46 -18
- package/dist/content/stages/_lint-metadata/index.js +1 -2
- package/dist/content/stages/brainstorm.js +6 -3
- package/dist/content/stages/design.js +13 -12
- package/dist/content/stages/plan.js +1 -1
- package/dist/content/stages/review.js +21 -21
- package/dist/content/stages/schema-types.d.ts +9 -0
- package/dist/content/stages/scope.js +22 -20
- package/dist/content/stages/spec.js +3 -3
- package/dist/content/stages/tdd.js +1 -0
- package/dist/content/templates.d.ts +8 -1
- package/dist/content/templates.js +115 -43
- package/dist/flow-state.d.ts +12 -0
- package/dist/gate-evidence.d.ts +37 -1
- package/dist/gate-evidence.js +37 -3
- package/dist/harness-adapters.js +8 -0
- package/dist/install.js +22 -11
- package/dist/internal/advance-stage/advance.d.ts +1 -0
- package/dist/internal/advance-stage/advance.js +5 -2
- package/dist/internal/advance-stage/parsers.d.ts +8 -0
- package/dist/internal/advance-stage/parsers.js +27 -1
- package/dist/internal/advance-stage/start-flow.js +13 -0
- package/dist/run-persistence.js +14 -2
- 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
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
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
|
|
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>;
|
package/dist/artifact-linter.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
|
233
|
-
required:
|
|
234
|
-
rule: "Every
|
|
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:
|
|
237
|
-
? "No
|
|
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 ${
|
|
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
|
-
"
|
|
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`",
|
package/dist/content/examples.js
CHANGED
|
@@ -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
|
package/dist/content/hooks.js
CHANGED
|
@@ -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() {
|
package/dist/content/idea.js
CHANGED
|
@@ -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
|
|
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
|
|
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#/
|
|
98
|
+
- [R#/D-XX]: <specific issue> — <why it matters>
|
|
99
99
|
|
|
100
100
|
**Recommendations:**
|
|
101
101
|
- <advisory item or None>
|