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,7 @@
|
|
|
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
|
+
import { readFlowState } from "../run-persistence.js";
|
|
4
5
|
export async function lintBrainstormStage(ctx) {
|
|
5
6
|
const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride } = ctx;
|
|
6
7
|
const qaLogBody = sectionBodyByName(sections, "Q&A Log");
|
|
@@ -9,7 +10,7 @@ export async function lintBrainstormStage(ctx) {
|
|
|
9
10
|
findings.push({
|
|
10
11
|
section: "qa_log_missing",
|
|
11
12
|
required: false,
|
|
12
|
-
rule: "[
|
|
13
|
+
rule: "[P2] qa_log_missing — Q&A Log empty — confirm you actually had a dialogue with the user, not a draft from memory.",
|
|
13
14
|
found: qaLogOk,
|
|
14
15
|
details: qaLogOk
|
|
15
16
|
? `Q&A Log contains ${qaLogRows.length} data row(s).`
|
|
@@ -17,6 +18,17 @@ export async function lintBrainstormStage(ctx) {
|
|
|
17
18
|
? "Missing `## Q&A Log` section."
|
|
18
19
|
: "Q&A Log is present but has zero data rows."
|
|
19
20
|
});
|
|
21
|
+
if (!brainstormShortCircuitActivated) {
|
|
22
|
+
const skipQuestions = ctx.activeStageFlags.includes("--skip-questions");
|
|
23
|
+
const floor = evaluateQaLogFloor(qaLogBody, track, "brainstorm", { skipQuestions });
|
|
24
|
+
findings.push({
|
|
25
|
+
section: "qa_log_unconverged",
|
|
26
|
+
required: !floor.skipQuestionsAdvisory,
|
|
27
|
+
rule: "[P1] qa_log_unconverged — Q&A Log has not converged for this stage. Continue elicitation until forcing-question topics are addressed, the last 2 rows produce no decision-changing impact (Ralph-Loop), or an explicit user stop-signal row is appended.",
|
|
28
|
+
found: floor.ok,
|
|
29
|
+
details: floor.details
|
|
30
|
+
});
|
|
31
|
+
}
|
|
20
32
|
// Brainstorm Iron Law: "NO ARTIFACT IS COMPLETE WITHOUT AN EXPLICITLY
|
|
21
33
|
// APPROVED DIRECTION — SILENCE IS NOT APPROVAL." Previously this was
|
|
22
34
|
// prose-only — nothing failed when the Selected Direction section
|
|
@@ -268,6 +280,43 @@ export async function lintBrainstormStage(ctx) {
|
|
|
268
280
|
: `Outside Voice section is missing field(s): ${missing.join(", ")}.`
|
|
269
281
|
});
|
|
270
282
|
}
|
|
283
|
+
let ideaHint = {};
|
|
284
|
+
try {
|
|
285
|
+
const flowState = await readFlowState(projectRoot);
|
|
286
|
+
const hint = flowState.interactionHints?.brainstorm;
|
|
287
|
+
if (hint) {
|
|
288
|
+
ideaHint = {
|
|
289
|
+
fromIdeaArtifact: hint.fromIdeaArtifact,
|
|
290
|
+
fromIdeaCandidateId: hint.fromIdeaCandidateId
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
ideaHint = {};
|
|
296
|
+
}
|
|
297
|
+
if (ideaHint.fromIdeaArtifact) {
|
|
298
|
+
const carryBody = sectionBodyByName(sections, "Idea Evidence Carry-forward");
|
|
299
|
+
const hasSection = carryBody !== null && meaningfulLineCount(carryBody) > 0;
|
|
300
|
+
const sourceCited = hasSection &&
|
|
301
|
+
carryBody.includes(ideaHint.fromIdeaArtifact);
|
|
302
|
+
const candidateCited = ideaHint.fromIdeaCandidateId
|
|
303
|
+
? hasSection && carryBody.toUpperCase().includes(ideaHint.fromIdeaCandidateId.toUpperCase())
|
|
304
|
+
: true;
|
|
305
|
+
const ok = hasSection && sourceCited && candidateCited;
|
|
306
|
+
findings.push({
|
|
307
|
+
section: "brainstorm.idea_evidence_carry_forward",
|
|
308
|
+
required: true,
|
|
309
|
+
rule: "[P1] brainstorm.idea_evidence_carry_forward — when `flow-state.interactionHints.brainstorm.fromIdeaArtifact` is set (Wave 23 / v5.0.0), the brainstorm artifact MUST include `## Idea Evidence Carry-forward` citing the idea artifact path and chosen `I-#`. Reuse divergent + critique + rank work from `/cc-ideate` as the `baseline` Approach; only newly generate the higher-upside challenger row(s).",
|
|
310
|
+
found: ok,
|
|
311
|
+
details: ok
|
|
312
|
+
? `Idea Evidence Carry-forward cites ${ideaHint.fromIdeaArtifact}${ideaHint.fromIdeaCandidateId ? ` (${ideaHint.fromIdeaCandidateId})` : ""}.`
|
|
313
|
+
: !hasSection
|
|
314
|
+
? `Brainstorm started from /cc-ideate (artifact ${ideaHint.fromIdeaArtifact}${ideaHint.fromIdeaCandidateId ? `, candidate ${ideaHint.fromIdeaCandidateId}` : ""}) but \`## Idea Evidence Carry-forward\` is missing or empty.`
|
|
315
|
+
: !sourceCited
|
|
316
|
+
? `\`## Idea Evidence Carry-forward\` does not cite the source idea artifact path \`${ideaHint.fromIdeaArtifact}\`.`
|
|
317
|
+
: `\`## Idea Evidence Carry-forward\` does not cite the chosen candidate id \`${ideaHint.fromIdeaCandidateId ?? ""}\`.`
|
|
318
|
+
});
|
|
319
|
+
}
|
|
271
320
|
const wavePlansDir = path.join(projectRoot, ".cclaw", "wave-plans");
|
|
272
321
|
let wavePlanEntries = [];
|
|
273
322
|
try {
|
|
@@ -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: "[
|
|
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_unconverged",
|
|
226
|
+
required: !floor.skipQuestionsAdvisory,
|
|
227
|
+
rule: "[P1] qa_log_unconverged — Q&A Log has not converged for this stage. Continue elicitation until forcing-question topics are addressed, the last 2 rows produce no decision-changing impact (Ralph-Loop), or an explicit user stop-signal row is appended.",
|
|
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({
|
|
@@ -21,4 +21,29 @@ export interface ReviewSecurityNoChangeAttestationResult {
|
|
|
21
21
|
* APPROVED while open Critical findings or shipBlockers remain.
|
|
22
22
|
*/
|
|
23
23
|
export declare function checkReviewVerdictConsistency(projectRoot: string): Promise<ReviewVerdictConsistencyResult>;
|
|
24
|
+
export interface ReviewTddDuplicationConflict {
|
|
25
|
+
findingId: string;
|
|
26
|
+
tddSeverity: string | null;
|
|
27
|
+
reviewSeverity: string | null;
|
|
28
|
+
tddDisposition: string | null;
|
|
29
|
+
reviewDisposition: string | null;
|
|
30
|
+
}
|
|
31
|
+
export interface ReviewTddDuplicationResult {
|
|
32
|
+
ok: boolean;
|
|
33
|
+
errors: string[];
|
|
34
|
+
conflicts: ReviewTddDuplicationConflict[];
|
|
35
|
+
tddArtifactExists: boolean;
|
|
36
|
+
reviewArtifactExists: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Cross-artifact duplication guard (Wave 23 / v5.0.0).
|
|
40
|
+
*
|
|
41
|
+
* When the same finding ID (`F-NN`) appears in both
|
|
42
|
+
* `06-tdd.md > Per-Slice Review` and `07-review-army.json`, the
|
|
43
|
+
* severity and disposition MUST match. Per-slice tdd reviews own
|
|
44
|
+
* single-slice findings; review cites them, never re-classifies.
|
|
45
|
+
*
|
|
46
|
+
* If neither artifact uses `F-NN` IDs, the check is a no-op.
|
|
47
|
+
*/
|
|
48
|
+
export declare function checkReviewTddNoCrossArtifactDuplication(projectRoot: string): Promise<ReviewTddDuplicationResult>;
|
|
24
49
|
export declare function checkReviewSecurityNoChangeAttestation(projectRoot: string): Promise<ReviewSecurityNoChangeAttestationResult>;
|
|
@@ -319,6 +319,161 @@ export async function checkReviewVerdictConsistency(projectRoot) {
|
|
|
319
319
|
shipBlockerCount
|
|
320
320
|
};
|
|
321
321
|
}
|
|
322
|
+
const FINDING_ID_PATTERN = /\bF-\d+\b/giu;
|
|
323
|
+
const SEVERITY_TOKENS = ["Critical", "Important", "Suggestion"];
|
|
324
|
+
const DISPOSITION_TOKENS = ["open", "accepted", "resolved", "deferred", "won't-fix", "wont-fix"];
|
|
325
|
+
function findFirstToken(text, tokens) {
|
|
326
|
+
for (const token of tokens) {
|
|
327
|
+
const escaped = token.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
328
|
+
const regex = new RegExp(`\\b${escaped}\\b`, "iu");
|
|
329
|
+
if (regex.test(text))
|
|
330
|
+
return token;
|
|
331
|
+
}
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
function normalizeDisposition(value) {
|
|
335
|
+
if (value === null)
|
|
336
|
+
return null;
|
|
337
|
+
const lower = value.toLowerCase();
|
|
338
|
+
if (lower === "wont-fix" || lower === "won't-fix")
|
|
339
|
+
return "won't-fix";
|
|
340
|
+
return lower;
|
|
341
|
+
}
|
|
342
|
+
function extractTddPerSliceFindings(perSliceBody) {
|
|
343
|
+
const rows = new Map();
|
|
344
|
+
const lines = perSliceBody.split(/\r?\n/u);
|
|
345
|
+
for (const line of lines) {
|
|
346
|
+
const ids = line.match(FINDING_ID_PATTERN);
|
|
347
|
+
if (!ids || ids.length === 0)
|
|
348
|
+
continue;
|
|
349
|
+
const severity = findFirstToken(line, SEVERITY_TOKENS);
|
|
350
|
+
const disposition = normalizeDisposition(findFirstToken(line, DISPOSITION_TOKENS));
|
|
351
|
+
for (const rawId of ids) {
|
|
352
|
+
const id = rawId.toUpperCase();
|
|
353
|
+
if (rows.has(id))
|
|
354
|
+
continue;
|
|
355
|
+
rows.set(id, { id, severity, disposition });
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return rows;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Cross-artifact duplication guard (Wave 23 / v5.0.0).
|
|
362
|
+
*
|
|
363
|
+
* When the same finding ID (`F-NN`) appears in both
|
|
364
|
+
* `06-tdd.md > Per-Slice Review` and `07-review-army.json`, the
|
|
365
|
+
* severity and disposition MUST match. Per-slice tdd reviews own
|
|
366
|
+
* single-slice findings; review cites them, never re-classifies.
|
|
367
|
+
*
|
|
368
|
+
* If neither artifact uses `F-NN` IDs, the check is a no-op.
|
|
369
|
+
*/
|
|
370
|
+
export async function checkReviewTddNoCrossArtifactDuplication(projectRoot) {
|
|
371
|
+
const tddPath = path.join(projectRoot, RUNTIME_ROOT, "artifacts", "06-tdd.md");
|
|
372
|
+
const armyPath = path.join(projectRoot, RUNTIME_ROOT, "artifacts", "07-review-army.json");
|
|
373
|
+
const tddArtifactExists = await exists(tddPath);
|
|
374
|
+
const reviewArtifactExists = await exists(armyPath);
|
|
375
|
+
if (!tddArtifactExists || !reviewArtifactExists) {
|
|
376
|
+
return {
|
|
377
|
+
ok: true,
|
|
378
|
+
errors: [],
|
|
379
|
+
conflicts: [],
|
|
380
|
+
tddArtifactExists,
|
|
381
|
+
reviewArtifactExists
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
const tddRaw = await fs.readFile(tddPath, "utf8");
|
|
385
|
+
const tddSections = extractH2Sections(tddRaw);
|
|
386
|
+
const perSliceBody = sectionBodyByName(tddSections, "Per-Slice Review");
|
|
387
|
+
if (!perSliceBody) {
|
|
388
|
+
return {
|
|
389
|
+
ok: true,
|
|
390
|
+
errors: [],
|
|
391
|
+
conflicts: [],
|
|
392
|
+
tddArtifactExists,
|
|
393
|
+
reviewArtifactExists
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
const tddFindings = extractTddPerSliceFindings(perSliceBody);
|
|
397
|
+
if (tddFindings.size === 0) {
|
|
398
|
+
return {
|
|
399
|
+
ok: true,
|
|
400
|
+
errors: [],
|
|
401
|
+
conflicts: [],
|
|
402
|
+
tddArtifactExists,
|
|
403
|
+
reviewArtifactExists
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
let parsed;
|
|
407
|
+
try {
|
|
408
|
+
parsed = JSON.parse(await fs.readFile(armyPath, "utf8"));
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
return {
|
|
412
|
+
ok: true,
|
|
413
|
+
errors: [],
|
|
414
|
+
conflicts: [],
|
|
415
|
+
tddArtifactExists,
|
|
416
|
+
reviewArtifactExists
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
420
|
+
return {
|
|
421
|
+
ok: true,
|
|
422
|
+
errors: [],
|
|
423
|
+
conflicts: [],
|
|
424
|
+
tddArtifactExists,
|
|
425
|
+
reviewArtifactExists
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
const root = parsed;
|
|
429
|
+
const findings = Array.isArray(root.findings) ? root.findings : [];
|
|
430
|
+
const conflicts = [];
|
|
431
|
+
for (const f of findings) {
|
|
432
|
+
if (!f || typeof f !== "object" || Array.isArray(f))
|
|
433
|
+
continue;
|
|
434
|
+
const o = f;
|
|
435
|
+
if (typeof o.id !== "string")
|
|
436
|
+
continue;
|
|
437
|
+
const id = o.id.toUpperCase();
|
|
438
|
+
const tddRow = tddFindings.get(id);
|
|
439
|
+
if (!tddRow)
|
|
440
|
+
continue;
|
|
441
|
+
const reviewSeverity = typeof o.severity === "string" ? o.severity : null;
|
|
442
|
+
const reviewDisposition = normalizeDisposition(typeof o.status === "string" ? o.status : null);
|
|
443
|
+
const severityMismatch = tddRow.severity !== null &&
|
|
444
|
+
reviewSeverity !== null &&
|
|
445
|
+
tddRow.severity.toLowerCase() !== reviewSeverity.toLowerCase();
|
|
446
|
+
const dispositionMismatch = tddRow.disposition !== null &&
|
|
447
|
+
reviewDisposition !== null &&
|
|
448
|
+
tddRow.disposition !== reviewDisposition;
|
|
449
|
+
if (severityMismatch || dispositionMismatch) {
|
|
450
|
+
conflicts.push({
|
|
451
|
+
findingId: id,
|
|
452
|
+
tddSeverity: tddRow.severity,
|
|
453
|
+
reviewSeverity,
|
|
454
|
+
tddDisposition: tddRow.disposition,
|
|
455
|
+
reviewDisposition
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
const errors = conflicts.map((c) => {
|
|
460
|
+
const parts = [];
|
|
461
|
+
if (c.tddSeverity !== null && c.reviewSeverity !== null && c.tddSeverity.toLowerCase() !== c.reviewSeverity.toLowerCase()) {
|
|
462
|
+
parts.push(`severity tdd=${c.tddSeverity} vs review-army=${c.reviewSeverity}`);
|
|
463
|
+
}
|
|
464
|
+
if (c.tddDisposition !== null && c.reviewDisposition !== null && c.tddDisposition !== c.reviewDisposition) {
|
|
465
|
+
parts.push(`disposition tdd=${c.tddDisposition} vs review-army=${c.reviewDisposition}`);
|
|
466
|
+
}
|
|
467
|
+
return `Finding ${c.findingId} appears in both 06-tdd.md > Per-Slice Review and 07-review-army.json with mismatched ${parts.join(" and ")}. Review must cite, not re-classify.`;
|
|
468
|
+
});
|
|
469
|
+
return {
|
|
470
|
+
ok: errors.length === 0,
|
|
471
|
+
errors,
|
|
472
|
+
conflicts,
|
|
473
|
+
tddArtifactExists,
|
|
474
|
+
reviewArtifactExists
|
|
475
|
+
};
|
|
476
|
+
}
|
|
322
477
|
export async function checkReviewSecurityNoChangeAttestation(projectRoot) {
|
|
323
478
|
const reviewMdPath = path.join(projectRoot, RUNTIME_ROOT, "artifacts", "07-review.md");
|
|
324
479
|
if (!(await exists(reviewMdPath))) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { markdownFieldRegex, sectionBodyByName } from "./shared.js";
|
|
2
|
+
import { checkReviewTddNoCrossArtifactDuplication } from "./review-army.js";
|
|
2
3
|
export async function lintReviewStage(ctx) {
|
|
3
4
|
const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride } = ctx;
|
|
4
5
|
// Universal Layer 2.7 structural checks (superpowers requesting + receiving).
|
|
@@ -62,6 +63,18 @@ export async function lintReviewStage(ctx) {
|
|
|
62
63
|
: "Receiving Posture is missing the anti-sycophancy acknowledgement line."
|
|
63
64
|
});
|
|
64
65
|
}
|
|
66
|
+
const dupResult = await checkReviewTddNoCrossArtifactDuplication(projectRoot);
|
|
67
|
+
findings.push({
|
|
68
|
+
section: "review.no_cross_artifact_duplication",
|
|
69
|
+
required: true,
|
|
70
|
+
rule: "[P1] review.no_cross_artifact_duplication — when a finding ID appears in both `06-tdd.md > Per-Slice Review` and `07-review-army.json`, severity and disposition must match (review cites tdd; never re-classifies).",
|
|
71
|
+
found: dupResult.ok,
|
|
72
|
+
details: dupResult.ok
|
|
73
|
+
? dupResult.tddArtifactExists && dupResult.reviewArtifactExists
|
|
74
|
+
? "No cross-artifact severity/disposition conflicts between tdd Per-Slice Review and review-army findings."
|
|
75
|
+
: "Skipped: tdd Per-Slice Review or review-army artifact not present."
|
|
76
|
+
: dupResult.errors.join(" ")
|
|
77
|
+
});
|
|
65
78
|
const lensCoverageBody = sectionBodyByName(sections, "Lens Coverage");
|
|
66
79
|
if (lensCoverageBody === null) {
|
|
67
80
|
findings.push({
|
|
@@ -1,24 +1,17 @@
|
|
|
1
|
-
import { checkCriticPredictionsContract, sectionBodyByHeadingPrefix, sectionBodyByName, extractCanonicalScopeMode,
|
|
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: "[
|
|
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_unconverged",
|
|
27
|
+
required: !floor.skipQuestionsAdvisory,
|
|
28
|
+
rule: "[P1] qa_log_unconverged — Q&A Log has not converged for this stage. Continue elicitation until forcing-question topics are addressed, the last 2 rows produce no decision-changing impact (Ralph-Loop), or an explicit user stop-signal row is appended.",
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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,27 +100,19 @@ export async function lintScopeStage(ctx) {
|
|
|
113
100
|
}
|
|
114
101
|
findings.push({
|
|
115
102
|
section: "Locked Decisions ID Integrity",
|
|
116
|
-
required:
|
|
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.`
|
|
121
108
|
: issues.join("; ")
|
|
122
109
|
});
|
|
123
110
|
}
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
required: true,
|
|
132
|
-
rule: "Implementation Alternatives must conclude with a `RECOMMENDATION:` line citing the chosen option and rationale.",
|
|
133
|
-
found: recommendation,
|
|
134
|
-
details: recommendation
|
|
135
|
-
? "Recommendation marker present."
|
|
136
|
-
: "Missing or empty `RECOMMENDATION:` line under Implementation Alternatives."
|
|
137
|
-
});
|
|
138
|
-
}
|
|
111
|
+
// Wave 23 (v5.0.0): scope no longer owns architecture-tier alternatives
|
|
112
|
+
// (`## Implementation Alternatives` was removed from the scope template
|
|
113
|
+
// and stage schema). Design OWNS the architecture-tier decision via
|
|
114
|
+
// `## Architecture Decision Record (ADR)` and `## Engineering Lock`.
|
|
115
|
+
// The legacy linter rule `Implementation Alternatives Recommendation`
|
|
116
|
+
// was removed in Wave 23 — if a legacy artifact still has the section,
|
|
117
|
+
// it is now treated as informational only.
|
|
139
118
|
}
|
|
@@ -1,4 +1,95 @@
|
|
|
1
1
|
import { type FlowStage, type FlowTrack } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Stages that run adaptive elicitation. The `qa_log_unconverged` rule
|
|
4
|
+
* only fires for these. Other stages may still record a Q&A Log but no
|
|
5
|
+
* convergence floor is enforced.
|
|
6
|
+
*/
|
|
7
|
+
export declare const ELICITATION_STAGES: ReadonlySet<FlowStage>;
|
|
8
|
+
export interface QaLogFloorOptions {
|
|
9
|
+
/**
|
|
10
|
+
* When true, downgrades the finding to advisory (`required: false`).
|
|
11
|
+
* Set when `--skip-questions` was persisted to the active stage flags.
|
|
12
|
+
*/
|
|
13
|
+
skipQuestions?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Optional pre-extracted forcing-question topics. When omitted, the
|
|
16
|
+
* evaluator calls `extractForcingQuestions(stage)` which scans the
|
|
17
|
+
* stage's checklist row.
|
|
18
|
+
*/
|
|
19
|
+
forcingQuestions?: string[];
|
|
20
|
+
}
|
|
21
|
+
export interface QaLogFloorResult {
|
|
22
|
+
/** Whether convergence is satisfied (passes the gate). */
|
|
23
|
+
ok: boolean;
|
|
24
|
+
/** Substantive Q&A Log row count (excludes `skipped`/`waived` only rows). */
|
|
25
|
+
count: number;
|
|
26
|
+
/**
|
|
27
|
+
* Legacy field, retained for harness UI compatibility. Always 0 in
|
|
28
|
+
* Wave 23 — the convergence floor no longer enforces a fixed count.
|
|
29
|
+
* Harness can still surface `questionBudgetHint(track, stage).recommended`
|
|
30
|
+
* as a soft hint, but it is NOT tied to gate blocking.
|
|
31
|
+
*/
|
|
32
|
+
min: number;
|
|
33
|
+
/** Whether a stop-signal row was detected. */
|
|
34
|
+
hasStopSignal: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Legacy field, retained for harness UI compatibility. Always false in
|
|
37
|
+
* Wave 23 — convergence semantics replaced the lite-tier short-circuit.
|
|
38
|
+
*/
|
|
39
|
+
liteShortCircuit: boolean;
|
|
40
|
+
/** Whether `--skip-questions` flag downgraded the finding to advisory. */
|
|
41
|
+
skipQuestionsAdvisory: boolean;
|
|
42
|
+
/** Forcing-question topics deemed addressed (substring match in Q&A). */
|
|
43
|
+
forcingCovered: string[];
|
|
44
|
+
/** Forcing-question topics still pending (no matching Q&A row). */
|
|
45
|
+
forcingPending: string[];
|
|
46
|
+
/**
|
|
47
|
+
* True when the last 2 substantive rows have decision_impact marking
|
|
48
|
+
* `skip`/`continue`/`no-change`/`done`/etc. — i.e. Q&A is no longer
|
|
49
|
+
* surfacing decision-changing answers (Ralph-Loop convergence detector).
|
|
50
|
+
*/
|
|
51
|
+
noNewDecisions: boolean;
|
|
52
|
+
/** Human-readable details for the linter finding. */
|
|
53
|
+
details: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Extract forcing-question topics from a stage's checklist. Looks for
|
|
57
|
+
* the canonical `**<Stage> forcing questions (must be covered or
|
|
58
|
+
* explicitly waived)** — <topic1>, <topic2>, ...` row and tokenizes the
|
|
59
|
+
* comma-separated topic list. Returns trimmed topic strings stripped of
|
|
60
|
+
* leading question words (`what`/`who`/`where`/`which`/`how`/`is`/`do`/`does`).
|
|
61
|
+
*
|
|
62
|
+
* Returns empty array when no forcing-questions row is present (caller
|
|
63
|
+
* should treat absence as "no forcing requirement" — convergence falls
|
|
64
|
+
* back to the no-new-decisions / stop-signal detectors).
|
|
65
|
+
*/
|
|
66
|
+
export declare function extractForcingQuestions(stage: FlowStage): string[];
|
|
67
|
+
/**
|
|
68
|
+
* Evaluate the Q&A Log convergence floor for a brainstorm / scope /
|
|
69
|
+
* design artifact. Returns ok=true when convergence is reached or any
|
|
70
|
+
* escape hatch fires.
|
|
71
|
+
*
|
|
72
|
+
* Convergence sources (any one is sufficient):
|
|
73
|
+
* - All forcing-question topics from the stage checklist appear addressed
|
|
74
|
+
* in `## Q&A Log` (substring keyword match in question/answer columns).
|
|
75
|
+
* - The Ralph-Loop convergence detector reports the last 2 substantive
|
|
76
|
+
* rows have decision_impact marking `skip`/`continue`/`no-change`/`done`
|
|
77
|
+
* (i.e. the dialogue is no longer producing decision-changing rows).
|
|
78
|
+
* - Q&A Log contains a stop-signal row (existing
|
|
79
|
+
* `QA_LOG_STOP_SIGNAL_PATTERNS` keep working).
|
|
80
|
+
* - `--skip-questions` flag was persisted to the active stage flags
|
|
81
|
+
* (`options.skipQuestions=true`); finding downgrades to advisory.
|
|
82
|
+
* - The stage checklist exposes no forcing-questions row (e.g. simple
|
|
83
|
+
* refactor) AND the artifact has at least one substantive row — treat
|
|
84
|
+
* as converged because there is nothing left to force.
|
|
85
|
+
*
|
|
86
|
+
* Wave 23 (v5.0.0) replaces the count-based `qa_log_below_min` rule with
|
|
87
|
+
* `qa_log_unconverged`. The fixed count constant (10 for standard) and
|
|
88
|
+
* the `CCLAW_ELICITATION_FLOOR=advisory` env override were removed. The
|
|
89
|
+
* `min` and `liteShortCircuit` fields on the result are retained for
|
|
90
|
+
* harness UI compatibility but are always 0/false.
|
|
91
|
+
*/
|
|
92
|
+
export declare function evaluateQaLogFloor(qaLogBody: string | null, track: FlowTrack, stage: FlowStage, options?: QaLogFloorOptions): QaLogFloorResult;
|
|
2
93
|
export interface LintFinding {
|
|
3
94
|
section: string;
|
|
4
95
|
required: boolean;
|
|
@@ -89,10 +180,6 @@ export declare function canonicalModesInText(text: string): CanonicalScopeMode[]
|
|
|
89
180
|
export declare function shortModeToCanonical(text: string): CanonicalScopeMode | null;
|
|
90
181
|
export declare function canonicalModeFromCandidate(candidate: string): CanonicalScopeMode | null;
|
|
91
182
|
export declare function extractCanonicalScopeMode(body: string): CanonicalScopeMode | null;
|
|
92
|
-
export declare function validatePremiseChallenge(sectionBody: string): {
|
|
93
|
-
ok: boolean;
|
|
94
|
-
details: string;
|
|
95
|
-
};
|
|
96
183
|
export declare function validateScopeSummary(sectionBody: string): {
|
|
97
184
|
ok: boolean;
|
|
98
185
|
details: string;
|
|
@@ -116,11 +203,6 @@ export declare function validateRequirementsTaxonomy(sectionBody: string): {
|
|
|
116
203
|
ok: boolean;
|
|
117
204
|
details: string;
|
|
118
205
|
};
|
|
119
|
-
export declare function validateLockedDecisionAnchors(sectionBody: string): {
|
|
120
|
-
ok: boolean;
|
|
121
|
-
anchors: string[];
|
|
122
|
-
details: string;
|
|
123
|
-
};
|
|
124
206
|
export interface InteractionEdgeCaseRequirement {
|
|
125
207
|
label: string;
|
|
126
208
|
pattern: RegExp;
|
|
@@ -220,8 +302,6 @@ export declare const SCOPE_REDUCTION_PATTERNS: Array<{
|
|
|
220
302
|
export declare function parseFrontmatter(markdown: string): ParsedFrontmatter;
|
|
221
303
|
export declare function extractDecisionIds(text: string): string[];
|
|
222
304
|
export declare function extractRequirementIdsFromMarkdown(text: string): string[];
|
|
223
|
-
export declare function extractLockedDecisionAnchors(text: string): string[];
|
|
224
|
-
export declare function lockedDecisionHash(value: string): string;
|
|
225
305
|
export declare function collectPatternHits(text: string, patterns: Array<{
|
|
226
306
|
label: string;
|
|
227
307
|
regex: RegExp;
|
|
@@ -245,4 +325,11 @@ export interface StageLintContext {
|
|
|
245
325
|
staleDiagramAuditEnabled: boolean;
|
|
246
326
|
isTrivialOverride: boolean;
|
|
247
327
|
overrideSet: Set<string> | null;
|
|
328
|
+
/**
|
|
329
|
+
* Stage-level flags persisted to flow-state.json `activeRun.currentStage.flags`
|
|
330
|
+
* (or equivalent). Used as escape-hatch signal for the Q&A floor rule
|
|
331
|
+
* (e.g. `--skip-questions` downgrades `qa_log_unconverged` to advisory).
|
|
332
|
+
* When orchestrator cannot read flow-state, defaults to an empty array.
|
|
333
|
+
*/
|
|
334
|
+
activeStageFlags: string[];
|
|
248
335
|
}
|