cclaw-cli 0.51.30 → 0.55.2

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 (142) hide show
  1. package/README.md +22 -16
  2. package/dist/artifact-linter/brainstorm.d.ts +2 -0
  3. package/dist/artifact-linter/brainstorm.js +245 -0
  4. package/dist/artifact-linter/design.d.ts +2 -0
  5. package/dist/artifact-linter/design.js +323 -0
  6. package/dist/artifact-linter/plan.d.ts +2 -0
  7. package/dist/artifact-linter/plan.js +162 -0
  8. package/dist/artifact-linter/review-army.d.ts +24 -0
  9. package/dist/artifact-linter/review-army.js +365 -0
  10. package/dist/artifact-linter/review.d.ts +2 -0
  11. package/dist/artifact-linter/review.js +65 -0
  12. package/dist/artifact-linter/scope.d.ts +2 -0
  13. package/dist/artifact-linter/scope.js +115 -0
  14. package/dist/artifact-linter/shared.d.ts +246 -0
  15. package/dist/artifact-linter/shared.js +1488 -0
  16. package/dist/artifact-linter/ship.d.ts +2 -0
  17. package/dist/artifact-linter/ship.js +46 -0
  18. package/dist/artifact-linter/spec.d.ts +2 -0
  19. package/dist/artifact-linter/spec.js +108 -0
  20. package/dist/artifact-linter/tdd.d.ts +2 -0
  21. package/dist/artifact-linter/tdd.js +124 -0
  22. package/dist/artifact-linter.d.ts +4 -76
  23. package/dist/artifact-linter.js +56 -2949
  24. package/dist/cli.d.ts +1 -6
  25. package/dist/cli.js +4 -159
  26. package/dist/codex-feature-flag.d.ts +1 -1
  27. package/dist/codex-feature-flag.js +1 -1
  28. package/dist/config.d.ts +3 -2
  29. package/dist/config.js +67 -3
  30. package/dist/constants.d.ts +1 -7
  31. package/dist/constants.js +9 -15
  32. package/dist/content/cancel-command.js +2 -2
  33. package/dist/content/closeout-guidance.js +10 -7
  34. package/dist/content/core-agents.d.ts +18 -0
  35. package/dist/content/core-agents.js +46 -2
  36. package/dist/content/decision-protocol.d.ts +1 -1
  37. package/dist/content/decision-protocol.js +1 -1
  38. package/dist/content/examples.js +6 -6
  39. package/dist/content/harness-doc.js +20 -2
  40. package/dist/content/hook-inline-snippets.d.ts +17 -4
  41. package/dist/content/hook-inline-snippets.js +218 -5
  42. package/dist/content/hook-manifest.d.ts +2 -2
  43. package/dist/content/hook-manifest.js +2 -2
  44. package/dist/content/hooks.d.ts +1 -0
  45. package/dist/content/hooks.js +32 -137
  46. package/dist/content/idea-command.d.ts +8 -0
  47. package/dist/content/{ideate-command.js → idea-command.js} +57 -50
  48. package/dist/content/idea-frames.d.ts +31 -0
  49. package/dist/content/{ideate-frames.js → idea-frames.js} +9 -9
  50. package/dist/content/idea-ranking.d.ts +25 -0
  51. package/dist/content/{ideate-ranking.js → idea-ranking.js} +5 -5
  52. package/dist/content/iron-laws.d.ts +0 -1
  53. package/dist/content/iron-laws.js +31 -16
  54. package/dist/content/learnings.js +1 -1
  55. package/dist/content/meta-skill.js +7 -7
  56. package/dist/content/node-hooks.d.ts +10 -0
  57. package/dist/content/node-hooks.js +43 -9
  58. package/dist/content/opencode-plugin.js +3 -3
  59. package/dist/content/skills.js +19 -7
  60. package/dist/content/stage-schema.js +44 -2
  61. package/dist/content/stages/_lint-metadata/index.js +26 -2
  62. package/dist/content/stages/brainstorm.js +13 -7
  63. package/dist/content/stages/design.js +16 -11
  64. package/dist/content/stages/plan.js +7 -4
  65. package/dist/content/stages/review.js +4 -4
  66. package/dist/content/stages/schema-types.d.ts +1 -1
  67. package/dist/content/stages/scope.js +15 -12
  68. package/dist/content/stages/ship.js +2 -2
  69. package/dist/content/stages/spec.js +9 -3
  70. package/dist/content/stages/tdd.js +14 -4
  71. package/dist/content/start-command.js +11 -10
  72. package/dist/content/status-command.js +3 -3
  73. package/dist/content/subagents.js +60 -6
  74. package/dist/content/templates.d.ts +1 -1
  75. package/dist/content/templates.js +102 -150
  76. package/dist/content/tree-command.js +2 -2
  77. package/dist/content/utility-skills.d.ts +2 -2
  78. package/dist/content/utility-skills.js +2 -2
  79. package/dist/content/view-command.js +4 -2
  80. package/dist/delegation.d.ts +2 -0
  81. package/dist/delegation.js +2 -1
  82. package/dist/early-loop.d.ts +66 -0
  83. package/dist/early-loop.js +275 -0
  84. package/dist/gate-evidence.d.ts +8 -0
  85. package/dist/gate-evidence.js +141 -5
  86. package/dist/harness-adapters.d.ts +2 -2
  87. package/dist/harness-adapters.js +47 -18
  88. package/dist/install.js +153 -29
  89. package/dist/internal/advance-stage/advance.d.ts +50 -0
  90. package/dist/internal/advance-stage/advance.js +480 -0
  91. package/dist/internal/advance-stage/cancel-run.d.ts +8 -0
  92. package/dist/internal/advance-stage/cancel-run.js +19 -0
  93. package/dist/internal/advance-stage/flow-state-coercion.d.ts +3 -0
  94. package/dist/internal/advance-stage/flow-state-coercion.js +81 -0
  95. package/dist/internal/advance-stage/helpers.d.ts +14 -0
  96. package/dist/internal/advance-stage/helpers.js +145 -0
  97. package/dist/internal/advance-stage/hook.d.ts +8 -0
  98. package/dist/internal/advance-stage/hook.js +40 -0
  99. package/dist/internal/advance-stage/parsers.d.ts +54 -0
  100. package/dist/internal/advance-stage/parsers.js +307 -0
  101. package/dist/internal/advance-stage/review-loop.d.ts +7 -0
  102. package/dist/internal/advance-stage/review-loop.js +170 -0
  103. package/dist/internal/advance-stage/rewind.d.ts +14 -0
  104. package/dist/internal/advance-stage/rewind.js +108 -0
  105. package/dist/internal/advance-stage/start-flow.d.ts +11 -0
  106. package/dist/internal/advance-stage/start-flow.js +136 -0
  107. package/dist/internal/advance-stage/verify.d.ts +29 -0
  108. package/dist/internal/advance-stage/verify.js +225 -0
  109. package/dist/internal/advance-stage.js +21 -1470
  110. package/dist/internal/compound-readiness.d.ts +1 -1
  111. package/dist/internal/compound-readiness.js +2 -2
  112. package/dist/internal/early-loop-status.d.ts +7 -0
  113. package/dist/internal/early-loop-status.js +90 -0
  114. package/dist/internal/runtime-integrity.d.ts +7 -0
  115. package/dist/internal/runtime-integrity.js +288 -0
  116. package/dist/internal/tdd-red-evidence.js +1 -1
  117. package/dist/knowledge-store.d.ts +3 -8
  118. package/dist/knowledge-store.js +16 -29
  119. package/dist/managed-resources.js +24 -2
  120. package/dist/policy.js +4 -6
  121. package/dist/run-archive.d.ts +1 -1
  122. package/dist/run-archive.js +12 -12
  123. package/dist/run-persistence.js +111 -11
  124. package/dist/tdd-cycle.d.ts +3 -3
  125. package/dist/tdd-cycle.js +1 -1
  126. package/dist/types.d.ts +18 -10
  127. package/package.json +1 -1
  128. package/dist/content/ideate-command.d.ts +0 -8
  129. package/dist/content/ideate-frames.d.ts +0 -31
  130. package/dist/content/ideate-ranking.d.ts +0 -25
  131. package/dist/content/next-command.d.ts +0 -20
  132. package/dist/content/next-command.js +0 -298
  133. package/dist/content/seed-shelf.d.ts +0 -36
  134. package/dist/content/seed-shelf.js +0 -301
  135. package/dist/content/stage-common-guidance.d.ts +0 -1
  136. package/dist/content/stage-common-guidance.js +0 -106
  137. package/dist/doctor-registry.d.ts +0 -10
  138. package/dist/doctor-registry.js +0 -186
  139. package/dist/doctor.d.ts +0 -17
  140. package/dist/doctor.js +0 -2201
  141. package/dist/internal/hook-manifest.d.ts +0 -16
  142. package/dist/internal/hook-manifest.js +0 -77
@@ -0,0 +1,365 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { RUNTIME_ROOT } from "../constants.js";
4
+ import { exists } from "../fs-utils.js";
5
+ import { extractH2Sections, sectionBodyByName } from "./shared.js";
6
+ async function resolveNamedArtifactPath(projectRoot, fileName) {
7
+ const relPath = path.join(RUNTIME_ROOT, "artifacts", fileName);
8
+ const absPath = path.join(projectRoot, relPath);
9
+ return { absPath, relPath };
10
+ }
11
+ function isNonEmptyString(v) {
12
+ return typeof v === "string" && v.length > 0;
13
+ }
14
+ function isFiniteNumber(v) {
15
+ return typeof v === "number" && Number.isFinite(v);
16
+ }
17
+ function isNonNegativeInteger(v) {
18
+ return Number.isInteger(v) && v >= 0;
19
+ }
20
+ function isStringArray(v) {
21
+ return Array.isArray(v) && v.every((item) => typeof item === "string");
22
+ }
23
+ export async function validateReviewArmy(projectRoot) {
24
+ const errors = [];
25
+ const { absPath, relPath } = await resolveNamedArtifactPath(projectRoot, "07-review-army.json");
26
+ if (!(await exists(absPath))) {
27
+ return { valid: false, errors: [`Missing file: ${relPath}`] };
28
+ }
29
+ let parsed;
30
+ try {
31
+ parsed = JSON.parse(await fs.readFile(absPath, "utf8"));
32
+ }
33
+ catch (e) {
34
+ const msg = e instanceof Error ? e.message : String(e);
35
+ return { valid: false, errors: [`Invalid JSON: ${msg}`] };
36
+ }
37
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
38
+ return { valid: false, errors: ["Root value must be a JSON object."] };
39
+ }
40
+ const root = parsed;
41
+ if (!("version" in root) || !isFiniteNumber(root.version) || root.version < 1) {
42
+ errors.push('Field "version" must be a finite number >= 1.');
43
+ }
44
+ if (!isNonEmptyString(root.generatedAt)) {
45
+ errors.push('Field "generatedAt" must be a non-empty string.');
46
+ }
47
+ if (!("scope" in root) || root.scope === null || typeof root.scope !== "object" || Array.isArray(root.scope)) {
48
+ errors.push('Field "scope" must be an object.');
49
+ }
50
+ else {
51
+ const scope = root.scope;
52
+ if (!isNonEmptyString(scope.base)) {
53
+ errors.push("scope.base must be a non-empty string.");
54
+ }
55
+ if (!isNonEmptyString(scope.head)) {
56
+ errors.push("scope.head must be a non-empty string.");
57
+ }
58
+ if (!isStringArray(scope.files)) {
59
+ errors.push("scope.files must be an array of strings.");
60
+ }
61
+ }
62
+ const severitySet = new Set(["Critical", "Important", "Suggestion"]);
63
+ const statusSet = new Set(["open", "accepted", "resolved"]);
64
+ const sourceSet = new Set([
65
+ "spec",
66
+ "correctness",
67
+ "security",
68
+ "performance",
69
+ "architecture",
70
+ "external-safety"
71
+ ]);
72
+ const findingIds = new Set();
73
+ const openCriticalIds = new Set();
74
+ if (!Array.isArray(root.findings)) {
75
+ errors.push('Field "findings" must be an array.');
76
+ }
77
+ else {
78
+ root.findings.forEach((f, i) => {
79
+ if (f === null || typeof f !== "object" || Array.isArray(f)) {
80
+ errors.push(`findings[${i}] must be an object.`);
81
+ return;
82
+ }
83
+ const o = f;
84
+ if (!isNonEmptyString(o.id)) {
85
+ errors.push(`findings[${i}].id must be a non-empty string.`);
86
+ }
87
+ else if (findingIds.has(o.id)) {
88
+ errors.push(`findings[${i}].id must be unique.`);
89
+ }
90
+ else {
91
+ findingIds.add(o.id);
92
+ }
93
+ if (!isNonEmptyString(o.severity) || !severitySet.has(o.severity)) {
94
+ errors.push(`findings[${i}].severity must be one of: Critical, Important, Suggestion.`);
95
+ }
96
+ if (!isNonEmptyString(o.status) || !statusSet.has(o.status)) {
97
+ errors.push(`findings[${i}].status must be one of: open, accepted, resolved.`);
98
+ }
99
+ if (!isNonEmptyString(o.fingerprint)) {
100
+ errors.push(`findings[${i}].fingerprint must be a non-empty string.`);
101
+ }
102
+ if (!isFiniteNumber(o.confidence) || o.confidence < 1 || o.confidence > 10) {
103
+ errors.push(`findings[${i}].confidence must be a number in [1,10].`);
104
+ }
105
+ if (!isStringArray(o.reportedBy) || o.reportedBy.length === 0) {
106
+ errors.push(`findings[${i}].reportedBy must be a non-empty string array.`);
107
+ }
108
+ if (o.sources !== undefined) {
109
+ if (!isStringArray(o.sources) || o.sources.length === 0) {
110
+ errors.push(`findings[${i}].sources must be a non-empty string array when present.`);
111
+ }
112
+ else {
113
+ const invalidSources = o.sources.filter((source) => !sourceSet.has(source));
114
+ if (invalidSources.length > 0) {
115
+ errors.push(`findings[${i}].sources contains unknown values: ${invalidSources.join(", ")}.`);
116
+ }
117
+ }
118
+ }
119
+ if (o.location === undefined || o.location === null) {
120
+ errors.push(`findings[${i}].location is required and must be an object with file + line.`);
121
+ }
122
+ else if (typeof o.location !== "object" || Array.isArray(o.location)) {
123
+ errors.push(`findings[${i}].location must be an object with file + line.`);
124
+ }
125
+ else {
126
+ const loc = o.location;
127
+ if (!isNonEmptyString(loc.file)) {
128
+ errors.push(`findings[${i}].location.file must be a non-empty string.`);
129
+ }
130
+ if (!isFiniteNumber(loc.line) || loc.line < 1) {
131
+ errors.push(`findings[${i}].location.line must be a positive number.`);
132
+ }
133
+ }
134
+ if (o.recommendation !== undefined && !isNonEmptyString(o.recommendation)) {
135
+ errors.push(`findings[${i}].recommendation must be a non-empty string when present.`);
136
+ }
137
+ if (o.severity === "Critical" && o.status === "open" && !isNonEmptyString(o.recommendation)) {
138
+ errors.push(`findings[${i}] open Critical finding must include recommendation.`);
139
+ }
140
+ if (o.id && o.severity === "Critical" && o.status === "open" && typeof o.id === "string") {
141
+ openCriticalIds.add(o.id);
142
+ }
143
+ });
144
+ }
145
+ if (!("reconciliation" in root) || root.reconciliation === null || typeof root.reconciliation !== "object") {
146
+ errors.push('Field "reconciliation" must be an object.');
147
+ }
148
+ else {
149
+ const rec = root.reconciliation;
150
+ if (!isNonNegativeInteger(rec.duplicatesCollapsed)) {
151
+ errors.push("reconciliation.duplicatesCollapsed must be a non-negative integer.");
152
+ }
153
+ if (!Array.isArray(rec.conflicts)) {
154
+ errors.push("reconciliation.conflicts must be an array.");
155
+ }
156
+ else {
157
+ rec.conflicts.forEach((c, ci) => {
158
+ if (c === null || typeof c !== "object" || Array.isArray(c)) {
159
+ errors.push(`reconciliation.conflicts[${ci}] must be an object.`);
160
+ return;
161
+ }
162
+ const co = c;
163
+ if (!isNonEmptyString(co.findingId)) {
164
+ errors.push(`reconciliation.conflicts[${ci}].findingId must be a non-empty string.`);
165
+ }
166
+ else if (!findingIds.has(co.findingId)) {
167
+ errors.push(`reconciliation.conflicts[${ci}].findingId references unknown finding "${co.findingId}".`);
168
+ }
169
+ if (!isNonEmptyString(co.description)) {
170
+ errors.push(`reconciliation.conflicts[${ci}].description must be a non-empty string.`);
171
+ }
172
+ });
173
+ }
174
+ if (!isStringArray(rec.multiSpecialistConfirmed)) {
175
+ errors.push("reconciliation.multiSpecialistConfirmed must be an array of finding ids.");
176
+ }
177
+ else {
178
+ for (const msId of rec.multiSpecialistConfirmed) {
179
+ if (!findingIds.has(msId)) {
180
+ errors.push(`reconciliation.multiSpecialistConfirmed references unknown finding id "${msId}".`);
181
+ continue;
182
+ }
183
+ if (Array.isArray(root.findings)) {
184
+ const finding = root.findings.find((f) => {
185
+ return f && typeof f === "object" && !Array.isArray(f) && f.id === msId;
186
+ });
187
+ if (finding && typeof finding === "object" && !Array.isArray(finding)) {
188
+ const reportedBy = finding.reportedBy;
189
+ const count = Array.isArray(reportedBy)
190
+ ? new Set(reportedBy.filter((v) => typeof v === "string")).size
191
+ : 0;
192
+ if (count < 2) {
193
+ errors.push(`reconciliation.multiSpecialistConfirmed entry "${msId}" must be confirmed by at least 2 distinct reviewers (found ${count}).`);
194
+ }
195
+ }
196
+ }
197
+ }
198
+ }
199
+ if (!isStringArray(rec.shipBlockers)) {
200
+ errors.push("reconciliation.shipBlockers must be an array of finding ids.");
201
+ }
202
+ else {
203
+ const blockers = new Set(rec.shipBlockers);
204
+ for (const id of rec.shipBlockers) {
205
+ if (!findingIds.has(id)) {
206
+ errors.push(`reconciliation.shipBlockers references unknown finding id "${id}".`);
207
+ }
208
+ }
209
+ for (const criticalId of openCriticalIds) {
210
+ if (!blockers.has(criticalId)) {
211
+ errors.push(`reconciliation.shipBlockers must include open Critical finding "${criticalId}".`);
212
+ }
213
+ }
214
+ }
215
+ if (isStringArray(rec.multiSpecialistConfirmed)) {
216
+ for (const id of rec.multiSpecialistConfirmed) {
217
+ if (!findingIds.has(id)) {
218
+ errors.push(`reconciliation.multiSpecialistConfirmed references unknown finding id "${id}".`);
219
+ }
220
+ }
221
+ }
222
+ if (rec.layerCoverage !== undefined) {
223
+ if (rec.layerCoverage === null || typeof rec.layerCoverage !== "object" || Array.isArray(rec.layerCoverage)) {
224
+ errors.push("reconciliation.layerCoverage must be an object when present.");
225
+ }
226
+ else {
227
+ const coverage = rec.layerCoverage;
228
+ for (const source of sourceSet) {
229
+ if (coverage[source] !== undefined && typeof coverage[source] !== "boolean") {
230
+ errors.push(`reconciliation.layerCoverage.${source} must be boolean when present.`);
231
+ }
232
+ }
233
+ }
234
+ }
235
+ }
236
+ return { valid: errors.length === 0, errors };
237
+ }
238
+ /**
239
+ * Ensure the narrative verdict in 07-review.md is consistent with the
240
+ * structured review-army reconciliation. A review cannot declare
241
+ * APPROVED while open Critical findings or shipBlockers remain.
242
+ */
243
+ export async function checkReviewVerdictConsistency(projectRoot) {
244
+ const errors = [];
245
+ const reviewMdPath = path.join(projectRoot, RUNTIME_ROOT, "artifacts", "07-review.md");
246
+ const armyJsonPath = path.join(projectRoot, RUNTIME_ROOT, "artifacts", "07-review-army.json");
247
+ let finalVerdict = "UNKNOWN";
248
+ if (await exists(reviewMdPath)) {
249
+ const raw = await fs.readFile(reviewMdPath, "utf8");
250
+ const sections = extractH2Sections(raw);
251
+ const verdictBody = sectionBodyByName(sections, "Final Verdict");
252
+ if (verdictBody) {
253
+ const chosen = [];
254
+ for (const token of ["APPROVED_WITH_CONCERNS", "APPROVED", "BLOCKED"]) {
255
+ const regex = new RegExp(`\\b${token}\\b`, "u");
256
+ if (regex.test(verdictBody)) {
257
+ // APPROVED would match inside APPROVED_WITH_CONCERNS; prefer the longer match first.
258
+ if (token === "APPROVED" && /\bAPPROVED_WITH_CONCERNS\b/u.test(verdictBody))
259
+ continue;
260
+ chosen.push(token);
261
+ }
262
+ }
263
+ if (chosen.length === 1) {
264
+ finalVerdict = chosen[0];
265
+ }
266
+ else if (chosen.length > 1) {
267
+ errors.push(`Final Verdict section lists multiple verdict tokens (${chosen.join(", ")}). Select exactly one.`);
268
+ }
269
+ else {
270
+ errors.push('Final Verdict section does not select APPROVED, APPROVED_WITH_CONCERNS, or BLOCKED.');
271
+ }
272
+ }
273
+ else {
274
+ errors.push('07-review.md is missing the "## Final Verdict" section.');
275
+ }
276
+ }
277
+ let openCriticalCount = 0;
278
+ let shipBlockerCount = 0;
279
+ if (await exists(armyJsonPath)) {
280
+ try {
281
+ const raw = await fs.readFile(armyJsonPath, "utf8");
282
+ const parsed = JSON.parse(raw);
283
+ const findings = Array.isArray(parsed.findings) ? parsed.findings : [];
284
+ for (const f of findings) {
285
+ if (!f || typeof f !== "object" || Array.isArray(f))
286
+ continue;
287
+ const o = f;
288
+ if (o.severity === "Critical" && o.status === "open") {
289
+ openCriticalCount++;
290
+ }
291
+ }
292
+ const rec = parsed.reconciliation && typeof parsed.reconciliation === "object" && !Array.isArray(parsed.reconciliation)
293
+ ? parsed.reconciliation
294
+ : null;
295
+ if (rec && Array.isArray(rec.shipBlockers)) {
296
+ shipBlockerCount = rec.shipBlockers.filter((v) => typeof v === "string").length;
297
+ }
298
+ }
299
+ catch {
300
+ // JSON validity is the concern of validateReviewArmy; skip silently here.
301
+ }
302
+ }
303
+ if (finalVerdict === "APPROVED" && (openCriticalCount > 0 || shipBlockerCount > 0)) {
304
+ errors.push(`Final Verdict is APPROVED but review-army has ${openCriticalCount} open Critical finding(s) and ${shipBlockerCount} shipBlocker(s). Use BLOCKED or APPROVED_WITH_CONCERNS.`);
305
+ }
306
+ // APPROVED_WITH_CONCERNS is intended for Important/Suggestion findings
307
+ // the author has accepted. An *open* Critical finding or an active
308
+ // shipBlocker must route through BLOCKED (review_verdict_blocked gate)
309
+ // rather than pass as a concession — previously this slipped through.
310
+ if (finalVerdict === "APPROVED_WITH_CONCERNS" &&
311
+ (openCriticalCount > 0 || shipBlockerCount > 0)) {
312
+ errors.push(`Final Verdict is APPROVED_WITH_CONCERNS but review-army has ${openCriticalCount} open Critical finding(s) and ${shipBlockerCount} shipBlocker(s). Resolve them or use BLOCKED.`);
313
+ }
314
+ return {
315
+ ok: errors.length === 0,
316
+ errors,
317
+ finalVerdict,
318
+ openCriticalCount,
319
+ shipBlockerCount
320
+ };
321
+ }
322
+ export async function checkReviewSecurityNoChangeAttestation(projectRoot) {
323
+ const reviewMdPath = path.join(projectRoot, RUNTIME_ROOT, "artifacts", "07-review.md");
324
+ if (!(await exists(reviewMdPath))) {
325
+ return {
326
+ ok: true,
327
+ errors: [],
328
+ hasSecurityFinding: false,
329
+ hasNoChangeAttestation: false
330
+ };
331
+ }
332
+ const errors = [];
333
+ const raw = await fs.readFile(reviewMdPath, "utf8");
334
+ const sections = extractH2Sections(raw);
335
+ const securityBody = sectionBodyByName(sections, "Layer 2 Security")
336
+ ?? sectionBodyByName(sections, "Layer 2b: Security")
337
+ ?? sectionBodyByName(sections, "Layer 2 Findings");
338
+ if (!securityBody) {
339
+ errors.push('07-review.md is missing a Layer 2 security section.');
340
+ return {
341
+ ok: false,
342
+ errors,
343
+ hasSecurityFinding: false,
344
+ hasNoChangeAttestation: false
345
+ };
346
+ }
347
+ const securityTableRowPattern = /^\|\s*[^|\n]+\|\s*[^|\n]+\|\s*security\s*\|\s*[^|\n]+\|\s*[^|\n]+\|/imu;
348
+ const securityBulletPattern = /^[*-]\s+.*\b(?:security|auth|injection|secret|credential|permission)\b/imu;
349
+ const hasSecurityFinding = securityTableRowPattern.test(securityBody) || securityBulletPattern.test(securityBody);
350
+ const attestationMatch = /\b(NO_CHANGE_ATTESTATION|NO_SECURITY_IMPACT)\b\s*:\s*(.*)/iu.exec(securityBody);
351
+ const attestationToken = attestationMatch?.[1] ?? "NO_CHANGE_ATTESTATION";
352
+ const hasNoChangeAttestation = Boolean(attestationMatch && attestationMatch[2]?.trim().length > 0);
353
+ if (attestationMatch && attestationMatch[2]?.trim().length === 0) {
354
+ errors.push(`${attestationToken} must include a non-empty rationale.`);
355
+ }
356
+ if (!hasSecurityFinding && !hasNoChangeAttestation) {
357
+ errors.push("Layer 2 security evidence missing: include at least one security finding or `NO_CHANGE_ATTESTATION: <reason>` / `NO_SECURITY_IMPACT: <reason>`.");
358
+ }
359
+ return {
360
+ ok: errors.length === 0,
361
+ errors,
362
+ hasSecurityFinding,
363
+ hasNoChangeAttestation
364
+ };
365
+ }
@@ -0,0 +1,2 @@
1
+ import { type StageLintContext } from "./shared.js";
2
+ export declare function lintReviewStage(ctx: StageLintContext): Promise<void>;
@@ -0,0 +1,65 @@
1
+ import { sectionBodyByName } from "./shared.js";
2
+ export async function lintReviewStage(ctx) {
3
+ const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride } = ctx;
4
+ // Universal Layer 2.7 structural checks (superpowers requesting + receiving).
5
+ const frameBody = sectionBodyByName(sections, "Pre-Critic Self-Review");
6
+ if (frameBody !== null) {
7
+ const required = [
8
+ "Build/lint/type-check/tests passed locally",
9
+ "Diff matches spec/plan (no scope creep)",
10
+ "Evidence (commands + result):",
11
+ "Goal:",
12
+ "Approach:",
13
+ "Risk areas:",
14
+ "Verification done:",
15
+ "Open questions"
16
+ ];
17
+ const missing = required.filter((token) => {
18
+ const escaped = token
19
+ .replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")
20
+ .replace(/\\:/gu, "\\s*:");
21
+ return !new RegExp(escaped, "iu").test(frameBody);
22
+ });
23
+ findings.push({
24
+ section: "Pre-Critic Self-Review Coverage",
25
+ required: true,
26
+ rule: "Pre-Critic Self-Review must include key self-check lines plus Goal, Approach, Risk areas, Verification done, and Open questions.",
27
+ found: missing.length === 0,
28
+ details: missing.length === 0
29
+ ? "Pre-Critic Self-Review covers all required fields."
30
+ : `Pre-Critic Self-Review is missing field(s): ${missing.join(", ")}.`
31
+ });
32
+ }
33
+ const criticBody = sectionBodyByName(sections, "Critic Subagent Dispatch");
34
+ if (criticBody !== null) {
35
+ const required = [
36
+ "Critic agent definition path",
37
+ "Dispatch surface",
38
+ "Frame sent",
39
+ "Critic returned"
40
+ ];
41
+ const missing = required.filter((token) => !criticBody.includes(token));
42
+ findings.push({
43
+ section: "Critic Subagent Dispatch Shape",
44
+ required: true,
45
+ rule: "Critic Subagent Dispatch must declare agent definition path, dispatch surface, frame sent, and critic-returned summary.",
46
+ found: missing.length === 0,
47
+ details: missing.length === 0
48
+ ? "Critic dispatch metadata complete."
49
+ : `Critic Subagent Dispatch is missing field(s): ${missing.join(", ")}.`
50
+ });
51
+ }
52
+ const receivingBody = sectionBodyByName(sections, "Receiving Posture");
53
+ if (receivingBody !== null) {
54
+ const ack = /no performative agreement/iu.test(receivingBody);
55
+ findings.push({
56
+ section: "Receiving Posture Anti-Sycophancy",
57
+ required: true,
58
+ rule: "Receiving Posture must affirm `No performative agreement (forbidden openers acknowledged)`.",
59
+ found: ack,
60
+ details: ack
61
+ ? "Receiving posture acknowledged anti-sycophancy."
62
+ : "Receiving Posture is missing the anti-sycophancy acknowledgement line."
63
+ });
64
+ }
65
+ }
@@ -0,0 +1,2 @@
1
+ import { type StageLintContext } from "./shared.js";
2
+ export declare function lintScopeStage(ctx: StageLintContext): Promise<void>;
@@ -0,0 +1,115 @@
1
+ import { sectionBodyByHeadingPrefix, sectionBodyByName, extractCanonicalScopeMode, sectionBodyByAnyName, collectPatternHits, SCOPE_REDUCTION_PATTERNS, validateLockedDecisionAnchors, getMarkdownTableRows } from "./shared.js";
2
+ import { readDelegationLedger } from "../delegation.js";
3
+ export async function lintScopeStage(ctx) {
4
+ const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride } = ctx;
5
+ const lockedDecisionsBody = sectionBodyByHeadingPrefix(sections, "Locked Decisions") ?? "";
6
+ const scopeSummaryBody = sectionBodyByName(sections, "Scope Summary") ?? "";
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
+ const strategistRequired = selectedScopeMode === "SCOPE EXPANSION" || selectedScopeMode === "SELECTIVE EXPANSION";
16
+ if (strategistRequired) {
17
+ const delegationLedger = await readDelegationLedger(projectRoot);
18
+ const strategistRows = delegationLedger.entries.filter((entry) => entry.stage === "scope" &&
19
+ entry.agent === "product-strategist" &&
20
+ entry.runId === delegationLedger.runId &&
21
+ entry.status === "completed");
22
+ const hasCompleted = strategistRows.length > 0;
23
+ const hasEvidence = strategistRows.some((entry) => Array.isArray(entry.evidenceRefs) && entry.evidenceRefs.length > 0);
24
+ findings.push({
25
+ section: "Expansion Strategist Delegation",
26
+ 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.",
28
+ found: hasCompleted && hasEvidence,
29
+ details: !hasCompleted
30
+ ? `Scope mode ${selectedScopeMode} requires a completed product-strategist delegation row for active run ${delegationLedger.runId}.`
31
+ : 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."
34
+ });
35
+ }
36
+ const reductionHits = collectPatternHits(scopeSections, SCOPE_REDUCTION_PATTERNS);
37
+ findings.push({
38
+ section: "No Scope Reduction Language",
39
+ required: strictScopeGuards,
40
+ rule: "Scope boundary sections must not use reduction placeholders (`v1`, `for now`, `later`, `temporary`, `placeholder`).",
41
+ found: reductionHits.length === 0,
42
+ details: reductionHits.length === 0
43
+ ? "No scope-reduction phrases detected in scope boundary sections."
44
+ : `Detected scope-reduction phrase(s): ${reductionHits.join(", ")}.`
45
+ });
46
+ if (sectionBodyByHeadingPrefix(sections, "Locked Decisions") !== null) {
47
+ const anchorValidation = validateLockedDecisionAnchors(lockedDecisionsBody);
48
+ findings.push({
49
+ section: "Locked Decisions Hash Integrity",
50
+ required: true,
51
+ rule: "Locked Decisions section must list unique LD#<sha8> content-derived anchors.",
52
+ found: anchorValidation.ok,
53
+ details: anchorValidation.details
54
+ });
55
+ // Legacy D-XX rows remain advisory for older artifacts, but new templates
56
+ // use LD#hash anchors. This check keeps D-XX duplicates visible without
57
+ // making old artifacts the primary contract.
58
+ const listDecisionLines = lockedDecisionsBody
59
+ .split(/\r?\n/u)
60
+ .map((line) => line.trim())
61
+ .filter((line) => /^[-*]\s+\S/u.test(line));
62
+ const tableDecisionRows = getMarkdownTableRows(lockedDecisionsBody);
63
+ const tableDecisionLines = tableDecisionRows.map((row) => row.join(" | "));
64
+ const decisionLines = [...listDecisionLines, ...tableDecisionLines];
65
+ const orphanDecisionLines = decisionLines.filter((line) => !/\bD-\d+\b/u.test(line));
66
+ const rowDecisionIds = [
67
+ ...listDecisionLines.map((line) => /\bD-\d+\b/u.exec(line)?.[0]),
68
+ ...tableDecisionRows.map((row) => /\bD-\d+\b/u.exec(row[0] ?? "")?.[0])
69
+ ].filter((id) => typeof id === "string");
70
+ const duplicateIds = (() => {
71
+ const counts = new Map();
72
+ for (const id of rowDecisionIds)
73
+ counts.set(id, (counts.get(id) ?? 0) + 1);
74
+ return [...counts.entries()].filter(([, n]) => n > 1).map(([id]) => id);
75
+ })();
76
+ const issues = [];
77
+ if (rowDecisionIds.length === 0 && decisionLines.length === 0) {
78
+ issues.push("section is empty");
79
+ }
80
+ if (orphanDecisionLines.length > 0) {
81
+ const examples = orphanDecisionLines
82
+ .slice(0, 3)
83
+ .map((line) => `\`${line.slice(0, 120)}\``)
84
+ .join(", ");
85
+ issues.push(`${orphanDecisionLines.length} decision row(s) missing a D-XX ID${examples.length > 0 ? `: ${examples}` : ""}`);
86
+ }
87
+ if (duplicateIds.length > 0) {
88
+ issues.push(`duplicate IDs: ${duplicateIds.join(", ")}`);
89
+ }
90
+ findings.push({
91
+ section: "Locked Decisions ID Integrity",
92
+ required: false,
93
+ rule: "Locked Decisions section must list each decision with a unique stable D-XX ID.",
94
+ found: issues.length === 0,
95
+ details: issues.length === 0
96
+ ? `${rowDecisionIds.length} decision ID(s) recorded with no duplicates.`
97
+ : issues.join("; ")
98
+ });
99
+ }
100
+ // Universal Layer 2.2 structural checks (gstack plan-ceo-review). All
101
+ // present-only — they validate shape when the section exists.
102
+ const altsBody = sectionBodyByName(sections, "Implementation Alternatives");
103
+ if (altsBody !== null) {
104
+ const recommendation = /^RECOMMENDATION:\s*(.+)$/imu.test(altsBody);
105
+ findings.push({
106
+ section: "Implementation Alternatives Recommendation",
107
+ required: true,
108
+ rule: "Implementation Alternatives must conclude with a `RECOMMENDATION:` line citing the chosen option and rationale.",
109
+ found: recommendation,
110
+ details: recommendation
111
+ ? "Recommendation marker present."
112
+ : "Missing or empty `RECOMMENDATION:` line under Implementation Alternatives."
113
+ });
114
+ }
115
+ }