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.
Files changed (92) hide show
  1. package/README.md +3 -3
  2. package/dist/artifact-linter/brainstorm.js +59 -1
  3. package/dist/artifact-linter/design.js +46 -1
  4. package/dist/artifact-linter/plan.js +22 -1
  5. package/dist/artifact-linter/review.js +35 -1
  6. package/dist/artifact-linter/scope.js +33 -9
  7. package/dist/artifact-linter/shared.d.ts +12 -10
  8. package/dist/artifact-linter/shared.js +102 -41
  9. package/dist/artifact-linter/ship.js +36 -0
  10. package/dist/artifact-linter/spec.js +23 -1
  11. package/dist/artifact-linter/tdd.js +74 -0
  12. package/dist/artifact-linter.d.ts +1 -1
  13. package/dist/artifact-linter.js +11 -1
  14. package/dist/constants.d.ts +1 -1
  15. package/dist/constants.js +1 -0
  16. package/dist/content/closeout-guidance.d.ts +1 -1
  17. package/dist/content/closeout-guidance.js +10 -11
  18. package/dist/content/core-agents.d.ts +35 -36
  19. package/dist/content/core-agents.js +189 -99
  20. package/dist/content/diff-command.js +1 -1
  21. package/dist/content/examples.d.ts +0 -3
  22. package/dist/content/examples.js +197 -752
  23. package/dist/content/hook-events.js +1 -2
  24. package/dist/content/hook-manifest.d.ts +3 -4
  25. package/dist/content/hook-manifest.js +22 -25
  26. package/dist/content/hooks.js +54 -14
  27. package/dist/content/idea.d.ts +60 -0
  28. package/dist/content/idea.js +404 -0
  29. package/dist/content/learnings.d.ts +2 -4
  30. package/dist/content/learnings.js +10 -26
  31. package/dist/content/meta-skill.js +4 -3
  32. package/dist/content/node-hooks.js +368 -164
  33. package/dist/content/observe.js +3 -3
  34. package/dist/content/opencode-plugin.js +12 -32
  35. package/dist/content/reference-patterns.js +2 -2
  36. package/dist/content/runtime-shared-snippets.d.ts +8 -0
  37. package/dist/content/runtime-shared-snippets.js +80 -0
  38. package/dist/content/session-hooks.js +1 -1
  39. package/dist/content/skills-elicitation.d.ts +1 -0
  40. package/dist/content/skills-elicitation.js +123 -0
  41. package/dist/content/skills.d.ts +1 -0
  42. package/dist/content/skills.js +54 -2
  43. package/dist/content/stage-schema.js +107 -63
  44. package/dist/content/stages/brainstorm.js +7 -3
  45. package/dist/content/stages/design.js +4 -0
  46. package/dist/content/stages/review.js +8 -8
  47. package/dist/content/stages/schema-types.d.ts +2 -2
  48. package/dist/content/stages/scope.js +7 -3
  49. package/dist/content/stages/ship.js +1 -1
  50. package/dist/content/start-command.js +4 -4
  51. package/dist/content/status-command.js +3 -3
  52. package/dist/content/subagent-context-skills.js +156 -1
  53. package/dist/content/subagents.d.ts +0 -5
  54. package/dist/content/subagents.js +12 -82
  55. package/dist/content/templates.js +108 -6
  56. package/dist/content/utility-skills.js +26 -97
  57. package/dist/flow-state.d.ts +12 -6
  58. package/dist/flow-state.js +5 -6
  59. package/dist/gate-evidence.d.ts +0 -31
  60. package/dist/gate-evidence.js +3 -181
  61. package/dist/harness-adapters.js +1 -1
  62. package/dist/hook-schemas/claude-hooks.v1.json +2 -3
  63. package/dist/hook-schemas/codex-hooks.v1.json +1 -1
  64. package/dist/hook-schemas/cursor-hooks.v1.json +1 -1
  65. package/dist/install.js +50 -7
  66. package/dist/internal/advance-stage/advance.js +22 -2
  67. package/dist/internal/advance-stage/parsers.d.ts +1 -0
  68. package/dist/internal/advance-stage/parsers.js +6 -0
  69. package/dist/internal/advance-stage/review-loop.js +1 -10
  70. package/dist/knowledge-store.d.ts +2 -20
  71. package/dist/knowledge-store.js +43 -57
  72. package/dist/policy.js +3 -3
  73. package/dist/retro-gate.js +8 -90
  74. package/dist/run-archive.js +1 -4
  75. package/dist/run-persistence.d.ts +1 -1
  76. package/dist/run-persistence.js +43 -111
  77. package/dist/runtime/run-hook.entry.d.ts +3 -0
  78. package/dist/runtime/run-hook.entry.js +5 -0
  79. package/dist/runtime/run-hook.mjs +9647 -0
  80. package/dist/track-heuristics.d.ts +7 -1
  81. package/dist/track-heuristics.js +12 -0
  82. package/package.json +4 -2
  83. package/dist/content/hook-inline-snippets.d.ts +0 -96
  84. package/dist/content/hook-inline-snippets.js +0 -515
  85. package/dist/content/idea-command.d.ts +0 -8
  86. package/dist/content/idea-command.js +0 -322
  87. package/dist/content/idea-frames.d.ts +0 -31
  88. package/dist/content/idea-frames.js +0 -140
  89. package/dist/content/idea-ranking.d.ts +0 -25
  90. package/dist/content/idea-ranking.js +0 -65
  91. package/dist/trace-matrix.d.ts +0 -27
  92. package/dist/trace-matrix.js +0 -226
package/README.md CHANGED
@@ -16,7 +16,7 @@
16
16
  +--------+ +------+
17
17
  |
18
18
  v
19
- retro -> compound -> archive
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 retro -> compound -> archive
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 `retro -> compound -> archive` with `closeout.shipSubstate`.
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 { sectionBodyByName, validateApproachesTaxonomy, headingLineIndex, meaningfulLineCount, parseShortCircuitStatus, validateCalibratedSelfReview, markdownFieldRegex } from "./shared.js";
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 strategistRows = delegationLedger.entries.filter((entry) => entry.stage === "scope" &&
19
- entry.agent === "product-strategist" &&
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 = strategistRows.length > 0;
23
- const hasEvidence = strategistRows.some((entry) => Array.isArray(entry.evidenceRefs) && entry.evidenceRefs.length > 0);
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-strategist` delegation for the active run with non-empty evidenceRefs is required.",
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-strategist delegation row for active run ${delegationLedger.runId}.`
44
+ ? `Scope mode ${selectedScopeMode} requires a completed product-discovery delegation row for active run ${delegationLedger.runId}.`
31
45
  : hasEvidence
32
- ? `product-strategist delegation satisfied for mode ${selectedScopeMode}.`
33
- : "product-strategist delegation exists but evidenceRefs is empty; add at least one artifact/code evidence reference."
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
  }