cclaw-cli 2.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 +30 -16
- package/dist/cli.js +2 -9
- package/dist/config.d.ts +11 -67
- package/dist/config.js +59 -649
- package/dist/content/examples.js +8 -0
- package/dist/content/hook-events.js +0 -3
- package/dist/content/hook-manifest.d.ts +5 -2
- package/dist/content/hook-manifest.js +18 -64
- package/dist/content/hooks.js +2 -1
- package/dist/content/node-hooks.d.ts +0 -26
- package/dist/content/node-hooks.js +237 -105
- package/dist/content/observe.js +2 -1
- package/dist/content/opencode-plugin.js +1 -72
- 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 +4 -4
- package/dist/content/stages/plan.js +3 -3
- package/dist/content/stages/schema-types.d.ts +9 -0
- package/dist/content/stages/scope.js +8 -8
- package/dist/content/stages/tdd.js +11 -11
- 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 +35 -8
- package/dist/harness-adapters.js +8 -0
- package/dist/hook-schema.js +3 -0
- package/dist/hook-schemas/claude-hooks.v1.json +0 -2
- package/dist/hook-schemas/codex-hooks.v1.json +0 -3
- package/dist/hook-schemas/cursor-hooks.v1.json +0 -2
- package/dist/install.d.ts +2 -7
- package/dist/install.js +42 -131
- package/dist/internal/advance-stage/advance.d.ts +1 -0
- package/dist/internal/advance-stage/advance.js +5 -2
- package/dist/internal/compound-readiness.js +1 -16
- package/dist/internal/early-loop-status.js +1 -3
- package/dist/internal/runtime-integrity.js +0 -20
- package/dist/policy.js +6 -9
- package/dist/runtime/run-hook.mjs +237 -213
- package/dist/tdd-verification-evidence.js +6 -18
- package/dist/types.d.ts +0 -56
- 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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import { resolveArtifactPath as resolveStageArtifactPath } from "./artifact-paths.js";
|
|
3
|
-
import { readConfig } from "./config.js";
|
|
4
3
|
import { exists } from "./fs-utils.js";
|
|
5
4
|
import { stageSchema } from "./content/stage-schema.js";
|
|
6
|
-
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";
|
|
7
7
|
import { lintBrainstormStage } from "./artifact-linter/brainstorm.js";
|
|
8
8
|
import { lintDesignStage } from "./artifact-linter/design.js";
|
|
9
9
|
import { lintPlanStage } from "./artifact-linter/plan.js";
|
|
@@ -21,7 +21,7 @@ const FRONTMATTER_REQUIRED_KEYS = [
|
|
|
21
21
|
"locked_decisions",
|
|
22
22
|
"inputs_hash"
|
|
23
23
|
];
|
|
24
|
-
export async function lintArtifact(projectRoot, stage, track = "standard") {
|
|
24
|
+
export async function lintArtifact(projectRoot, stage, track = "standard", options = {}) {
|
|
25
25
|
const schema = stageSchema(stage, track);
|
|
26
26
|
const { absPath: absFile, relPath: relFile } = await resolveStageArtifactPath(stage, {
|
|
27
27
|
projectRoot,
|
|
@@ -58,7 +58,6 @@ export async function lintArtifact(projectRoot, stage, track = "standard") {
|
|
|
58
58
|
details: `Duplicate H2 heading(s): ${duplicateHeadings.join(", ")}. Merge edits into the existing heading to avoid split contracts.`
|
|
59
59
|
});
|
|
60
60
|
}
|
|
61
|
-
const projectConfig = await readConfig(projectRoot);
|
|
62
61
|
const parsedFrontmatter = parseFrontmatter(raw);
|
|
63
62
|
const frontmatterMissingKeys = FRONTMATTER_REQUIRED_KEYS.filter((key) => {
|
|
64
63
|
const value = parsedFrontmatter.values[key];
|
|
@@ -97,8 +96,8 @@ export async function lintArtifact(projectRoot, stage, track = "standard") {
|
|
|
97
96
|
});
|
|
98
97
|
const brainstormShortCircuitBody = stage === "brainstorm" ? sectionBodyByName(sections, "Short-Circuit Decision") : null;
|
|
99
98
|
const brainstormShortCircuitActivated = stage === "brainstorm" && isShortCircuitActivated(brainstormShortCircuitBody);
|
|
100
|
-
const scopePreAuditEnabled =
|
|
101
|
-
const staleDiagramAuditEnabled =
|
|
99
|
+
const scopePreAuditEnabled = true;
|
|
100
|
+
const staleDiagramAuditEnabled = true;
|
|
102
101
|
const isTrivialOverride = Boolean(schema.trivialOverrideSections &&
|
|
103
102
|
schema.trivialOverrideSections.length > 0 &&
|
|
104
103
|
(/trivial.change|mini.design|escape.hatch/iu.test(raw) ||
|
|
@@ -159,6 +158,21 @@ export async function lintArtifact(projectRoot, stage, track = "standard") {
|
|
|
159
158
|
details: `${learnings.details}${meaningfulStageNoneWarning}`
|
|
160
159
|
});
|
|
161
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
|
+
}
|
|
162
176
|
const stageContext = {
|
|
163
177
|
projectRoot,
|
|
164
178
|
stage,
|
|
@@ -173,7 +187,8 @@ export async function lintArtifact(projectRoot, stage, track = "standard") {
|
|
|
173
187
|
scopePreAuditEnabled,
|
|
174
188
|
staleDiagramAuditEnabled,
|
|
175
189
|
isTrivialOverride,
|
|
176
|
-
overrideSet
|
|
190
|
+
overrideSet,
|
|
191
|
+
activeStageFlags
|
|
177
192
|
};
|
|
178
193
|
switch (stage) {
|
|
179
194
|
case "brainstorm":
|
|
@@ -215,10 +230,9 @@ export async function lintArtifact(projectRoot, stage, track = "standard") {
|
|
|
215
230
|
const requirementsBody = sectionBodyByHeadingPrefix(scopeSections, "Requirements") ?? "";
|
|
216
231
|
const lockedDecisionsBody = sectionBodyByHeadingPrefix(scopeSections, "Locked Decisions") ?? "";
|
|
217
232
|
const requirementIds = extractRequirementIdsFromMarkdown(requirementsBody);
|
|
218
|
-
const
|
|
233
|
+
const decisionIds = Array.from(new Set((lockedDecisionsBody.match(/\bD-\d+\b/giu) ?? []).map((id) => id.toUpperCase())));
|
|
219
234
|
const missingRequirementRefs = requirementIds.filter((id) => !raw.includes(id));
|
|
220
|
-
const
|
|
221
|
-
const missingDecisionRefs = lockedDecisionAnchors.filter((id) => !normalizedCurrentRaw.includes(id));
|
|
235
|
+
const missingDecisionRefs = decisionIds.filter((id) => !raw.toUpperCase().includes(id));
|
|
222
236
|
findings.push({
|
|
223
237
|
section: "Scope Requirement Reference Integrity",
|
|
224
238
|
required: requirementIds.length > 0,
|
|
@@ -231,14 +245,14 @@ export async function lintArtifact(projectRoot, stage, track = "standard") {
|
|
|
231
245
|
: `Missing scope requirement reference(s): ${missingRequirementRefs.join(", ")}.`
|
|
232
246
|
});
|
|
233
247
|
findings.push({
|
|
234
|
-
section: "Locked Decision
|
|
235
|
-
required:
|
|
236
|
-
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.",
|
|
237
251
|
found: missingDecisionRefs.length === 0,
|
|
238
|
-
details:
|
|
239
|
-
? "No
|
|
252
|
+
details: decisionIds.length === 0
|
|
253
|
+
? "No D-XX decision IDs found in scope artifact; reference check skipped."
|
|
240
254
|
: missingDecisionRefs.length === 0
|
|
241
|
-
? `All ${
|
|
255
|
+
? `All ${decisionIds.length} locked decision ID(s) are referenced.`
|
|
242
256
|
: `Missing locked decision reference(s): ${missingDecisionRefs.join(", ")}.`
|
|
243
257
|
});
|
|
244
258
|
}
|
package/dist/cli.js
CHANGED
|
@@ -425,11 +425,8 @@ async function runCommand(parsed, ctx) {
|
|
|
425
425
|
info(ctx, `Detected harnesses from repo: ${resolved.detectedHarnesses.join(", ")}`);
|
|
426
426
|
}
|
|
427
427
|
ctx.stdout.write(`${JSON.stringify({
|
|
428
|
-
track:
|
|
428
|
+
track: effectiveTrack ?? "standard",
|
|
429
429
|
harnesses: previewConfig.harnesses,
|
|
430
|
-
strictness: previewConfig.strictness ?? "advisory",
|
|
431
|
-
gitHookGuards: previewConfig.gitHookGuards,
|
|
432
|
-
languageRulePacks: previewConfig.languageRulePacks,
|
|
433
430
|
generatedSurfaces: previewSurfaces
|
|
434
431
|
}, null, 2)}\n`);
|
|
435
432
|
return 0;
|
|
@@ -444,11 +441,7 @@ async function runCommand(parsed, ctx) {
|
|
|
444
441
|
}
|
|
445
442
|
const trackNote = effectiveTrack ? ` (track=${effectiveTrack})` : "";
|
|
446
443
|
info(ctx, `Initialized .cclaw runtime and generated harness shims${trackNote}`);
|
|
447
|
-
|
|
448
|
-
// `strictness` and `gitHookGuards` — without overselling the other knobs
|
|
449
|
-
// (those live behind docs/config.md until someone needs them).
|
|
450
|
-
info(ctx, "Config: .cclaw/config.yaml (strictness=advisory, gitHookGuards=false).");
|
|
451
|
-
info(ctx, "Need stricter guards or language rule packs? See docs/config.md.");
|
|
444
|
+
info(ctx, "Config: .cclaw/config.yaml (harnesses + auto-managed version stamps).");
|
|
452
445
|
await maybeEnableCodexHooksFlag(effectiveHarnesses, parsed, ctx);
|
|
453
446
|
return 0;
|
|
454
447
|
}
|