@nforma.ai/nforma 0.2.1

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 (215) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +1024 -0
  3. package/agents/qgsd-codebase-mapper.md +764 -0
  4. package/agents/qgsd-debugger.md +1201 -0
  5. package/agents/qgsd-executor.md +472 -0
  6. package/agents/qgsd-integration-checker.md +443 -0
  7. package/agents/qgsd-phase-researcher.md +502 -0
  8. package/agents/qgsd-plan-checker.md +643 -0
  9. package/agents/qgsd-planner.md +1182 -0
  10. package/agents/qgsd-project-researcher.md +621 -0
  11. package/agents/qgsd-quorum-orchestrator.md +628 -0
  12. package/agents/qgsd-quorum-slot-worker.md +41 -0
  13. package/agents/qgsd-quorum-synthesizer.md +133 -0
  14. package/agents/qgsd-quorum-test-worker.md +37 -0
  15. package/agents/qgsd-quorum-worker.md +161 -0
  16. package/agents/qgsd-research-synthesizer.md +239 -0
  17. package/agents/qgsd-roadmapper.md +660 -0
  18. package/agents/qgsd-verifier.md +628 -0
  19. package/bin/accept-debug-invariant.cjs +165 -0
  20. package/bin/account-manager.cjs +719 -0
  21. package/bin/aggregate-requirements.cjs +466 -0
  22. package/bin/analyze-assumptions.cjs +757 -0
  23. package/bin/analyze-state-space.cjs +921 -0
  24. package/bin/attribute-trace-divergence.cjs +150 -0
  25. package/bin/auth-drivers/gh-cli.cjs +93 -0
  26. package/bin/auth-drivers/index.cjs +46 -0
  27. package/bin/auth-drivers/pool.cjs +67 -0
  28. package/bin/auth-drivers/simple.cjs +95 -0
  29. package/bin/autoClosePtoF.cjs +110 -0
  30. package/bin/blessed-terminal.cjs +350 -0
  31. package/bin/build-phase-index.cjs +472 -0
  32. package/bin/call-quorum-slot.cjs +541 -0
  33. package/bin/ccr-secure-config.cjs +99 -0
  34. package/bin/ccr-secure-start.cjs +83 -0
  35. package/bin/check-bundled-sdks.cjs +177 -0
  36. package/bin/check-coverage-guard.cjs +112 -0
  37. package/bin/check-liveness-fairness.cjs +95 -0
  38. package/bin/check-mcp-health.cjs +123 -0
  39. package/bin/check-provider-health.cjs +395 -0
  40. package/bin/check-results-exit.cjs +24 -0
  41. package/bin/check-spec-sync.cjs +360 -0
  42. package/bin/check-trace-redaction.cjs +271 -0
  43. package/bin/check-trace-schema-drift.cjs +99 -0
  44. package/bin/compareDrift.cjs +21 -0
  45. package/bin/conformance-schema.cjs +12 -0
  46. package/bin/count-scenarios.cjs +420 -0
  47. package/bin/debt-dedup.cjs +144 -0
  48. package/bin/debt-ledger.cjs +61 -0
  49. package/bin/debt-retention.cjs +76 -0
  50. package/bin/debt-state-machine.cjs +80 -0
  51. package/bin/detect-coverage-gaps.cjs +204 -0
  52. package/bin/detect-project-intent.cjs +362 -0
  53. package/bin/export-prism-constants.cjs +164 -0
  54. package/bin/extract-annotations.cjs +633 -0
  55. package/bin/extractFormalExpected.cjs +104 -0
  56. package/bin/fingerprint-drift.cjs +24 -0
  57. package/bin/fingerprint-issue.cjs +46 -0
  58. package/bin/formal-core.cjs +519 -0
  59. package/bin/formal-ref-linker.cjs +141 -0
  60. package/bin/formal-test-sync.cjs +788 -0
  61. package/bin/generate-formal-specs.cjs +588 -0
  62. package/bin/generate-petri-net.cjs +397 -0
  63. package/bin/generate-phase-spec.cjs +249 -0
  64. package/bin/generate-proposed-changes.cjs +194 -0
  65. package/bin/generate-tla-cfg.cjs +122 -0
  66. package/bin/generate-traceability-matrix.cjs +701 -0
  67. package/bin/generate-triage-bundle.cjs +300 -0
  68. package/bin/gh-account-rotate.cjs +34 -0
  69. package/bin/initialize-model-registry.cjs +105 -0
  70. package/bin/install-formal-tools.cjs +382 -0
  71. package/bin/install.js +2424 -0
  72. package/bin/isNumericThreshold.cjs +34 -0
  73. package/bin/issue-classifier.cjs +151 -0
  74. package/bin/levenshtein.cjs +74 -0
  75. package/bin/lint-formal-models.cjs +580 -0
  76. package/bin/load-baseline-requirements.cjs +275 -0
  77. package/bin/manage-agents-core.cjs +815 -0
  78. package/bin/migrate-formal-dir.cjs +172 -0
  79. package/bin/migrate-planning.cjs +206 -0
  80. package/bin/migrate-to-slots.cjs +255 -0
  81. package/bin/nForma.cjs +2726 -0
  82. package/bin/observe-config.cjs +353 -0
  83. package/bin/observe-debt-writer.cjs +140 -0
  84. package/bin/observe-handler-grafana.cjs +128 -0
  85. package/bin/observe-handler-internal.cjs +301 -0
  86. package/bin/observe-handler-logstash.cjs +153 -0
  87. package/bin/observe-handler-prometheus.cjs +185 -0
  88. package/bin/observe-handlers.cjs +436 -0
  89. package/bin/observe-registry.cjs +131 -0
  90. package/bin/observe-render.cjs +168 -0
  91. package/bin/planning-paths.cjs +167 -0
  92. package/bin/polyrepo.cjs +560 -0
  93. package/bin/prism-priority.cjs +153 -0
  94. package/bin/probe-quorum-slots.cjs +167 -0
  95. package/bin/promote-model.cjs +225 -0
  96. package/bin/propose-debug-invariants.cjs +165 -0
  97. package/bin/providers.json +392 -0
  98. package/bin/pty-proxy.py +129 -0
  99. package/bin/qgsd-solve.cjs +2477 -0
  100. package/bin/quorum-consensus-gate.cjs +238 -0
  101. package/bin/quorum-formal-context.cjs +183 -0
  102. package/bin/quorum-slot-dispatch.cjs +934 -0
  103. package/bin/read-policy.cjs +60 -0
  104. package/bin/requirement-map.cjs +63 -0
  105. package/bin/requirements-core.cjs +247 -0
  106. package/bin/resolve-cli.cjs +101 -0
  107. package/bin/review-mcp-logs.cjs +294 -0
  108. package/bin/run-account-manager-tlc.cjs +188 -0
  109. package/bin/run-account-pool-alloy.cjs +158 -0
  110. package/bin/run-alloy.cjs +153 -0
  111. package/bin/run-audit-alloy.cjs +187 -0
  112. package/bin/run-breaker-tlc.cjs +181 -0
  113. package/bin/run-formal-check.cjs +395 -0
  114. package/bin/run-formal-verify.cjs +701 -0
  115. package/bin/run-installer-alloy.cjs +188 -0
  116. package/bin/run-oauth-rotation-prism.cjs +132 -0
  117. package/bin/run-oscillation-tlc.cjs +202 -0
  118. package/bin/run-phase-tlc.cjs +228 -0
  119. package/bin/run-prism.cjs +446 -0
  120. package/bin/run-protocol-tlc.cjs +201 -0
  121. package/bin/run-quorum-composition-alloy.cjs +155 -0
  122. package/bin/run-sensitivity-sweep.cjs +231 -0
  123. package/bin/run-stop-hook-tlc.cjs +188 -0
  124. package/bin/run-tlc.cjs +467 -0
  125. package/bin/run-transcript-alloy.cjs +173 -0
  126. package/bin/run-uppaal.cjs +264 -0
  127. package/bin/secrets.cjs +134 -0
  128. package/bin/sensitivity-report.cjs +219 -0
  129. package/bin/sensitivity-sweep-feedback.cjs +194 -0
  130. package/bin/set-secret.cjs +29 -0
  131. package/bin/setup-telemetry-cron.sh +36 -0
  132. package/bin/sweepPtoF.cjs +63 -0
  133. package/bin/sync-baseline-requirements.cjs +290 -0
  134. package/bin/task-envelope.cjs +360 -0
  135. package/bin/telemetry-collector.cjs +229 -0
  136. package/bin/unified-mcp-server.mjs +735 -0
  137. package/bin/update-agents.cjs +369 -0
  138. package/bin/update-scoreboard.cjs +1134 -0
  139. package/bin/validate-debt-entry.cjs +207 -0
  140. package/bin/validate-invariant.cjs +419 -0
  141. package/bin/validate-memory.cjs +389 -0
  142. package/bin/validate-requirements-haiku.cjs +435 -0
  143. package/bin/validate-traces.cjs +438 -0
  144. package/bin/verify-formal-results.cjs +124 -0
  145. package/bin/verify-quorum-health.cjs +273 -0
  146. package/bin/write-check-result.cjs +106 -0
  147. package/bin/xstate-to-tla.cjs +483 -0
  148. package/bin/xstate-trace-walker.cjs +205 -0
  149. package/commands/qgsd/add-phase.md +43 -0
  150. package/commands/qgsd/add-requirement.md +24 -0
  151. package/commands/qgsd/add-todo.md +47 -0
  152. package/commands/qgsd/audit-milestone.md +37 -0
  153. package/commands/qgsd/check-todos.md +45 -0
  154. package/commands/qgsd/cleanup.md +18 -0
  155. package/commands/qgsd/close-formal-gaps.md +33 -0
  156. package/commands/qgsd/complete-milestone.md +136 -0
  157. package/commands/qgsd/debug.md +166 -0
  158. package/commands/qgsd/discuss-phase.md +83 -0
  159. package/commands/qgsd/execute-phase.md +117 -0
  160. package/commands/qgsd/fix-tests.md +27 -0
  161. package/commands/qgsd/formal-test-sync.md +32 -0
  162. package/commands/qgsd/health.md +22 -0
  163. package/commands/qgsd/help.md +22 -0
  164. package/commands/qgsd/insert-phase.md +32 -0
  165. package/commands/qgsd/join-discord.md +18 -0
  166. package/commands/qgsd/list-phase-assumptions.md +46 -0
  167. package/commands/qgsd/map-codebase.md +71 -0
  168. package/commands/qgsd/map-requirements.md +20 -0
  169. package/commands/qgsd/mcp-restart.md +176 -0
  170. package/commands/qgsd/mcp-set-model.md +134 -0
  171. package/commands/qgsd/mcp-setup.md +1371 -0
  172. package/commands/qgsd/mcp-status.md +274 -0
  173. package/commands/qgsd/mcp-update.md +238 -0
  174. package/commands/qgsd/new-milestone.md +44 -0
  175. package/commands/qgsd/new-project.md +42 -0
  176. package/commands/qgsd/observe.md +260 -0
  177. package/commands/qgsd/pause-work.md +38 -0
  178. package/commands/qgsd/plan-milestone-gaps.md +34 -0
  179. package/commands/qgsd/plan-phase.md +44 -0
  180. package/commands/qgsd/polyrepo.md +50 -0
  181. package/commands/qgsd/progress.md +24 -0
  182. package/commands/qgsd/queue.md +54 -0
  183. package/commands/qgsd/quick.md +133 -0
  184. package/commands/qgsd/quorum-test.md +275 -0
  185. package/commands/qgsd/quorum.md +707 -0
  186. package/commands/qgsd/reapply-patches.md +110 -0
  187. package/commands/qgsd/remove-phase.md +31 -0
  188. package/commands/qgsd/research-phase.md +189 -0
  189. package/commands/qgsd/resume-work.md +40 -0
  190. package/commands/qgsd/set-profile.md +34 -0
  191. package/commands/qgsd/settings.md +39 -0
  192. package/commands/qgsd/solve.md +565 -0
  193. package/commands/qgsd/sync-baselines.md +119 -0
  194. package/commands/qgsd/triage.md +233 -0
  195. package/commands/qgsd/update.md +37 -0
  196. package/commands/qgsd/verify-work.md +38 -0
  197. package/hooks/dist/config-loader.js +297 -0
  198. package/hooks/dist/conformance-schema.cjs +12 -0
  199. package/hooks/dist/gsd-context-monitor.js +64 -0
  200. package/hooks/dist/qgsd-check-update.js +62 -0
  201. package/hooks/dist/qgsd-circuit-breaker.js +682 -0
  202. package/hooks/dist/qgsd-precompact.js +156 -0
  203. package/hooks/dist/qgsd-prompt.js +653 -0
  204. package/hooks/dist/qgsd-session-start.js +122 -0
  205. package/hooks/dist/qgsd-slot-correlator.js +58 -0
  206. package/hooks/dist/qgsd-spec-regen.js +86 -0
  207. package/hooks/dist/qgsd-statusline.js +91 -0
  208. package/hooks/dist/qgsd-stop.js +553 -0
  209. package/hooks/dist/qgsd-token-collector.js +133 -0
  210. package/hooks/dist/unified-mcp-server.mjs +669 -0
  211. package/package.json +95 -0
  212. package/scripts/build-hooks.js +46 -0
  213. package/scripts/postinstall.js +48 -0
  214. package/scripts/secret-audit.sh +45 -0
  215. package/templates/qgsd.json +49 -0
@@ -0,0 +1,435 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * validate-requirements-haiku.cjs
6
+ *
7
+ * Semantic requirements validator using Claude Haiku
8
+ *
9
+ * Detects:
10
+ * - DUPLICATES: Different requirement IDs with same intent
11
+ * - CONTRADICTIONS: Requirements that cannot both be satisfied
12
+ * - AMBIGUITY: Requirements with multiple incompatible interpretations
13
+ *
14
+ * Usage:
15
+ * node bin/validate-requirements-haiku.cjs [--envelope=path] [--passes=3] [--freeze]
16
+ *
17
+ * Exits with code 0 for validation complete (regardless of findings).
18
+ * Exits with code 1 for operational errors (missing file, API failure, etc).
19
+ */
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const https = require('https');
24
+
25
+ // ─────────────────────────────────────────────────────────────────────────────
26
+ // Raw HTTPS Haiku API helper (replaces SDK dependency)
27
+ // ─────────────────────────────────────────────────────────────────────────────
28
+
29
+ function callHaikuAPI(apiKey, prompt, maxTokens) {
30
+ return new Promise((resolve, reject) => {
31
+ const body = JSON.stringify({
32
+ model: 'claude-haiku-4-5-20251001',
33
+ max_tokens: maxTokens,
34
+ messages: [{ role: 'user', content: prompt }],
35
+ });
36
+
37
+ const req = https.request({
38
+ hostname: 'api.anthropic.com',
39
+ path: '/v1/messages',
40
+ method: 'POST',
41
+ headers: {
42
+ 'Content-Type': 'application/json',
43
+ 'x-api-key': apiKey,
44
+ 'anthropic-version': '2023-06-01',
45
+ 'Content-Length': Buffer.byteLength(body),
46
+ },
47
+ timeout: 30000,
48
+ }, (res) => {
49
+ let data = '';
50
+ res.on('data', chunk => { data += chunk; });
51
+ res.on('end', () => {
52
+ try {
53
+ const parsed = JSON.parse(data);
54
+ const text = ((parsed.content || [])[0] || {}).text || '';
55
+ resolve(text);
56
+ } catch (e) { reject(e); }
57
+ });
58
+ });
59
+ req.on('error', reject);
60
+ req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
61
+ req.write(body);
62
+ req.end();
63
+ });
64
+ }
65
+
66
+ // ─────────────────────────────────────────────────────────────────────────────
67
+ // buildValidationPrompt
68
+ // ─────────────────────────────────────────────────────────────────────────────
69
+
70
+ /**
71
+ * Construct the Haiku validation prompt with explicit rubrics
72
+ * @param {Array} requirements - Array of requirement objects { id, text, category, phase }
73
+ * @returns {string} The prompt string
74
+ */
75
+ function buildValidationPrompt(requirements) {
76
+ const reqList = requirements
77
+ .map(r => `- ${r.id}: ${r.text}`)
78
+ .join('\n');
79
+
80
+ const prompt = `You are reviewing a requirements envelope for quality issues.
81
+
82
+ Requirements:
83
+ ${reqList}
84
+
85
+ Analyze for exactly these three categories:
86
+
87
+ 1. DUPLICATES: Different IDs where the INTENT is the same (not just similar wording).
88
+ Example: "Users can log in" and "Authentication allows user access" are duplicates.
89
+ NOT duplicates: "Users can log in" and "Admins can log in" (different scope).
90
+
91
+ 2. CONTRADICTIONS: Two requirements that CANNOT BOTH be satisfied.
92
+ Example: "Data is immutable" and "Data auto-updates daily" are contradictory.
93
+ NOT contradictions: "Data is immutable" and "Amendments require approval" (compatible).
94
+
95
+ 3. AMBIGUITY: A single requirement that admits two or more INCOMPATIBLE interpretations.
96
+ Example: "The system should be fast" (fast for whom? what metric?).
97
+ NOT ambiguous: "Response time under 200ms for 95th percentile" (specific).
98
+
99
+ Return ONLY valid JSON (no markdown, no explanation):
100
+ {
101
+ "findings": [
102
+ {
103
+ "type": "duplicate|contradiction|ambiguity",
104
+ "requirement_ids": ["ENV-XX", "ENV-YY"],
105
+ "description": "...",
106
+ "severity": "high|medium|low",
107
+ "suggested_resolution": "..."
108
+ }
109
+ ],
110
+ "summary": "N duplicates, N contradictions, N ambiguities found"
111
+ }
112
+
113
+ If no issues found, return: {"findings": [], "summary": "No issues found"}`;
114
+
115
+ return prompt;
116
+ }
117
+
118
+ // ─────────────────────────────────────────────────────────────────────────────
119
+ // parseHaikuResponse
120
+ // ─────────────────────────────────────────────────────────────────────────────
121
+
122
+ /**
123
+ * Parse Haiku JSON response, handling markdown wrapping
124
+ * @param {string} responseText - Raw Haiku response
125
+ * @returns {Object} Parsed findings or error object
126
+ */
127
+ function parseHaikuResponse(responseText) {
128
+ try {
129
+ // Try to extract JSON from markdown code fence
130
+ let jsonStr = responseText;
131
+ const codeBlockMatch = responseText.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
132
+ if (codeBlockMatch) {
133
+ jsonStr = codeBlockMatch[1];
134
+ }
135
+
136
+ const parsed = JSON.parse(jsonStr);
137
+
138
+ // Validate structure
139
+ if (!Array.isArray(parsed.findings)) {
140
+ return { findings: [], summary: 'Invalid structure', error: true };
141
+ }
142
+
143
+ // Ensure each finding has required fields
144
+ for (const finding of parsed.findings) {
145
+ if (!finding.type || !Array.isArray(finding.requirement_ids) || !finding.description) {
146
+ return { findings: [], summary: 'Invalid finding structure', error: true };
147
+ }
148
+ }
149
+
150
+ return parsed;
151
+ } catch (e) {
152
+ return { findings: [], summary: 'Parse error: ' + e.message, error: true };
153
+ }
154
+ }
155
+
156
+ // ─────────────────────────────────────────────────────────────────────────────
157
+ // aggregateFindings
158
+ // ─────────────────────────────────────────────────────────────────────────────
159
+
160
+ /**
161
+ * Aggregate multi-pass results — only report findings in 2+ of N passes
162
+ * @param {Array} passResults - Array of parsed responses from Haiku passes
163
+ * @returns {Object} { confirmed, total_passes, agreement_threshold }
164
+ */
165
+ function aggregateFindings(passResults) {
166
+ const total_passes = passResults.length;
167
+ const agreement_threshold = Math.ceil(total_passes / 2);
168
+
169
+ // Collect all unique findings with pass counts
170
+ const findingMap = new Map();
171
+
172
+ for (let passIdx = 0; passIdx < passResults.length; passIdx++) {
173
+ const result = passResults[passIdx];
174
+ if (!result.findings) continue;
175
+
176
+ for (const finding of result.findings) {
177
+ // Create a key for matching: type + sorted IDs
178
+ const key = JSON.stringify({
179
+ type: finding.type,
180
+ ids: Array.from(new Set(finding.requirement_ids)).sort(),
181
+ });
182
+
183
+ if (!findingMap.has(key)) {
184
+ findingMap.set(key, {
185
+ finding: finding,
186
+ passes: new Set(),
187
+ });
188
+ }
189
+
190
+ findingMap.get(key).passes.add(passIdx);
191
+ }
192
+ }
193
+
194
+ // Filter to confirmed findings (2+ passes)
195
+ const confirmed = Array.from(findingMap.values())
196
+ .filter(item => item.passes.size >= agreement_threshold)
197
+ .map(item => item.finding);
198
+
199
+ return { confirmed, total_passes, agreement_threshold };
200
+ }
201
+
202
+ // ─────────────────────────────────────────────────────────────────────────────
203
+ // validateRequirements
204
+ // ─────────────────────────────────────────────────────────────────────────────
205
+
206
+ /**
207
+ * Main validation pipeline
208
+ * @param {Object} options - { envelopePath, passes, apiKey, mockCall }
209
+ * @returns {Promise<Object>} Validation result
210
+ */
211
+ async function validateRequirements(options = {}) {
212
+ const {
213
+ envelopePath = '.planning/formal/requirements.json',
214
+ passes = 3,
215
+ apiKey = process.env.ANTHROPIC_API_KEY,
216
+ mockCall = null, // For testing: mock Haiku response
217
+ } = options;
218
+
219
+ // Read envelope first (needed to check frozen_at before SDK availability)
220
+ if (!fs.existsSync(envelopePath)) {
221
+ return { status: 'error', reason: `Envelope file not found: ${envelopePath}` };
222
+ }
223
+
224
+ let envelope;
225
+ try {
226
+ const content = fs.readFileSync(envelopePath, 'utf8');
227
+ envelope = JSON.parse(content);
228
+ } catch (e) {
229
+ return { status: 'error', reason: `Failed to read envelope: ${e.message}` };
230
+ }
231
+
232
+ // Check if already frozen (before SDK check)
233
+ if (envelope.frozen_at) {
234
+ return { status: 'already-frozen', frozen_at: envelope.frozen_at };
235
+ }
236
+
237
+ // Check if API key is available (skip check if using mockCall)
238
+ if (!apiKey && !mockCall) {
239
+ return { status: 'skipped', reason: 'ANTHROPIC_API_KEY not set' };
240
+ }
241
+
242
+ // Extract requirements array
243
+ const requirements = Array.isArray(envelope.requirements) ? envelope.requirements : [];
244
+ if (requirements.length === 0) {
245
+ return { status: 'validated', confirmed: [], total_passes: 0, message: 'No requirements to validate' };
246
+ }
247
+
248
+ // Build prompt
249
+ const prompt = buildValidationPrompt(requirements);
250
+
251
+ // Run Haiku passes
252
+ const rawPasses = [];
253
+
254
+ try {
255
+ for (let i = 0; i < passes; i++) {
256
+ let responseText;
257
+
258
+ if (mockCall) {
259
+ // For testing
260
+ responseText = mockCall();
261
+ } else {
262
+ // Real Haiku call via raw HTTPS
263
+ responseText = await callHaikuAPI(apiKey, prompt, 4096);
264
+ }
265
+
266
+ const parsed = parseHaikuResponse(responseText);
267
+ rawPasses.push(parsed);
268
+ }
269
+ } catch (e) {
270
+ return { status: 'error', reason: `Haiku call failed: ${e.message}` };
271
+ }
272
+
273
+ // Aggregate findings
274
+ const { confirmed, total_passes, agreement_threshold } = aggregateFindings(rawPasses);
275
+
276
+ return {
277
+ status: 'validated',
278
+ confirmed,
279
+ total_passes,
280
+ agreement_threshold,
281
+ raw_passes: rawPasses,
282
+ };
283
+ }
284
+
285
+ // ─────────────────────────────────────────────────────────────────────────────
286
+ // freezeEnvelope
287
+ // ─────────────────────────────────────────────────────────────────────────────
288
+
289
+ /**
290
+ * Freeze the envelope by setting frozen_at timestamp
291
+ * @param {string} envelopePath - Path to envelope file
292
+ * @returns {Object} { frozen: true, frozen_at }
293
+ */
294
+ function freezeEnvelope(envelopePath) {
295
+ if (!fs.existsSync(envelopePath)) {
296
+ throw new Error(`Envelope file not found: ${envelopePath}`);
297
+ }
298
+
299
+ let envelope;
300
+ try {
301
+ const content = fs.readFileSync(envelopePath, 'utf8');
302
+ envelope = JSON.parse(content);
303
+ } catch (e) {
304
+ throw new Error(`Failed to read envelope: ${e.message}`);
305
+ }
306
+
307
+ // Set frozen_at timestamp
308
+ const frozen_at = new Date().toISOString();
309
+ envelope.frozen_at = frozen_at;
310
+
311
+ // Write atomically: temp file + rename
312
+ const dir = path.dirname(envelopePath);
313
+ const basename = path.basename(envelopePath);
314
+ const tempPath = path.join(dir, '.' + basename + '.tmp');
315
+
316
+ try {
317
+ fs.writeFileSync(tempPath, JSON.stringify(envelope, null, 2) + '\n', 'utf8');
318
+ fs.renameSync(tempPath, envelopePath);
319
+ } catch (e) {
320
+ // Clean up temp file if rename failed
321
+ try {
322
+ fs.unlinkSync(tempPath);
323
+ } catch (e2) {
324
+ // Ignore cleanup error
325
+ }
326
+ throw new Error(`Failed to write envelope: ${e.message}`);
327
+ }
328
+
329
+ return { frozen: true, frozen_at };
330
+ }
331
+
332
+ // ─────────────────────────────────────────────────────────────────────────────
333
+ // CLI entrypoint
334
+ // ─────────────────────────────────────────────────────────────────────────────
335
+
336
+ async function main() {
337
+ // Parse CLI arguments
338
+ const args = {};
339
+ for (let i = 2; i < process.argv.length; i++) {
340
+ const arg = process.argv[i];
341
+ if (arg.startsWith('--')) {
342
+ const [key, value] = arg.slice(2).split('=');
343
+ args[key] = value || true;
344
+ }
345
+ }
346
+
347
+ const envelopePath = args.envelope || '.planning/formal/requirements.json';
348
+ const passes = parseInt(args.passes || '3', 10);
349
+ const shouldFreeze = args.freeze === true || args.freeze === '';
350
+
351
+ try {
352
+ // Run validation
353
+ const result = await validateRequirements({ envelopePath, passes });
354
+
355
+ if (result.status === 'skipped') {
356
+ console.log(`Validation skipped: ${result.reason}`);
357
+ process.exit(0);
358
+ }
359
+
360
+ if (result.status === 'error') {
361
+ console.error(`Validation error: ${result.reason}`);
362
+ process.exit(1);
363
+ }
364
+
365
+ if (result.status === 'already-frozen') {
366
+ console.log(`Envelope already frozen at ${result.frozen_at}`);
367
+ process.exit(0);
368
+ }
369
+
370
+ // Display results
371
+ console.log(`\nValidation Results (${result.total_passes} passes, ${result.agreement_threshold}+ agreement):\n`);
372
+
373
+ if (result.confirmed.length === 0) {
374
+ console.log('CONFIRMED FINDINGS: None\n');
375
+ } else {
376
+ console.log('CONFIRMED FINDINGS:');
377
+ for (const finding of result.confirmed) {
378
+ const severity = finding.severity?.toUpperCase() || 'MEDIUM';
379
+ const type = finding.type.toUpperCase();
380
+ const ids = finding.requirement_ids.join(', ');
381
+ console.log(` [${severity}] ${type}: ${ids} -- ${finding.description}`);
382
+ }
383
+ console.log('');
384
+ }
385
+
386
+ // Summary
387
+ const counts = {
388
+ duplicates: result.confirmed.filter(f => f.type === 'duplicate').length,
389
+ contradictions: result.confirmed.filter(f => f.type === 'contradiction').length,
390
+ ambiguities: result.confirmed.filter(f => f.type === 'ambiguity').length,
391
+ };
392
+
393
+ const summary = `${counts.duplicates} duplicates, ${counts.contradictions} contradictions, ${counts.ambiguities} ambiguities`;
394
+ console.log(`Summary: ${summary}\n`);
395
+
396
+ // Freeze if requested and no high-severity findings
397
+ if (shouldFreeze) {
398
+ const hasHighSeverity = result.confirmed.some(f => f.severity === 'high');
399
+ if (hasHighSeverity) {
400
+ console.log('Cannot freeze: high-severity findings must be resolved first\n');
401
+ process.exit(0);
402
+ }
403
+
404
+ const frozen = freezeEnvelope(envelopePath);
405
+ console.log(`Envelope frozen at: ${frozen.frozen_at}`);
406
+ } else {
407
+ console.log(`To freeze: node bin/validate-requirements-haiku.cjs --freeze`);
408
+ }
409
+
410
+ process.exit(0);
411
+ } catch (e) {
412
+ console.error(`Fatal error: ${e.message}`);
413
+ process.exit(1);
414
+ }
415
+ }
416
+
417
+ // ─────────────────────────────────────────────────────────────────────────────
418
+ // Exports
419
+ // ─────────────────────────────────────────────────────────────────────────────
420
+
421
+ module.exports = {
422
+ buildValidationPrompt,
423
+ parseHaikuResponse,
424
+ validateRequirements,
425
+ aggregateFindings,
426
+ freezeEnvelope,
427
+ };
428
+
429
+ // Run CLI if this is the main module
430
+ if (require.main === module) {
431
+ main().catch(err => {
432
+ console.error(`Unhandled error: ${err.message}`);
433
+ process.exit(1);
434
+ });
435
+ }