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.
- package/dist/artifact-linter/brainstorm.js +13 -2
- package/dist/artifact-linter/design.js +14 -3
- package/dist/artifact-linter/scope.js +20 -33
- package/dist/artifact-linter/shared.d.ts +48 -7
- package/dist/artifact-linter/shared.js +130 -55
- package/dist/artifact-linter.d.ts +11 -1
- package/dist/artifact-linter.js +28 -12
- package/dist/content/examples.js +8 -0
- package/dist/content/hooks.js +2 -1
- package/dist/content/review-prompts.js +3 -3
- package/dist/content/skills-elicitation.js +58 -20
- package/dist/content/skills.js +19 -6
- package/dist/content/stage-schema.js +36 -18
- package/dist/content/stages/brainstorm.js +3 -3
- package/dist/content/stages/design.js +2 -2
- package/dist/content/stages/plan.js +1 -1
- package/dist/content/stages/schema-types.d.ts +9 -0
- package/dist/content/stages/scope.js +5 -5
- package/dist/content/templates.d.ts +8 -1
- package/dist/content/templates.js +80 -18
- package/dist/gate-evidence.d.ts +25 -1
- package/dist/gate-evidence.js +34 -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/package.json +1 -1
|
@@ -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: "[
|
|
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: "[
|
|
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,
|
|
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_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
|
-
|
|
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,8 +100,8 @@ 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.`
|
|
@@ -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
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
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
|
|
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";
|
|
@@ -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
|
}
|
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() {
|
|
@@ -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>
|