@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,290 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const crypto = require('crypto');
7
+
8
+ /**
9
+ * Internal helper: merge baseline into existing requirements.
10
+ * Handles the core sync logic (steps 2-7 from original implementation).
11
+ */
12
+ function _syncFromBaseline(baseline, projectRoot) {
13
+ const root = projectRoot || process.cwd();
14
+
15
+ // 2. Read existing requirements
16
+ const reqPath = path.join(root, '.planning', 'formal', 'requirements.json');
17
+ let rawEnvelope;
18
+ let requirements;
19
+
20
+ if (fs.existsSync(reqPath)) {
21
+ try {
22
+ rawEnvelope = JSON.parse(fs.readFileSync(reqPath, 'utf8'));
23
+ requirements = rawEnvelope.requirements || [];
24
+ } catch (_) {
25
+ rawEnvelope = {};
26
+ requirements = [];
27
+ }
28
+ } else {
29
+ // Create .formal directory if needed
30
+ fs.mkdirSync(path.join(root, '.planning', 'formal'), { recursive: true });
31
+ rawEnvelope = {};
32
+ requirements = [];
33
+ }
34
+
35
+ // 3. Build lookup of existing requirement texts -> id
36
+ const existingTexts = new Map(requirements.map(r => [r.text, r.id]));
37
+
38
+ // 4. Build map of highest existing ID number per prefix
39
+ const maxId = {};
40
+ for (const r of requirements) {
41
+ if (!r.id) continue;
42
+ const dashIdx = r.id.lastIndexOf('-');
43
+ if (dashIdx === -1) continue;
44
+ const prefix = r.id.substring(0, dashIdx);
45
+ const num = parseInt(r.id.substring(dashIdx + 1), 10);
46
+ if (!isNaN(num)) {
47
+ maxId[prefix] = Math.max(maxId[prefix] || 0, num);
48
+ }
49
+ }
50
+
51
+ const added = [];
52
+ const skipped = [];
53
+ const totalBefore = requirements.length;
54
+
55
+ // 5-6. Process each baseline category
56
+ for (const cat of baseline.categories) {
57
+ if (!cat.requirements || cat.requirements.length === 0) continue;
58
+
59
+ // Derive prefix from first requirement's ID (e.g., "UX-01" -> "UX")
60
+ const firstId = cat.requirements[0].id;
61
+ const dashIdx = firstId.lastIndexOf('-');
62
+ const prefix = firstId.substring(0, dashIdx);
63
+
64
+ for (const req of cat.requirements) {
65
+ if (existingTexts.has(req.text)) {
66
+ // 6a. Skip duplicate
67
+ skipped.push({
68
+ id: req.id,
69
+ text: req.text,
70
+ existingId: existingTexts.get(req.text),
71
+ });
72
+ } else {
73
+ // 6c. Assign next available ID
74
+ maxId[prefix] = (maxId[prefix] || 0) + 1;
75
+ const padLen = maxId[prefix] > 99 ? String(maxId[prefix]).length : 2;
76
+ const newId = prefix + '-' + String(maxId[prefix]).padStart(padLen, '0');
77
+
78
+ const newReq = {
79
+ id: newId,
80
+ text: req.text,
81
+ category: cat.name,
82
+ phase: 'baseline',
83
+ status: 'Pending',
84
+ provenance: {
85
+ source_file: 'qgsd-baseline',
86
+ milestone: 'baseline',
87
+ },
88
+ };
89
+
90
+ requirements.push(newReq);
91
+ existingTexts.set(req.text, newId);
92
+ added.push({ id: newId, text: req.text });
93
+ }
94
+ }
95
+ }
96
+
97
+ // 7. Write if anything was added
98
+ if (added.length > 0) {
99
+ const contentHash = 'sha256:' + crypto
100
+ .createHash('sha256')
101
+ .update(JSON.stringify(requirements, null, 2))
102
+ .digest('hex');
103
+
104
+ // Defensive: only write if hash actually changed
105
+ const existingHash = rawEnvelope.content_hash || null;
106
+ if (existingHash !== contentHash) {
107
+ const envelope = {
108
+ aggregated_at: new Date().toISOString(),
109
+ content_hash: contentHash,
110
+ frozen_at: rawEnvelope.frozen_at || null,
111
+ schema_version: rawEnvelope.schema_version || undefined,
112
+ requirements,
113
+ };
114
+
115
+ // Remove undefined keys
116
+ if (envelope.schema_version === undefined) delete envelope.schema_version;
117
+
118
+ fs.writeFileSync(reqPath, JSON.stringify(envelope, null, 2) + '\n');
119
+ }
120
+ }
121
+
122
+ return {
123
+ added,
124
+ skipped,
125
+ total_before: totalBefore,
126
+ total_after: requirements.length,
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Merge baseline requirements into .planning/formal/requirements.json.
132
+ * Idempotent: matches on exact `text` field to skip duplicates.
133
+ * Assigns next-available IDs per category prefix for new entries.
134
+ *
135
+ * @param {string} profile - One of: web, mobile, desktop, api, cli, library
136
+ * @param {string} [projectRoot] - Path to project root, defaults to process.cwd()
137
+ * @returns {{ added: Array, skipped: Array, total_before: number, total_after: number }}
138
+ */
139
+ function syncBaselineRequirements(profile, projectRoot) {
140
+ const root = projectRoot || process.cwd();
141
+
142
+ // 1. Load baseline requirements
143
+ let baseline;
144
+ try {
145
+ const { loadBaselineRequirements } = require('./load-baseline-requirements.cjs');
146
+ baseline = loadBaselineRequirements(profile);
147
+ } catch (err) {
148
+ console.error(`Error loading baseline requirements: ${err.message}`);
149
+ process.exit(2);
150
+ }
151
+
152
+ return _syncFromBaseline(baseline, root);
153
+ }
154
+
155
+ /**
156
+ * Merge intent-based baseline requirements into .planning/formal/requirements.json.
157
+ * Idempotent: matches on exact `text` field to skip duplicates.
158
+ * Assigns next-available IDs per category prefix for new entries.
159
+ *
160
+ * @param {Object} intent - Intent object with base_profile and optional dimensions
161
+ * @param {string} [projectRoot] - Path to project root, defaults to process.cwd()
162
+ * @returns {{ added: Array, skipped: Array, total_before: number, total_after: number }}
163
+ */
164
+ function syncBaselineRequirementsFromIntent(intent, projectRoot) {
165
+ const root = projectRoot || process.cwd();
166
+
167
+ // 1. Load baseline requirements from intent
168
+ let baseline;
169
+ try {
170
+ const { loadBaselineRequirementsFromIntent } = require('./load-baseline-requirements.cjs');
171
+ baseline = loadBaselineRequirementsFromIntent(intent);
172
+ } catch (err) {
173
+ console.error(`Error loading baseline requirements: ${err.message}`);
174
+ process.exit(2);
175
+ }
176
+
177
+ return _syncFromBaseline(baseline, root);
178
+ }
179
+
180
+ // ---------------------------------------------------------------------------
181
+ // CLI
182
+ // ---------------------------------------------------------------------------
183
+
184
+ function printReport(result, profile) {
185
+ console.log(`Baseline sync: ${profile} profile`);
186
+ console.log(` Before: ${result.total_before} requirements`);
187
+ console.log(` Added: ${result.added.length} new requirements`);
188
+ console.log(` Skipped: ${result.skipped.length} (already present by text match)`);
189
+ console.log(` After: ${result.total_after} requirements`);
190
+
191
+ if (result.added.length > 0) {
192
+ console.log('\nAdded:');
193
+ for (const a of result.added) {
194
+ console.log(` + [${a.id}] ${a.text}`);
195
+ }
196
+ }
197
+
198
+ if (result.skipped.length > 0) {
199
+ console.log('\nSkipped:');
200
+ for (const s of result.skipped) {
201
+ console.log(` ~ [${s.id}] matched existing [${s.existingId}]`);
202
+ }
203
+ }
204
+ }
205
+
206
+ if (require.main === module) {
207
+ const args = process.argv.slice(2);
208
+
209
+ // --json flag
210
+ const jsonOutput = args.includes('--json');
211
+
212
+ // --detect is deprecated (auto-detect is now the default behavior) — silently strip it
213
+ const cleanArgs = args.filter(arg => arg !== '--detect');
214
+
215
+ // Priority: --intent-file > --profile > config.json intent > config.json profile > AUTO-DETECT
216
+
217
+ // Check --intent-file
218
+ const intentFileIdx = cleanArgs.indexOf('--intent-file');
219
+ if (intentFileIdx !== -1 && cleanArgs[intentFileIdx + 1]) {
220
+ try {
221
+ const intentFilePath = cleanArgs[intentFileIdx + 1];
222
+ const intentContent = fs.readFileSync(intentFilePath, 'utf8');
223
+ const intent = JSON.parse(intentContent);
224
+ const result = syncBaselineRequirementsFromIntent(intent);
225
+ if (jsonOutput) {
226
+ console.log(JSON.stringify(result, null, 2));
227
+ } else {
228
+ printReport(result, `intent (base_profile: ${intent.base_profile})`);
229
+ }
230
+ process.exit(0);
231
+ } catch (err) {
232
+ console.error(`Error loading intent file: ${err.message}`);
233
+ process.exit(1);
234
+ }
235
+ }
236
+
237
+ // Parse --profile
238
+ const profileIdx = cleanArgs.indexOf('--profile');
239
+ let profile = null;
240
+
241
+ if (profileIdx !== -1 && cleanArgs[profileIdx + 1]) {
242
+ profile = cleanArgs[profileIdx + 1];
243
+ } else {
244
+ // Try reading from .planning/config.json (intent first, then profile)
245
+ try {
246
+ const config = JSON.parse(fs.readFileSync(
247
+ path.join(process.cwd(), '.planning/config.json'), 'utf8'
248
+ ));
249
+ if (config.intent) {
250
+ const result = syncBaselineRequirementsFromIntent(config.intent);
251
+ if (jsonOutput) {
252
+ console.log(JSON.stringify(result, null, 2));
253
+ } else {
254
+ printReport(result, `config intent (base_profile: ${config.intent.base_profile})`);
255
+ }
256
+ process.exit(0);
257
+ }
258
+ profile = config.profile;
259
+ } catch (_) {}
260
+ }
261
+
262
+ // If no profile found, auto-detect (new default behavior)
263
+ if (!profile) {
264
+ try {
265
+ const { detectProjectIntent } = require('./detect-project-intent.cjs');
266
+ const detectionResult = detectProjectIntent(process.cwd());
267
+ const intent = detectionResult.suggested;
268
+ const result = syncBaselineRequirementsFromIntent(intent);
269
+ if (jsonOutput) {
270
+ console.log(JSON.stringify({ ...result, detection: detectionResult }, null, 2));
271
+ } else {
272
+ printReport(result, `auto-detected intent (base_profile: ${intent.base_profile})`);
273
+ }
274
+ process.exit(0);
275
+ } catch (err) {
276
+ console.error(`Error auto-detecting project intent: ${err.message}`);
277
+ console.error('Hint: use --profile <web|mobile|desktop|api|cli|library> to specify manually');
278
+ process.exit(1);
279
+ }
280
+ }
281
+
282
+ const result = syncBaselineRequirements(profile);
283
+ if (jsonOutput) {
284
+ console.log(JSON.stringify(result, null, 2));
285
+ } else {
286
+ printReport(result, profile);
287
+ }
288
+ }
289
+
290
+ module.exports = { syncBaselineRequirements, syncBaselineRequirementsFromIntent };
@@ -0,0 +1,360 @@
1
+ #!/usr/bin/env node
2
+ // bin/task-envelope.cjs
3
+ // Task envelope CLI tool for structured research→plan→quorum handoff
4
+ // Provides: init, update, read, validate commands + exported validation functions
5
+
6
+ 'use strict';
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ // Schema definition
12
+ const ENVELOPE_SCHEMA = {
13
+ schema_version: 'string (must be "1")',
14
+ phase: 'string (must match v\\d+\\.\\d+-\\d{2})',
15
+ created_at: 'string (ISO 8601, auto-generated)',
16
+ risk_level: 'string (low|medium|high)',
17
+ research: 'object (optional)',
18
+ plan: 'object (optional)',
19
+ quorum: 'object (optional)'
20
+ };
21
+
22
+ // Validate envelope against schema
23
+ function validateEnvelope(obj) {
24
+ const errors = [];
25
+
26
+ if (!obj || typeof obj !== 'object') {
27
+ errors.push('Envelope must be an object');
28
+ return { valid: false, errors };
29
+ }
30
+
31
+ // Check schema_version
32
+ if (typeof obj.schema_version !== 'string' || obj.schema_version !== '1') {
33
+ errors.push('schema_version must be string "1"');
34
+ }
35
+
36
+ // Check phase format: v\d+\.\d+-\d{2}
37
+ if (!obj.phase || typeof obj.phase !== 'string' || !/^v\d+\.\d+-\d{2}/.test(obj.phase)) {
38
+ errors.push('phase must match format v\\d+\\.\\d+-\\d{2} (e.g., v0.18-03)');
39
+ }
40
+
41
+ // Check risk_level
42
+ if (!obj.risk_level || !['low', 'medium', 'high'].includes(obj.risk_level)) {
43
+ errors.push('risk_level must be one of: low, medium, high');
44
+ }
45
+
46
+ return {
47
+ valid: errors.length === 0,
48
+ errors
49
+ };
50
+ }
51
+
52
+ // Find phase directory in .planning/phases/
53
+ function findPhaseDir(phaseId) {
54
+ const phasesDir = path.join(process.cwd(), '.planning', 'phases');
55
+
56
+ if (!fs.existsSync(phasesDir)) {
57
+ return null;
58
+ }
59
+
60
+ // Look for directory matching phaseId or phaseId-*
61
+ const dirs = fs.readdirSync(phasesDir);
62
+ const match = dirs.find(d => d === phaseId || d.startsWith(phaseId + '-'));
63
+
64
+ if (!match) {
65
+ return null;
66
+ }
67
+
68
+ return path.join(phasesDir, match);
69
+ }
70
+
71
+ // Get envelope path for a phase
72
+ function getEnvelopePath(phaseId) {
73
+ const phaseDir = findPhaseDir(phaseId);
74
+ if (!phaseDir) {
75
+ return null;
76
+ }
77
+ return path.join(phaseDir, 'task-envelope.json');
78
+ }
79
+
80
+ // Write JSON atomically: tmpPath + renameSync
81
+ function writeAtomicJson(targetPath, obj) {
82
+ const dir = path.dirname(targetPath);
83
+
84
+ // Ensure directory exists
85
+ if (!fs.existsSync(dir)) {
86
+ fs.mkdirSync(dir, { recursive: true });
87
+ }
88
+
89
+ const tmpPath = `${targetPath}.${Date.now()}.tmp`;
90
+
91
+ try {
92
+ fs.writeFileSync(tmpPath, JSON.stringify(obj, null, 2), 'utf8');
93
+ fs.renameSync(tmpPath, targetPath);
94
+ return true;
95
+ } catch (e) {
96
+ // Clean up temp file if rename failed
97
+ try {
98
+ fs.unlinkSync(tmpPath);
99
+ } catch {}
100
+ throw e;
101
+ }
102
+ }
103
+
104
+ // Parse comma-separated arguments into array
105
+ function parseCommaList(str) {
106
+ if (!str) return [];
107
+ return str.split(',').map(s => s.trim()).filter(s => s.length > 0);
108
+ }
109
+
110
+ // main: handle CLI commands
111
+ function main() {
112
+ const args = process.argv.slice(2);
113
+
114
+ if (args.length === 0) {
115
+ process.stderr.write('[task-envelope] ERROR: no command specified\n');
116
+ process.stderr.write('Usage: task-envelope.cjs <init|update|read|validate> [options]\n');
117
+ process.exit(1);
118
+ }
119
+
120
+ const command = args[0];
121
+
122
+ try {
123
+ if (command === 'init') {
124
+ commandInit(args);
125
+ } else if (command === 'update') {
126
+ commandUpdate(args);
127
+ } else if (command === 'read') {
128
+ commandRead(args);
129
+ } else if (command === 'validate') {
130
+ commandValidate(args);
131
+ } else {
132
+ process.stderr.write(`[task-envelope] ERROR: unknown command "${command}"\n`);
133
+ process.exit(1);
134
+ }
135
+ } catch (e) {
136
+ process.stderr.write(`[task-envelope] ERROR: ${e.message}\n`);
137
+ process.exit(1);
138
+ }
139
+ }
140
+
141
+ // Command: init
142
+ function commandInit(args) {
143
+ // Parse arguments
144
+ let phase, objective = '', constraints = '', targetFiles = [], confidence = 'HIGH', riskLevel = 'medium';
145
+
146
+ for (let i = 1; i < args.length; i++) {
147
+ if (args[i] === '--phase' && i + 1 < args.length) {
148
+ phase = args[++i];
149
+ } else if (args[i] === '--objective' && i + 1 < args.length) {
150
+ objective = args[++i];
151
+ } else if (args[i] === '--constraints' && i + 1 < args.length) {
152
+ constraints = args[++i];
153
+ } else if (args[i] === '--target-files' && i + 1 < args.length) {
154
+ targetFiles = parseCommaList(args[++i]);
155
+ } else if (args[i] === '--confidence' && i + 1 < args.length) {
156
+ confidence = args[++i];
157
+ } else if (args[i] === '--risk-level' && i + 1 < args.length) {
158
+ riskLevel = args[++i];
159
+ }
160
+ }
161
+
162
+ if (!phase) {
163
+ process.stderr.write('[task-envelope] ERROR: --phase is required\n');
164
+ process.exit(1);
165
+ }
166
+
167
+ // Validate risk_level
168
+ const validRiskLevels = ['low', 'medium', 'high'];
169
+ let finalRiskLevel = riskLevel;
170
+ if (!validRiskLevels.includes(riskLevel)) {
171
+ process.stderr.write(`[task-envelope] WARNING: invalid risk_level "${riskLevel}"; using "medium"\n`);
172
+ finalRiskLevel = 'medium';
173
+ }
174
+
175
+ // Find phase directory
176
+ const phaseDir = findPhaseDir(phase);
177
+ if (!phaseDir) {
178
+ process.stderr.write(`[task-envelope] ERROR: phase directory not found for "${phase}"\n`);
179
+ process.exit(1);
180
+ }
181
+
182
+ // Create envelope object
183
+ const envelope = {
184
+ schema_version: '1',
185
+ phase,
186
+ created_at: new Date().toISOString(),
187
+ risk_level: finalRiskLevel,
188
+ research: {
189
+ objective,
190
+ constraints,
191
+ target_files: targetFiles,
192
+ confidence
193
+ }
194
+ };
195
+
196
+ // Validate and write
197
+ const validation = validateEnvelope(envelope);
198
+ if (!validation.valid) {
199
+ process.stderr.write(`[task-envelope] ERROR: invalid envelope: ${validation.errors.join('; ')}\n`);
200
+ process.exit(1);
201
+ }
202
+
203
+ const envelopePath = path.join(phaseDir, 'task-envelope.json');
204
+ writeAtomicJson(envelopePath, envelope);
205
+ console.log(`[task-envelope] initialized: ${envelopePath}`);
206
+ }
207
+
208
+ // Command: update
209
+ function commandUpdate(args) {
210
+ // Parse arguments
211
+ let section = null;
212
+ let phase, planPath, keyDecisions = [], waveCount = null;
213
+
214
+ for (let i = 1; i < args.length; i++) {
215
+ if (args[i] === '--section' && i + 1 < args.length) {
216
+ section = args[++i];
217
+ } else if (args[i] === '--phase' && i + 1 < args.length) {
218
+ phase = args[++i];
219
+ } else if (args[i] === '--plan-path' && i + 1 < args.length) {
220
+ planPath = args[++i];
221
+ } else if (args[i] === '--key-decisions' && i + 1 < args.length) {
222
+ keyDecisions = parseCommaList(args[++i]);
223
+ } else if (args[i] === '--wave-count' && i + 1 < args.length) {
224
+ waveCount = parseInt(args[++i], 10);
225
+ }
226
+ }
227
+
228
+ if (!section) {
229
+ process.stderr.write('[task-envelope] ERROR: --section is required for update\n');
230
+ process.exit(1);
231
+ }
232
+
233
+ if (!phase) {
234
+ process.stderr.write('[task-envelope] ERROR: --phase is required\n');
235
+ process.exit(1);
236
+ }
237
+
238
+ if (section === 'plan' && !planPath) {
239
+ process.stderr.write('[task-envelope] ERROR: --plan-path is required for --section plan\n');
240
+ process.exit(1);
241
+ }
242
+
243
+ // Find phase directory
244
+ const phaseDir = findPhaseDir(phase);
245
+ if (!phaseDir) {
246
+ process.stderr.write(`[task-envelope] ERROR: phase directory not found for "${phase}"\n`);
247
+ process.exit(1);
248
+ }
249
+
250
+ const envelopePath = path.join(phaseDir, 'task-envelope.json');
251
+
252
+ // Read existing envelope or create minimal one
253
+ let envelope;
254
+ if (fs.existsSync(envelopePath)) {
255
+ const content = fs.readFileSync(envelopePath, 'utf8');
256
+ envelope = JSON.parse(content);
257
+ } else {
258
+ // Create minimal envelope structure
259
+ envelope = {
260
+ schema_version: '1',
261
+ phase,
262
+ created_at: new Date().toISOString(),
263
+ risk_level: 'medium'
264
+ };
265
+ }
266
+
267
+ // Update section
268
+ if (section === 'plan') {
269
+ envelope.plan = {
270
+ plan_path: planPath,
271
+ key_decisions: keyDecisions
272
+ };
273
+ if (waveCount !== null && !isNaN(waveCount)) {
274
+ envelope.plan.wave_count = waveCount;
275
+ }
276
+ }
277
+
278
+ // Validate and write
279
+ const validation = validateEnvelope(envelope);
280
+ if (!validation.valid) {
281
+ process.stderr.write(`[task-envelope] ERROR: invalid envelope: ${validation.errors.join('; ')}\n`);
282
+ process.exit(1);
283
+ }
284
+
285
+ writeAtomicJson(envelopePath, envelope);
286
+ console.log(`[task-envelope] updated: ${envelopePath}`);
287
+ }
288
+
289
+ // Command: read
290
+ function commandRead(args) {
291
+ // Parse arguments
292
+ let phase;
293
+
294
+ for (let i = 1; i < args.length; i++) {
295
+ if (args[i] === '--phase' && i + 1 < args.length) {
296
+ phase = args[++i];
297
+ }
298
+ }
299
+
300
+ if (!phase) {
301
+ process.stderr.write('[task-envelope] ERROR: --phase is required\n');
302
+ process.exit(1);
303
+ }
304
+
305
+ const envelopePath = getEnvelopePath(phase);
306
+ if (!envelopePath || !fs.existsSync(envelopePath)) {
307
+ process.stderr.write(`[task-envelope] ERROR: envelope not found for "${phase}"\n`);
308
+ process.exit(1);
309
+ }
310
+
311
+ const content = fs.readFileSync(envelopePath, 'utf8');
312
+ const envelope = JSON.parse(content);
313
+ console.log(JSON.stringify(envelope, null, 2));
314
+ }
315
+
316
+ // Command: validate
317
+ function commandValidate(args) {
318
+ // Parse arguments
319
+ let phase;
320
+
321
+ for (let i = 1; i < args.length; i++) {
322
+ if (args[i] === '--phase' && i + 1 < args.length) {
323
+ phase = args[++i];
324
+ }
325
+ }
326
+
327
+ if (!phase) {
328
+ process.stderr.write('[task-envelope] ERROR: --phase is required\n');
329
+ process.exit(1);
330
+ }
331
+
332
+ const envelopePath = getEnvelopePath(phase);
333
+ if (!envelopePath || !fs.existsSync(envelopePath)) {
334
+ console.log(JSON.stringify({ valid: false, errors: ['envelope not found'] }));
335
+ return;
336
+ }
337
+
338
+ const content = fs.readFileSync(envelopePath, 'utf8');
339
+ let envelope;
340
+ try {
341
+ envelope = JSON.parse(content);
342
+ } catch (e) {
343
+ console.log(JSON.stringify({ valid: false, errors: ['malformed JSON'] }));
344
+ return;
345
+ }
346
+
347
+ const result = validateEnvelope(envelope);
348
+ console.log(JSON.stringify(result));
349
+ }
350
+
351
+ // Exports for testing
352
+ module.exports = {
353
+ validateEnvelope,
354
+ ENVELOPE_SCHEMA
355
+ };
356
+
357
+ // Run main if executed directly
358
+ if (require.main === module) {
359
+ main();
360
+ }