cclaw-cli 0.55.2 → 2.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/README.md +3 -3
- package/dist/artifact-linter/brainstorm.js +59 -1
- package/dist/artifact-linter/design.js +46 -1
- package/dist/artifact-linter/plan.js +22 -1
- package/dist/artifact-linter/review.js +35 -1
- package/dist/artifact-linter/scope.js +33 -9
- package/dist/artifact-linter/shared.d.ts +12 -10
- package/dist/artifact-linter/shared.js +102 -41
- package/dist/artifact-linter/ship.js +36 -0
- package/dist/artifact-linter/spec.js +23 -1
- package/dist/artifact-linter/tdd.js +74 -0
- package/dist/artifact-linter.d.ts +1 -1
- package/dist/artifact-linter.js +11 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -0
- package/dist/content/closeout-guidance.d.ts +1 -1
- package/dist/content/closeout-guidance.js +10 -11
- package/dist/content/core-agents.d.ts +35 -36
- package/dist/content/core-agents.js +189 -99
- package/dist/content/diff-command.js +1 -1
- package/dist/content/examples.d.ts +0 -3
- package/dist/content/examples.js +197 -752
- package/dist/content/hook-events.js +1 -2
- package/dist/content/hook-manifest.d.ts +3 -4
- package/dist/content/hook-manifest.js +22 -25
- package/dist/content/hooks.js +54 -14
- package/dist/content/idea.d.ts +60 -0
- package/dist/content/idea.js +404 -0
- package/dist/content/learnings.d.ts +2 -4
- package/dist/content/learnings.js +10 -26
- package/dist/content/meta-skill.js +4 -3
- package/dist/content/node-hooks.js +368 -164
- package/dist/content/observe.js +3 -3
- package/dist/content/opencode-plugin.js +12 -32
- package/dist/content/reference-patterns.js +2 -2
- package/dist/content/runtime-shared-snippets.d.ts +8 -0
- package/dist/content/runtime-shared-snippets.js +80 -0
- package/dist/content/session-hooks.js +1 -1
- package/dist/content/skills-elicitation.d.ts +1 -0
- package/dist/content/skills-elicitation.js +123 -0
- package/dist/content/skills.d.ts +1 -0
- package/dist/content/skills.js +54 -2
- package/dist/content/stage-schema.js +107 -63
- package/dist/content/stages/brainstorm.js +7 -3
- package/dist/content/stages/design.js +4 -0
- package/dist/content/stages/review.js +8 -8
- package/dist/content/stages/schema-types.d.ts +2 -2
- package/dist/content/stages/scope.js +7 -3
- package/dist/content/stages/ship.js +1 -1
- package/dist/content/start-command.js +4 -4
- package/dist/content/status-command.js +3 -3
- package/dist/content/subagent-context-skills.js +156 -1
- package/dist/content/subagents.d.ts +0 -5
- package/dist/content/subagents.js +12 -82
- package/dist/content/templates.js +108 -6
- package/dist/content/utility-skills.js +26 -97
- package/dist/flow-state.d.ts +12 -6
- package/dist/flow-state.js +5 -6
- package/dist/gate-evidence.d.ts +0 -31
- package/dist/gate-evidence.js +3 -181
- package/dist/harness-adapters.js +1 -1
- package/dist/hook-schemas/claude-hooks.v1.json +2 -3
- package/dist/hook-schemas/codex-hooks.v1.json +1 -1
- package/dist/hook-schemas/cursor-hooks.v1.json +1 -1
- package/dist/install.js +50 -7
- package/dist/internal/advance-stage/advance.js +22 -2
- package/dist/internal/advance-stage/parsers.d.ts +1 -0
- package/dist/internal/advance-stage/parsers.js +6 -0
- package/dist/internal/advance-stage/review-loop.js +1 -10
- package/dist/knowledge-store.d.ts +2 -20
- package/dist/knowledge-store.js +43 -57
- package/dist/policy.js +3 -3
- package/dist/retro-gate.js +8 -90
- package/dist/run-archive.js +1 -4
- package/dist/run-persistence.d.ts +1 -1
- package/dist/run-persistence.js +43 -111
- package/dist/runtime/run-hook.entry.d.ts +3 -0
- package/dist/runtime/run-hook.entry.js +5 -0
- package/dist/runtime/run-hook.mjs +9647 -0
- package/dist/track-heuristics.d.ts +7 -1
- package/dist/track-heuristics.js +12 -0
- package/package.json +4 -2
- package/dist/content/hook-inline-snippets.d.ts +0 -96
- package/dist/content/hook-inline-snippets.js +0 -515
- package/dist/content/idea-command.d.ts +0 -8
- package/dist/content/idea-command.js +0 -322
- package/dist/content/idea-frames.d.ts +0 -31
- package/dist/content/idea-frames.js +0 -140
- package/dist/content/idea-ranking.d.ts +0 -25
- package/dist/content/idea-ranking.js +0 -65
- package/dist/trace-matrix.d.ts +0 -27
- package/dist/trace-matrix.js +0 -226
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
+--------+ +------+
|
|
17
17
|
|
|
|
18
18
|
v
|
|
19
|
-
|
|
19
|
+
post_ship_review -> archive
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
The promise is simple: at any point you can ask **where are we, what is blocked, what evidence exists, and what should run next?**
|
|
@@ -90,7 +90,7 @@ That gives you:
|
|
|
90
90
|
npx cclaw-cli sync
|
|
91
91
|
|
|
92
92
|
5. Close out after ship
|
|
93
|
-
/cc continues
|
|
93
|
+
/cc continues post_ship_review -> archive
|
|
94
94
|
```
|
|
95
95
|
|
|
96
96
|
Tracks keep the flow proportional:
|
|
@@ -164,7 +164,7 @@ Enforced by generated helpers and state checks:
|
|
|
164
164
|
- Mandatory delegations need terminal evidence or explicit waiver.
|
|
165
165
|
- Stale stages block until redone and acknowledged.
|
|
166
166
|
- Review criticals route back to TDD.
|
|
167
|
-
- Ship continues through `
|
|
167
|
+
- Ship continues through `post_ship_review -> archive` with `closeout.shipSubstate`.
|
|
168
168
|
|
|
169
169
|
Advisory/model-guided:
|
|
170
170
|
|
|
@@ -1,6 +1,22 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { checkCriticPredictionsContract, sectionBodyByName, validateApproachesTaxonomy, headingLineIndex, meaningfulLineCount, getMarkdownTableRows, parseShortCircuitStatus, validateCalibratedSelfReview, markdownFieldRegex } from "./shared.js";
|
|
2
4
|
export async function lintBrainstormStage(ctx) {
|
|
3
5
|
const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride } = ctx;
|
|
6
|
+
const qaLogBody = sectionBodyByName(sections, "Q&A Log");
|
|
7
|
+
const qaLogRows = qaLogBody ? getMarkdownTableRows(qaLogBody) : [];
|
|
8
|
+
const qaLogOk = qaLogBody !== null && qaLogRows.length > 0;
|
|
9
|
+
findings.push({
|
|
10
|
+
section: "qa_log_missing",
|
|
11
|
+
required: false,
|
|
12
|
+
rule: "[P3] qa_log_missing — Q&A Log empty — confirm you actually had a dialogue with the user, not a draft from memory.",
|
|
13
|
+
found: qaLogOk,
|
|
14
|
+
details: qaLogOk
|
|
15
|
+
? `Q&A Log contains ${qaLogRows.length} data row(s).`
|
|
16
|
+
: qaLogBody === null
|
|
17
|
+
? "Missing `## Q&A Log` section."
|
|
18
|
+
: "Q&A Log is present but has zero data rows."
|
|
19
|
+
});
|
|
4
20
|
// Brainstorm Iron Law: "NO ARTIFACT IS COMPLETE WITHOUT AN EXPLICITLY
|
|
5
21
|
// APPROVED DIRECTION — SILENCE IS NOT APPROVAL." Previously this was
|
|
6
22
|
// prose-only — nothing failed when the Selected Direction section
|
|
@@ -165,6 +181,16 @@ export async function lintBrainstormStage(ctx) {
|
|
|
165
181
|
details: selfReview.details
|
|
166
182
|
});
|
|
167
183
|
}
|
|
184
|
+
const criticPredictions = checkCriticPredictionsContract(sections);
|
|
185
|
+
if (criticPredictions !== null) {
|
|
186
|
+
findings.push({
|
|
187
|
+
section: "critic.predictions_missing",
|
|
188
|
+
required: true,
|
|
189
|
+
rule: "[P2] critic.predictions_missing — pre-commitment predictions block missing or empty",
|
|
190
|
+
found: criticPredictions.found,
|
|
191
|
+
details: criticPredictions.details
|
|
192
|
+
});
|
|
193
|
+
}
|
|
168
194
|
// Universal structural checks (Layer 2.1). Each fires only when the
|
|
169
195
|
// matching section is present so legacy fixtures keep their current
|
|
170
196
|
// shape, while artifacts emitted from the v3 template have to satisfy
|
|
@@ -242,4 +268,36 @@ export async function lintBrainstormStage(ctx) {
|
|
|
242
268
|
: `Outside Voice section is missing field(s): ${missing.join(", ")}.`
|
|
243
269
|
});
|
|
244
270
|
}
|
|
271
|
+
const wavePlansDir = path.join(projectRoot, ".cclaw", "wave-plans");
|
|
272
|
+
let wavePlanEntries = [];
|
|
273
|
+
try {
|
|
274
|
+
wavePlanEntries = (await fs.readdir(wavePlansDir))
|
|
275
|
+
.filter((entry) => entry !== ".gitkeep" && !entry.startsWith("."));
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
wavePlanEntries = [];
|
|
279
|
+
}
|
|
280
|
+
const multiWaveDetected = wavePlanEntries.length >= 2;
|
|
281
|
+
if (multiWaveDetected) {
|
|
282
|
+
const carryForwardBody = sectionBodyByName(sections, "Wave Carry-forward");
|
|
283
|
+
const hasCarryForwardSection = carryForwardBody !== null;
|
|
284
|
+
const hasCarryForwardContent = carryForwardBody !== null && meaningfulLineCount(carryForwardBody) > 0;
|
|
285
|
+
const hasDriftAuditMarkers = carryForwardBody !== null &&
|
|
286
|
+
/\bcarrying\s+forward\b/iu.test(carryForwardBody) &&
|
|
287
|
+
/\bdrift\s+detected\b/iu.test(carryForwardBody);
|
|
288
|
+
const waveDriftAddressed = hasCarryForwardSection && hasCarryForwardContent && hasDriftAuditMarkers;
|
|
289
|
+
findings.push({
|
|
290
|
+
section: "wave.drift_unaddressed",
|
|
291
|
+
required: true,
|
|
292
|
+
rule: "[P1] wave.drift_unaddressed — when `.cclaw/wave-plans/` has >=2 entries, brainstorm must include `## Wave Carry-forward` with carry-forward and drift audit markers.",
|
|
293
|
+
found: waveDriftAddressed,
|
|
294
|
+
details: waveDriftAddressed
|
|
295
|
+
? `Multi-wave context detected (${wavePlanEntries.length} wave-plan entries); Wave Carry-forward audit is present.`
|
|
296
|
+
: !hasCarryForwardSection
|
|
297
|
+
? "Multi-wave context detected but `## Wave Carry-forward` section is missing."
|
|
298
|
+
: !hasCarryForwardContent
|
|
299
|
+
? "`## Wave Carry-forward` exists but has no meaningful content."
|
|
300
|
+
: "Wave Carry-forward section must include both `Carrying forward` and `Drift detected` markers."
|
|
301
|
+
});
|
|
302
|
+
}
|
|
245
303
|
}
|
|
@@ -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 { extractMarkdownSectionBody, getMarkdownTableRows, meaningfulLineCount, sectionBodyByName, markdownFieldRegex } from "./shared.js";
|
|
6
|
+
import { checkCriticPredictionsContract, evaluateLayeredDocumentReviewStatus, extractMarkdownSectionBody, getMarkdownTableRows, meaningfulLineCount, sectionBodyByName, markdownFieldRegex } from "./shared.js";
|
|
7
7
|
const DESIGN_DIAGRAM_REQUIREMENTS = {
|
|
8
8
|
lightweight: [
|
|
9
9
|
{
|
|
@@ -204,6 +204,30 @@ async function runStaleDiagramAudit(projectRoot, artifactPath, artifactRaw, code
|
|
|
204
204
|
}
|
|
205
205
|
export async function lintDesignStage(ctx) {
|
|
206
206
|
const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride } = ctx;
|
|
207
|
+
const qaLogBody = sectionBodyByName(sections, "Q&A Log");
|
|
208
|
+
const qaLogRows = qaLogBody ? getMarkdownTableRows(qaLogBody) : [];
|
|
209
|
+
const qaLogOk = qaLogBody !== null && qaLogRows.length > 0;
|
|
210
|
+
findings.push({
|
|
211
|
+
section: "qa_log_missing",
|
|
212
|
+
required: false,
|
|
213
|
+
rule: "[P3] qa_log_missing — Q&A Log empty — confirm you actually had a dialogue with the user, not a draft from memory.",
|
|
214
|
+
found: qaLogOk,
|
|
215
|
+
details: qaLogOk
|
|
216
|
+
? `Q&A Log contains ${qaLogRows.length} data row(s).`
|
|
217
|
+
: qaLogBody === null
|
|
218
|
+
? "Missing `## Q&A Log` section."
|
|
219
|
+
: "Q&A Log is present but has zero data rows."
|
|
220
|
+
});
|
|
221
|
+
const criticPredictions = checkCriticPredictionsContract(sections);
|
|
222
|
+
if (criticPredictions !== null) {
|
|
223
|
+
findings.push({
|
|
224
|
+
section: "critic.predictions_missing",
|
|
225
|
+
required: true,
|
|
226
|
+
rule: "[P2] critic.predictions_missing — pre-commitment predictions block missing or empty",
|
|
227
|
+
found: criticPredictions.found,
|
|
228
|
+
details: criticPredictions.details
|
|
229
|
+
});
|
|
230
|
+
}
|
|
207
231
|
const tierResolution = await resolveDesignDiagramTier(projectRoot, track, raw);
|
|
208
232
|
const diagramTier = isTrivialOverride
|
|
209
233
|
? "lightweight"
|
|
@@ -320,4 +344,25 @@ export async function lintDesignStage(ctx) {
|
|
|
320
344
|
: "No calibrated findings detected. Use `[P1|P2|P3] (confidence: <n>/10) <repo-path>[:<line>] — <description>`."
|
|
321
345
|
});
|
|
322
346
|
}
|
|
347
|
+
const layeredDocumentReview = evaluateLayeredDocumentReviewStatus(sections, CONFIDENCE_FINDING_REGEX_SOURCE);
|
|
348
|
+
if (layeredDocumentReview !== null) {
|
|
349
|
+
findings.push({
|
|
350
|
+
section: "Document Reviewer Structured Findings",
|
|
351
|
+
required: true,
|
|
352
|
+
rule: "When Layered review references coherence-reviewer/scope-guardian-reviewer/feasibility-reviewer, include explicit reviewer status plus calibrated finding lines.",
|
|
353
|
+
found: layeredDocumentReview.missingStructured.length === 0,
|
|
354
|
+
details: layeredDocumentReview.missingStructured.length === 0
|
|
355
|
+
? `Structured findings present for reviewers: ${layeredDocumentReview.triggeredReviewers.join(", ")}.`
|
|
356
|
+
: `Missing status or calibrated findings for: ${layeredDocumentReview.missingStructured.join(", ")}.`
|
|
357
|
+
});
|
|
358
|
+
findings.push({
|
|
359
|
+
section: "document-review.fail_without_waiver",
|
|
360
|
+
required: true,
|
|
361
|
+
rule: "[P1] document-review.fail_without_waiver — reviewer FAIL/PARTIAL requires fix evidence or explicit waiver.",
|
|
362
|
+
found: layeredDocumentReview.failOrPartialWithoutWaiver.length === 0,
|
|
363
|
+
details: layeredDocumentReview.failOrPartialWithoutWaiver.length === 0
|
|
364
|
+
? "No unwaived FAIL/PARTIAL reviewer statuses detected."
|
|
365
|
+
: `Unwaived FAIL/PARTIAL statuses: ${layeredDocumentReview.failOrPartialWithoutWaiver.join(", ")}.`
|
|
366
|
+
});
|
|
367
|
+
}
|
|
323
368
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { headingPresent, sectionBodyByName, collectPatternHits, PLACEHOLDER_PATTERNS, extractDecisionIds, SCOPE_REDUCTION_PATTERNS } from "./shared.js";
|
|
1
|
+
import { evaluateLayeredDocumentReviewStatus, headingPresent, sectionBodyByName, collectPatternHits, PLACEHOLDER_PATTERNS, extractDecisionIds, SCOPE_REDUCTION_PATTERNS } from "./shared.js";
|
|
2
2
|
import { resolveArtifactPath as resolveStageArtifactPath } from "../artifact-paths.js";
|
|
3
3
|
import { exists } from "../fs-utils.js";
|
|
4
4
|
import { FORBIDDEN_PLACEHOLDER_TOKENS, CONFIDENCE_FINDING_REGEX_SOURCE } from "../content/skills.js";
|
|
@@ -159,4 +159,25 @@ export async function lintPlanStage(ctx) {
|
|
|
159
159
|
: "Regression Iron Rule section is present but missing `Iron rule acknowledged: yes`."
|
|
160
160
|
});
|
|
161
161
|
}
|
|
162
|
+
const layeredDocumentReview = evaluateLayeredDocumentReviewStatus(sections, CONFIDENCE_FINDING_REGEX_SOURCE);
|
|
163
|
+
if (layeredDocumentReview !== null) {
|
|
164
|
+
findings.push({
|
|
165
|
+
section: "Document Reviewer Structured Findings",
|
|
166
|
+
required: true,
|
|
167
|
+
rule: "When Layered review references coherence-reviewer/scope-guardian-reviewer/feasibility-reviewer, include explicit reviewer status plus calibrated finding lines.",
|
|
168
|
+
found: layeredDocumentReview.missingStructured.length === 0,
|
|
169
|
+
details: layeredDocumentReview.missingStructured.length === 0
|
|
170
|
+
? `Structured findings present for reviewers: ${layeredDocumentReview.triggeredReviewers.join(", ")}.`
|
|
171
|
+
: `Missing status or calibrated findings for: ${layeredDocumentReview.missingStructured.join(", ")}.`
|
|
172
|
+
});
|
|
173
|
+
findings.push({
|
|
174
|
+
section: "document-review.fail_without_waiver",
|
|
175
|
+
required: true,
|
|
176
|
+
rule: "[P1] document-review.fail_without_waiver — reviewer FAIL/PARTIAL requires fix evidence or explicit waiver.",
|
|
177
|
+
found: layeredDocumentReview.failOrPartialWithoutWaiver.length === 0,
|
|
178
|
+
details: layeredDocumentReview.failOrPartialWithoutWaiver.length === 0
|
|
179
|
+
? "No unwaived FAIL/PARTIAL reviewer statuses detected."
|
|
180
|
+
: `Unwaived FAIL/PARTIAL statuses: ${layeredDocumentReview.failOrPartialWithoutWaiver.join(", ")}.`
|
|
181
|
+
});
|
|
182
|
+
}
|
|
162
183
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sectionBodyByName } from "./shared.js";
|
|
1
|
+
import { markdownFieldRegex, sectionBodyByName } from "./shared.js";
|
|
2
2
|
export async function lintReviewStage(ctx) {
|
|
3
3
|
const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride } = ctx;
|
|
4
4
|
// Universal Layer 2.7 structural checks (superpowers requesting + receiving).
|
|
@@ -62,4 +62,38 @@ export async function lintReviewStage(ctx) {
|
|
|
62
62
|
: "Receiving Posture is missing the anti-sycophancy acknowledgement line."
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
|
+
const lensCoverageBody = sectionBodyByName(sections, "Lens Coverage");
|
|
66
|
+
if (lensCoverageBody === null) {
|
|
67
|
+
findings.push({
|
|
68
|
+
section: "reviewer.lens_coverage_missing",
|
|
69
|
+
required: true,
|
|
70
|
+
rule: "[P1] reviewer.lens_coverage_missing — review artifact must include `## Lens Coverage` with Performance/Compatibility/Observability/Security lines.",
|
|
71
|
+
found: false,
|
|
72
|
+
details: "No ## heading matching required section \"Lens Coverage\"."
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const performance = markdownFieldRegex("Performance", "NO_IMPACT|FOUND_\\d+").test(lensCoverageBody);
|
|
77
|
+
const compatibility = markdownFieldRegex("Compatibility", "NO_IMPACT|FOUND_\\d+").test(lensCoverageBody);
|
|
78
|
+
const observability = markdownFieldRegex("Observability", "NO_IMPACT|FOUND_\\d+").test(lensCoverageBody);
|
|
79
|
+
const security = markdownFieldRegex("Security", "routed\\s+to\\s+security-reviewer").test(lensCoverageBody);
|
|
80
|
+
const missing = [];
|
|
81
|
+
if (!performance)
|
|
82
|
+
missing.push("Performance");
|
|
83
|
+
if (!compatibility)
|
|
84
|
+
missing.push("Compatibility");
|
|
85
|
+
if (!observability)
|
|
86
|
+
missing.push("Observability");
|
|
87
|
+
if (!security)
|
|
88
|
+
missing.push("Security");
|
|
89
|
+
findings.push({
|
|
90
|
+
section: "reviewer.lens_coverage_missing",
|
|
91
|
+
required: true,
|
|
92
|
+
rule: "[P1] reviewer.lens_coverage_missing — `Lens Coverage` must include Performance/Compatibility/Observability (`NO_IMPACT` or `FOUND_<n>`) and Security routing line.",
|
|
93
|
+
found: missing.length === 0,
|
|
94
|
+
details: missing.length === 0
|
|
95
|
+
? "Lens Coverage includes all required reviewer lens lines."
|
|
96
|
+
: `Lens Coverage missing or malformed line(s): ${missing.join(", ")}.`
|
|
97
|
+
});
|
|
98
|
+
}
|
|
65
99
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sectionBodyByHeadingPrefix, sectionBodyByName, extractCanonicalScopeMode, sectionBodyByAnyName, collectPatternHits, SCOPE_REDUCTION_PATTERNS, validateLockedDecisionAnchors, getMarkdownTableRows } from "./shared.js";
|
|
1
|
+
import { checkCriticPredictionsContract, sectionBodyByHeadingPrefix, sectionBodyByName, extractCanonicalScopeMode, sectionBodyByAnyName, collectPatternHits, SCOPE_REDUCTION_PATTERNS, validateLockedDecisionAnchors, getMarkdownTableRows } from "./shared.js";
|
|
2
2
|
import { readDelegationLedger } from "../delegation.js";
|
|
3
3
|
export async function lintScopeStage(ctx) {
|
|
4
4
|
const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride } = ctx;
|
|
@@ -12,25 +12,49 @@ export async function lintScopeStage(ctx) {
|
|
|
12
12
|
sectionBodyByName(sections, "Scope Summary") ?? "",
|
|
13
13
|
lockedDecisionsBody
|
|
14
14
|
].join("\n");
|
|
15
|
+
const qaLogBody = sectionBodyByName(sections, "Q&A Log");
|
|
16
|
+
const qaLogRows = qaLogBody ? getMarkdownTableRows(qaLogBody) : [];
|
|
17
|
+
const qaLogOk = qaLogBody !== null && qaLogRows.length > 0;
|
|
18
|
+
findings.push({
|
|
19
|
+
section: "qa_log_missing",
|
|
20
|
+
required: false,
|
|
21
|
+
rule: "[P3] qa_log_missing — Q&A Log empty — confirm you actually had a dialogue with the user, not a draft from memory.",
|
|
22
|
+
found: qaLogOk,
|
|
23
|
+
details: qaLogOk
|
|
24
|
+
? `Q&A Log contains ${qaLogRows.length} data row(s).`
|
|
25
|
+
: qaLogBody === null
|
|
26
|
+
? "Missing `## Q&A Log` section."
|
|
27
|
+
: "Q&A Log is present but has zero data rows."
|
|
28
|
+
});
|
|
15
29
|
const strategistRequired = selectedScopeMode === "SCOPE EXPANSION" || selectedScopeMode === "SELECTIVE EXPANSION";
|
|
16
30
|
if (strategistRequired) {
|
|
17
31
|
const delegationLedger = await readDelegationLedger(projectRoot);
|
|
18
|
-
const
|
|
19
|
-
entry.agent === "product-
|
|
32
|
+
const discoveryRows = delegationLedger.entries.filter((entry) => entry.stage === "scope" &&
|
|
33
|
+
entry.agent === "product-discovery" &&
|
|
20
34
|
entry.runId === delegationLedger.runId &&
|
|
21
35
|
entry.status === "completed");
|
|
22
|
-
const hasCompleted =
|
|
23
|
-
const hasEvidence =
|
|
36
|
+
const hasCompleted = discoveryRows.length > 0;
|
|
37
|
+
const hasEvidence = discoveryRows.some((entry) => Array.isArray(entry.evidenceRefs) && entry.evidenceRefs.length > 0);
|
|
24
38
|
findings.push({
|
|
25
39
|
section: "Expansion Strategist Delegation",
|
|
26
40
|
required: true,
|
|
27
|
-
rule: "When Scope Summary selects SCOPE EXPANSION or SELECTIVE EXPANSION, a completed `product-
|
|
41
|
+
rule: "When Scope Summary selects SCOPE EXPANSION or SELECTIVE EXPANSION, a completed `product-discovery` delegation for the active run with non-empty evidenceRefs is required.",
|
|
28
42
|
found: hasCompleted && hasEvidence,
|
|
29
43
|
details: !hasCompleted
|
|
30
|
-
? `Scope mode ${selectedScopeMode} requires a completed product-
|
|
44
|
+
? `Scope mode ${selectedScopeMode} requires a completed product-discovery delegation row for active run ${delegationLedger.runId}.`
|
|
31
45
|
: hasEvidence
|
|
32
|
-
? `product-
|
|
33
|
-
: "product-
|
|
46
|
+
? `product-discovery delegation satisfied for mode ${selectedScopeMode}.`
|
|
47
|
+
: "product-discovery delegation exists but evidenceRefs is empty; add at least one artifact/code evidence reference."
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const criticPredictions = checkCriticPredictionsContract(sections);
|
|
51
|
+
if (criticPredictions !== null) {
|
|
52
|
+
findings.push({
|
|
53
|
+
section: "critic.predictions_missing",
|
|
54
|
+
required: true,
|
|
55
|
+
rule: "[P2] critic.predictions_missing — pre-commitment predictions block missing or empty",
|
|
56
|
+
found: criticPredictions.found,
|
|
57
|
+
details: criticPredictions.details
|
|
34
58
|
});
|
|
35
59
|
}
|
|
36
60
|
const reductionHits = collectPatternHits(scopeSections, SCOPE_REDUCTION_PATTERNS);
|
|
@@ -26,10 +26,22 @@ export type H2SectionMap = Map<string, string>;
|
|
|
26
26
|
* into multiple passes.
|
|
27
27
|
*/
|
|
28
28
|
export declare function extractH2Sections(markdown: string): H2SectionMap;
|
|
29
|
+
export declare function duplicateH2Headings(markdown: string): string[];
|
|
29
30
|
export declare function headingPresent(sections: H2SectionMap, section: string): boolean;
|
|
30
31
|
export declare function sectionBodyByName(sections: H2SectionMap, section: string): string | null;
|
|
31
32
|
export declare function sectionBodyByAnyName(sections: H2SectionMap, sectionNames: string[]): string | null;
|
|
32
33
|
export declare function sectionBodyByHeadingPrefix(sections: H2SectionMap, prefix: string): string | null;
|
|
34
|
+
export interface CriticPredictionsContractCheck {
|
|
35
|
+
found: boolean;
|
|
36
|
+
details: string;
|
|
37
|
+
}
|
|
38
|
+
export declare function checkCriticPredictionsContract(sections: H2SectionMap): CriticPredictionsContractCheck | null;
|
|
39
|
+
export interface LayeredDocumentReviewStatus {
|
|
40
|
+
triggeredReviewers: string[];
|
|
41
|
+
missingStructured: string[];
|
|
42
|
+
failOrPartialWithoutWaiver: string[];
|
|
43
|
+
}
|
|
44
|
+
export declare function evaluateLayeredDocumentReviewStatus(sections: H2SectionMap, confidenceFindingRegexSource: string): LayeredDocumentReviewStatus | null;
|
|
33
45
|
/**
|
|
34
46
|
* Build a regex that matches `<field>: <value>` even when the field name
|
|
35
47
|
* and/or value are wrapped in markdown emphasis (`*`, `**`, `_`, `__`).
|
|
@@ -153,8 +165,6 @@ export declare function hasVerificationLadderTableRow(sectionBody: string): bool
|
|
|
153
165
|
export type LearningEntryType = "rule" | "pattern" | "lesson" | "compound";
|
|
154
166
|
export type LearningConfidence = "high" | "medium" | "low";
|
|
155
167
|
export type LearningSeverity = "critical" | "important" | "suggestion";
|
|
156
|
-
export type LearningUniversality = "project" | "personal" | "universal";
|
|
157
|
-
export type LearningMaturity = "raw" | "lifted-to-rule" | "lifted-to-enforcement";
|
|
158
168
|
export type LearningSource = "stage" | "retro" | "compound" | "idea" | "manual";
|
|
159
169
|
export interface LearningSeedEntry {
|
|
160
170
|
type: LearningEntryType;
|
|
@@ -162,20 +172,14 @@ export interface LearningSeedEntry {
|
|
|
162
172
|
action: string;
|
|
163
173
|
confidence: LearningConfidence;
|
|
164
174
|
severity?: LearningSeverity;
|
|
165
|
-
domain?: string | null;
|
|
166
175
|
stage?: FlowStage | null;
|
|
167
176
|
origin_stage?: FlowStage | null;
|
|
168
|
-
origin_run?: string | null;
|
|
169
177
|
frequency?: number;
|
|
170
|
-
universality?: LearningUniversality;
|
|
171
|
-
maturity?: LearningMaturity;
|
|
172
178
|
created?: string;
|
|
173
179
|
first_seen_ts?: string;
|
|
174
180
|
last_seen_ts?: string;
|
|
175
181
|
project?: string | null;
|
|
176
182
|
source?: LearningSource | null;
|
|
177
|
-
supersedes?: string[];
|
|
178
|
-
superseded_by?: string;
|
|
179
183
|
}
|
|
180
184
|
export interface LearningsParseResult {
|
|
181
185
|
ok: boolean;
|
|
@@ -187,8 +191,6 @@ export interface LearningsParseResult {
|
|
|
187
191
|
export declare const LEARNING_TYPE_SET: Set<LearningEntryType>;
|
|
188
192
|
export declare const LEARNING_CONFIDENCE_SET: Set<LearningConfidence>;
|
|
189
193
|
export declare const LEARNING_SEVERITY_SET: Set<LearningSeverity>;
|
|
190
|
-
export declare const LEARNING_UNIVERSALITY_SET: Set<LearningUniversality>;
|
|
191
|
-
export declare const LEARNING_MATURITY_SET: Set<LearningMaturity>;
|
|
192
194
|
export declare const LEARNING_SOURCE_SET: Set<LearningSource>;
|
|
193
195
|
export declare const FLOW_STAGE_SET: Set<"brainstorm" | "scope" | "design" | "spec" | "plan" | "tdd" | "review" | "ship">;
|
|
194
196
|
export declare const LEARNING_ALLOWED_KEYS: Set<string>;
|
|
@@ -57,6 +57,38 @@ export function extractH2Sections(markdown) {
|
|
|
57
57
|
flush();
|
|
58
58
|
return sections;
|
|
59
59
|
}
|
|
60
|
+
export function duplicateH2Headings(markdown) {
|
|
61
|
+
const lines = markdown.split(/\r?\n/);
|
|
62
|
+
let fenced = null;
|
|
63
|
+
const counts = new Map();
|
|
64
|
+
const displayHeading = new Map();
|
|
65
|
+
for (const line of lines) {
|
|
66
|
+
const fenceMatch = /^(```|~~~)/u.exec(line);
|
|
67
|
+
if (fenceMatch) {
|
|
68
|
+
if (fenced === null) {
|
|
69
|
+
fenced = fenceMatch[1] ?? null;
|
|
70
|
+
}
|
|
71
|
+
else if (line.startsWith(fenced)) {
|
|
72
|
+
fenced = null;
|
|
73
|
+
}
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (fenced !== null)
|
|
77
|
+
continue;
|
|
78
|
+
const match = /^##\s+(.+)$/u.exec(line);
|
|
79
|
+
if (!match)
|
|
80
|
+
continue;
|
|
81
|
+
const heading = normalizeHeadingTitle(match[1] ?? "");
|
|
82
|
+
const key = heading.toLowerCase();
|
|
83
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
84
|
+
if (!displayHeading.has(key)) {
|
|
85
|
+
displayHeading.set(key, heading);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return [...counts.entries()]
|
|
89
|
+
.filter(([, count]) => count > 1)
|
|
90
|
+
.map(([key]) => displayHeading.get(key) ?? key);
|
|
91
|
+
}
|
|
60
92
|
export function headingPresent(sections, section) {
|
|
61
93
|
const want = normalizeHeadingTitle(section).toLowerCase();
|
|
62
94
|
for (const h of sections.keys()) {
|
|
@@ -93,6 +125,75 @@ export function sectionBodyByHeadingPrefix(sections, prefix) {
|
|
|
93
125
|
}
|
|
94
126
|
return null;
|
|
95
127
|
}
|
|
128
|
+
export function checkCriticPredictionsContract(sections) {
|
|
129
|
+
const criticFindingsBody = sectionBodyByName(sections, "Critic Findings");
|
|
130
|
+
const layeredReviewBody = sectionBodyByHeadingPrefix(sections, "Layered review");
|
|
131
|
+
const layeredReviewMentionsCritic = layeredReviewBody !== null && /\bcritic\b/iu.test(layeredReviewBody);
|
|
132
|
+
const sourceBody = criticFindingsBody ?? (layeredReviewMentionsCritic ? layeredReviewBody : null);
|
|
133
|
+
if (sourceBody === null)
|
|
134
|
+
return null;
|
|
135
|
+
const predictionsMatch = /(?:^|\n)#{3,4}\s*Pre-commitment predictions\b([\s\S]*?)(?=\n#{2,4}\s+|$)/iu.exec(sourceBody);
|
|
136
|
+
const predictionsCount = predictionsMatch ? countListItems(predictionsMatch[1] ?? "") : 0;
|
|
137
|
+
const hasPredictions = predictionsCount >= 1;
|
|
138
|
+
const hasValidated = /(?:^|\n)#{3,4}\s*Validated\s*\/\s*Disproven\b/iu.test(sourceBody);
|
|
139
|
+
const hasOpenQuestions = /(?:^|\n)#{3,4}\s*Open Questions\b/iu.test(sourceBody);
|
|
140
|
+
const missing = [];
|
|
141
|
+
if (!hasPredictions) {
|
|
142
|
+
missing.push("`Pre-commitment predictions` subsection is missing or has no list items");
|
|
143
|
+
}
|
|
144
|
+
if (!hasValidated) {
|
|
145
|
+
missing.push("`Validated / Disproven` subsection is missing");
|
|
146
|
+
}
|
|
147
|
+
if (!hasOpenQuestions) {
|
|
148
|
+
missing.push("`Open Questions` subsection is missing");
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
found: missing.length === 0,
|
|
152
|
+
details: missing.length === 0
|
|
153
|
+
? "Critic pre-commitment predictions contract is present (predictions, validated/disproven mapping, open questions)."
|
|
154
|
+
: missing.join("; ")
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
const DOCUMENT_REVIEWER_NAMES = [
|
|
158
|
+
"coherence-reviewer",
|
|
159
|
+
"scope-guardian-reviewer",
|
|
160
|
+
"feasibility-reviewer"
|
|
161
|
+
];
|
|
162
|
+
export function evaluateLayeredDocumentReviewStatus(sections, confidenceFindingRegexSource) {
|
|
163
|
+
const layeredReviewBody = sectionBodyByHeadingPrefix(sections, "Layered review");
|
|
164
|
+
if (layeredReviewBody === null)
|
|
165
|
+
return null;
|
|
166
|
+
const triggeredReviewers = DOCUMENT_REVIEWER_NAMES.filter((reviewer) => new RegExp(`\\b${reviewer}\\b`, "iu").test(layeredReviewBody));
|
|
167
|
+
if (triggeredReviewers.length === 0)
|
|
168
|
+
return null;
|
|
169
|
+
const findingRegex = new RegExp(confidenceFindingRegexSource, "iu");
|
|
170
|
+
const hasCalibratedFinding = findingRegex.test(layeredReviewBody);
|
|
171
|
+
const missingStructured = [];
|
|
172
|
+
const failOrPartialWithoutWaiver = [];
|
|
173
|
+
const waiverRegex = /(?:explicit\s+waiver|waiver\s*:|waived\s*:|accepted[-\s]?risk)/iu;
|
|
174
|
+
for (const reviewer of triggeredReviewers) {
|
|
175
|
+
const escaped = reviewer.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
176
|
+
const subsectionMatch = new RegExp(`(?:^|\\n)#{3,4}\\s*${escaped}\\b([\\s\\S]*?)(?=\\n#{2,4}\\s+|$)`, "iu")
|
|
177
|
+
.exec(layeredReviewBody);
|
|
178
|
+
const reviewerBlock = subsectionMatch?.[1] ?? layeredReviewBody;
|
|
179
|
+
const statusMatch = /\b(?:Status|Result|Verdict)\s*:\s*(PASS|PASS_WITH_GAPS|FAIL|PARTIAL|BLOCKED)\b/iu
|
|
180
|
+
.exec(reviewerBlock);
|
|
181
|
+
const inlineStatusMatch = new RegExp(`${escaped}[\\s\\S]{0,120}\\b(PASS|PASS_WITH_GAPS|FAIL|PARTIAL|BLOCKED)\\b`, "iu")
|
|
182
|
+
.exec(layeredReviewBody);
|
|
183
|
+
const status = (statusMatch?.[1] ?? inlineStatusMatch?.[1] ?? "").toUpperCase();
|
|
184
|
+
if (!hasCalibratedFinding || status.length === 0) {
|
|
185
|
+
missingStructured.push(reviewer);
|
|
186
|
+
}
|
|
187
|
+
if ((status === "FAIL" || status === "PARTIAL") && !waiverRegex.test(reviewerBlock) && !waiverRegex.test(layeredReviewBody)) {
|
|
188
|
+
failOrPartialWithoutWaiver.push(`${reviewer}:${status}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
triggeredReviewers,
|
|
193
|
+
missingStructured,
|
|
194
|
+
failOrPartialWithoutWaiver
|
|
195
|
+
};
|
|
196
|
+
}
|
|
96
197
|
/**
|
|
97
198
|
* Build a regex that matches `<field>: <value>` even when the field name
|
|
98
199
|
* and/or value are wrapped in markdown emphasis (`*`, `**`, `_`, `__`).
|
|
@@ -991,8 +1092,6 @@ export function hasVerificationLadderTableRow(sectionBody) {
|
|
|
991
1092
|
export const LEARNING_TYPE_SET = new Set(["rule", "pattern", "lesson", "compound"]);
|
|
992
1093
|
export const LEARNING_CONFIDENCE_SET = new Set(["high", "medium", "low"]);
|
|
993
1094
|
export const LEARNING_SEVERITY_SET = new Set(["critical", "important", "suggestion"]);
|
|
994
|
-
export const LEARNING_UNIVERSALITY_SET = new Set(["project", "personal", "universal"]);
|
|
995
|
-
export const LEARNING_MATURITY_SET = new Set(["raw", "lifted-to-rule", "lifted-to-enforcement"]);
|
|
996
1095
|
export const LEARNING_SOURCE_SET = new Set([
|
|
997
1096
|
"stage",
|
|
998
1097
|
"retro",
|
|
@@ -1007,20 +1106,14 @@ export const LEARNING_ALLOWED_KEYS = new Set([
|
|
|
1007
1106
|
"action",
|
|
1008
1107
|
"confidence",
|
|
1009
1108
|
"severity",
|
|
1010
|
-
"domain",
|
|
1011
1109
|
"stage",
|
|
1012
1110
|
"origin_stage",
|
|
1013
|
-
"origin_run",
|
|
1014
1111
|
"frequency",
|
|
1015
|
-
"universality",
|
|
1016
|
-
"maturity",
|
|
1017
1112
|
"created",
|
|
1018
1113
|
"first_seen_ts",
|
|
1019
1114
|
"last_seen_ts",
|
|
1020
1115
|
"project",
|
|
1021
|
-
"source"
|
|
1022
|
-
"supersedes",
|
|
1023
|
-
"superseded_by"
|
|
1116
|
+
"source"
|
|
1024
1117
|
]);
|
|
1025
1118
|
export function isIsoUtcTimestamp(value) {
|
|
1026
1119
|
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/u.test(value);
|
|
@@ -1079,9 +1172,6 @@ export function parseLearningSeedEntry(raw, index) {
|
|
|
1079
1172
|
error: `Learnings bullet #${index} field "severity" must be critical|important|suggestion.`
|
|
1080
1173
|
};
|
|
1081
1174
|
}
|
|
1082
|
-
if (obj.domain !== undefined && !isNullableString(obj.domain)) {
|
|
1083
|
-
return { ok: false, error: `Learnings bullet #${index} field "domain" must be string or null.` };
|
|
1084
|
-
}
|
|
1085
1175
|
if (obj.stage !== undefined && !isNullableStage(obj.stage)) {
|
|
1086
1176
|
return {
|
|
1087
1177
|
ok: false,
|
|
@@ -1094,9 +1184,6 @@ export function parseLearningSeedEntry(raw, index) {
|
|
|
1094
1184
|
error: `Learnings bullet #${index} field "origin_stage" must be one of ${FLOW_STAGES.join(", ")} or null.`
|
|
1095
1185
|
};
|
|
1096
1186
|
}
|
|
1097
|
-
if (obj.origin_run !== undefined && !isNullableString(obj.origin_run)) {
|
|
1098
|
-
return { ok: false, error: `Learnings bullet #${index} field "origin_run" must be string or null.` };
|
|
1099
|
-
}
|
|
1100
1187
|
if (obj.project !== undefined && !isNullableString(obj.project)) {
|
|
1101
1188
|
return { ok: false, error: `Learnings bullet #${index} field "project" must be string or null.` };
|
|
1102
1189
|
}
|
|
@@ -1112,21 +1199,6 @@ export function parseLearningSeedEntry(raw, index) {
|
|
|
1112
1199
|
(typeof obj.frequency !== "number" || !Number.isInteger(obj.frequency) || obj.frequency < 1)) {
|
|
1113
1200
|
return { ok: false, error: `Learnings bullet #${index} field "frequency" must be an integer >= 1.` };
|
|
1114
1201
|
}
|
|
1115
|
-
if (obj.universality !== undefined &&
|
|
1116
|
-
(typeof obj.universality !== "string" ||
|
|
1117
|
-
!LEARNING_UNIVERSALITY_SET.has(obj.universality))) {
|
|
1118
|
-
return {
|
|
1119
|
-
ok: false,
|
|
1120
|
-
error: `Learnings bullet #${index} field "universality" must be project|personal|universal.`
|
|
1121
|
-
};
|
|
1122
|
-
}
|
|
1123
|
-
if (obj.maturity !== undefined &&
|
|
1124
|
-
(typeof obj.maturity !== "string" || !LEARNING_MATURITY_SET.has(obj.maturity))) {
|
|
1125
|
-
return {
|
|
1126
|
-
ok: false,
|
|
1127
|
-
error: `Learnings bullet #${index} field "maturity" must be raw|lifted-to-rule|lifted-to-enforcement.`
|
|
1128
|
-
};
|
|
1129
|
-
}
|
|
1130
1202
|
for (const timestampField of ["created", "first_seen_ts", "last_seen_ts"]) {
|
|
1131
1203
|
const value = obj[timestampField];
|
|
1132
1204
|
if (value === undefined)
|
|
@@ -1138,17 +1210,6 @@ export function parseLearningSeedEntry(raw, index) {
|
|
|
1138
1210
|
};
|
|
1139
1211
|
}
|
|
1140
1212
|
}
|
|
1141
|
-
if (obj.supersedes !== undefined) {
|
|
1142
|
-
if (!Array.isArray(obj.supersedes) ||
|
|
1143
|
-
obj.supersedes.length === 0 ||
|
|
1144
|
-
obj.supersedes.some((value) => typeof value !== "string" || value.trim().length === 0)) {
|
|
1145
|
-
return { ok: false, error: `Learnings bullet #${index} field "supersedes" must be a non-empty array of strings.` };
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
if (obj.superseded_by !== undefined &&
|
|
1149
|
-
(typeof obj.superseded_by !== "string" || obj.superseded_by.trim().length === 0)) {
|
|
1150
|
-
return { ok: false, error: `Learnings bullet #${index} field "superseded_by" must be a non-empty string.` };
|
|
1151
|
-
}
|
|
1152
1213
|
return {
|
|
1153
1214
|
ok: true,
|
|
1154
1215
|
entry: {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readDelegationLedger } from "../delegation.js";
|
|
1
2
|
import { sectionBodyByName } from "./shared.js";
|
|
2
3
|
export async function lintShipStage(ctx) {
|
|
3
4
|
const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride } = ctx;
|
|
@@ -43,4 +44,39 @@ export async function lintShipStage(ctx) {
|
|
|
43
44
|
: "Verify Tests Gate is missing a `Result: PASS|FAIL` line."
|
|
44
45
|
});
|
|
45
46
|
}
|
|
47
|
+
const delegationLedger = await readDelegationLedger(projectRoot);
|
|
48
|
+
const activeRunRows = delegationLedger.entries.filter((entry) => entry.stage === "ship" &&
|
|
49
|
+
entry.runId === delegationLedger.runId &&
|
|
50
|
+
entry.agent === "architect" &&
|
|
51
|
+
entry.status === "completed");
|
|
52
|
+
const hasCrossStageReferenceInArtifact = /\barchitect-cross-stage-verification\b/iu.test(raw) ||
|
|
53
|
+
/\barchitect\b[\s\S]{0,180}\bcross[-\s]?stage\b/iu.test(raw) ||
|
|
54
|
+
/\bCROSS_STAGE_VERIFIED\b/u.test(raw) ||
|
|
55
|
+
/\bDRIFT_DETECTED\b/u.test(raw);
|
|
56
|
+
findings.push({
|
|
57
|
+
section: "ship.cross_stage_cohesion_missing",
|
|
58
|
+
required: true,
|
|
59
|
+
rule: "Ship artifact must include architect cross-stage verification reference (`architect-cross-stage-verification` / CROSS_STAGE_VERIFIED / DRIFT_DETECTED) before finalization.",
|
|
60
|
+
found: hasCrossStageReferenceInArtifact,
|
|
61
|
+
details: hasCrossStageReferenceInArtifact
|
|
62
|
+
? "Architect cross-stage verification reference is present in ship artifact."
|
|
63
|
+
: activeRunRows.length > 0
|
|
64
|
+
? "Completed architect delegation exists in ledger, but ship artifact is missing explicit cross-stage verification reference."
|
|
65
|
+
: "Ship artifact is missing architect cross-stage verification reference."
|
|
66
|
+
});
|
|
67
|
+
const driftDetectedInArtifact = /\bDRIFT_DETECTED\b/u.test(raw);
|
|
68
|
+
const driftDetectedInDelegation = activeRunRows.some((row) => {
|
|
69
|
+
const refs = Array.isArray(row.evidenceRefs) ? row.evidenceRefs.join(" ") : "";
|
|
70
|
+
return /\bDRIFT_DETECTED\b/u.test(refs);
|
|
71
|
+
});
|
|
72
|
+
const driftDetected = driftDetectedInArtifact || driftDetectedInDelegation;
|
|
73
|
+
findings.push({
|
|
74
|
+
section: "ship.cross_stage_drift_detected",
|
|
75
|
+
required: true,
|
|
76
|
+
rule: "If architect cross-stage verification reports DRIFT_DETECTED, ship must be blocked until drift is resolved or explicitly waived.",
|
|
77
|
+
found: !driftDetected,
|
|
78
|
+
details: driftDetected
|
|
79
|
+
? "Architect cross-stage verification reported DRIFT_DETECTED; ship must not proceed."
|
|
80
|
+
: "No DRIFT_DETECTED signal found in ship artifact or architect delegation evidence."
|
|
81
|
+
});
|
|
46
82
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { sectionBodyByName, SPEC_MAX_MODULES } from "./shared.js";
|
|
1
|
+
import { evaluateLayeredDocumentReviewStatus, sectionBodyByName, SPEC_MAX_MODULES } from "./shared.js";
|
|
2
|
+
import { CONFIDENCE_FINDING_REGEX_SOURCE } from "../content/skills.js";
|
|
2
3
|
export async function lintSpecStage(ctx) {
|
|
3
4
|
const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride } = ctx;
|
|
4
5
|
// Universal Layer 2.4 structural checks (evanflow-prd + superpowers).
|
|
@@ -105,4 +106,25 @@ export async function lintSpecStage(ctx) {
|
|
|
105
106
|
: `Spec Self-Review is missing check(s): ${missing.join(", ")}.`
|
|
106
107
|
});
|
|
107
108
|
}
|
|
109
|
+
const layeredDocumentReview = evaluateLayeredDocumentReviewStatus(sections, CONFIDENCE_FINDING_REGEX_SOURCE);
|
|
110
|
+
if (layeredDocumentReview !== null) {
|
|
111
|
+
findings.push({
|
|
112
|
+
section: "Document Reviewer Structured Findings",
|
|
113
|
+
required: true,
|
|
114
|
+
rule: "When Layered review references coherence-reviewer/scope-guardian-reviewer/feasibility-reviewer, include explicit reviewer status plus calibrated finding lines.",
|
|
115
|
+
found: layeredDocumentReview.missingStructured.length === 0,
|
|
116
|
+
details: layeredDocumentReview.missingStructured.length === 0
|
|
117
|
+
? `Structured findings present for reviewers: ${layeredDocumentReview.triggeredReviewers.join(", ")}.`
|
|
118
|
+
: `Missing status or calibrated findings for: ${layeredDocumentReview.missingStructured.join(", ")}.`
|
|
119
|
+
});
|
|
120
|
+
findings.push({
|
|
121
|
+
section: "document-review.fail_without_waiver",
|
|
122
|
+
required: true,
|
|
123
|
+
rule: "[P1] document-review.fail_without_waiver — reviewer FAIL/PARTIAL requires fix evidence or explicit waiver.",
|
|
124
|
+
found: layeredDocumentReview.failOrPartialWithoutWaiver.length === 0,
|
|
125
|
+
details: layeredDocumentReview.failOrPartialWithoutWaiver.length === 0
|
|
126
|
+
? "No unwaived FAIL/PARTIAL reviewer statuses detected."
|
|
127
|
+
: `Unwaived FAIL/PARTIAL statuses: ${layeredDocumentReview.failOrPartialWithoutWaiver.join(", ")}.`
|
|
128
|
+
});
|
|
129
|
+
}
|
|
108
130
|
}
|