@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,172 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * migrate-formal-dir.cjs
6
+ *
7
+ * Migrates a legacy .formal/ directory at the project root into the canonical
8
+ * .planning/formal/ location. Projects that adopted formal verification before
9
+ * the layout consolidation may still have root-level .formal/ — this script
10
+ * detects, merges, and optionally removes it.
11
+ *
12
+ * Conflict resolution: .planning/formal/ (canonical) always wins. If a file
13
+ * exists in both locations, the legacy version is skipped.
14
+ *
15
+ * Usage:
16
+ * node bin/migrate-formal-dir.cjs [--project-root=PATH] [--json] [--remove-legacy]
17
+ *
18
+ * Exit codes:
19
+ * 0 = success (or nothing to migrate)
20
+ * 1 = could not start (invalid ROOT)
21
+ */
22
+
23
+ const fs = require('fs');
24
+ const path = require('path');
25
+
26
+ // ─── CLI args ────────────────────────────────────────────────────────────────
27
+
28
+ const args = process.argv.slice(2);
29
+
30
+ function getFlag(name) {
31
+ return args.some(a => a === `--${name}`);
32
+ }
33
+
34
+ function getOption(name) {
35
+ const prefix = `--${name}=`;
36
+ const arg = args.find(a => a.startsWith(prefix));
37
+ return arg ? arg.slice(prefix.length) : null;
38
+ }
39
+
40
+ const jsonMode = getFlag('json');
41
+ const removeLegacy = getFlag('remove-legacy');
42
+ const ROOT = getOption('project-root') || process.cwd();
43
+
44
+ const TAG = '[migrate-formal]';
45
+
46
+ // ─── Logging ─────────────────────────────────────────────────────────────────
47
+
48
+ const logs = [];
49
+
50
+ function log(msg) {
51
+ if (!jsonMode) {
52
+ console.log(`${TAG} ${msg}`);
53
+ }
54
+ logs.push(msg);
55
+ }
56
+
57
+ // ─── Validate ROOT ──────────────────────────────────────────────────────────
58
+
59
+ if (!fs.existsSync(ROOT) || !fs.statSync(ROOT).isDirectory()) {
60
+ if (jsonMode) {
61
+ console.log(JSON.stringify({ error: `Invalid project root: ${ROOT}` }));
62
+ } else {
63
+ console.error(`${TAG} ERROR: Invalid project root: ${ROOT}`);
64
+ }
65
+ process.exit(1);
66
+ }
67
+
68
+ // ─── Paths ───────────────────────────────────────────────────────────────────
69
+
70
+ const legacyDir = path.join(ROOT, '.formal');
71
+ const canonDir = path.join(ROOT, '.planning', 'formal');
72
+
73
+ // ─── Detect ──────────────────────────────────────────────────────────────────
74
+
75
+ if (!fs.existsSync(legacyDir) || !fs.statSync(legacyDir).isDirectory()) {
76
+ log('No legacy .formal/ found — nothing to migrate');
77
+ if (jsonMode) {
78
+ console.log(JSON.stringify({
79
+ legacy_found: false,
80
+ copied: 0,
81
+ skipped: 0,
82
+ total: 0,
83
+ removed: false,
84
+ files_copied: [],
85
+ files_skipped: []
86
+ }));
87
+ }
88
+ process.exit(0);
89
+ }
90
+
91
+ // ─── Ensure target ──────────────────────────────────────────────────────────
92
+
93
+ fs.mkdirSync(canonDir, { recursive: true });
94
+
95
+ // ─── Walk helper ─────────────────────────────────────────────────────────────
96
+
97
+ function walkDir(dir) {
98
+ const results = [];
99
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
100
+ const full = path.join(dir, entry.name);
101
+ if (entry.isDirectory()) {
102
+ results.push(...walkDir(full));
103
+ } else if (entry.isFile()) {
104
+ results.push(full);
105
+ }
106
+ }
107
+ return results;
108
+ }
109
+
110
+ // ─── Walk and merge ─────────────────────────────────────────────────────────
111
+
112
+ const allFiles = walkDir(legacyDir);
113
+ const filesCopied = [];
114
+ const filesSkipped = [];
115
+
116
+ for (const srcFile of allFiles) {
117
+ const relPath = path.relative(legacyDir, srcFile);
118
+ const destFile = path.join(canonDir, relPath);
119
+
120
+ try {
121
+ if (fs.existsSync(destFile)) {
122
+ log(`Skipped (already exists in .planning/formal/): ${relPath}`);
123
+ filesSkipped.push(relPath);
124
+ } else {
125
+ // Ensure intermediate directories
126
+ fs.mkdirSync(path.dirname(destFile), { recursive: true });
127
+ fs.copyFileSync(srcFile, destFile);
128
+ log(`Copied: ${relPath}`);
129
+ filesCopied.push(relPath);
130
+ }
131
+ } catch (err) {
132
+ log(`ERROR copying ${relPath}: ${err.message}`);
133
+ // Fail-open: continue with remaining files
134
+ }
135
+ }
136
+
137
+ // ─── Summary ─────────────────────────────────────────────────────────────────
138
+
139
+ const total = allFiles.length;
140
+ log(`Migration complete: ${filesCopied.length} copied, ${filesSkipped.length} skipped (conflicts), ${total} total files in legacy .formal/`);
141
+
142
+ // ─── Optional removal ────────────────────────────────────────────────────────
143
+
144
+ let removed = false;
145
+
146
+ if (removeLegacy && total > 0) {
147
+ try {
148
+ fs.rmSync(legacyDir, { recursive: true, force: true });
149
+ log('Removed legacy .formal/ directory');
150
+ removed = true;
151
+ } catch (err) {
152
+ log(`ERROR removing legacy .formal/: ${err.message}`);
153
+ }
154
+ } else if (!removeLegacy && total > 0) {
155
+ log('Legacy .formal/ preserved — pass --remove-legacy to remove');
156
+ }
157
+
158
+ // ─── JSON output ─────────────────────────────────────────────────────────────
159
+
160
+ if (jsonMode) {
161
+ console.log(JSON.stringify({
162
+ legacy_found: true,
163
+ copied: filesCopied.length,
164
+ skipped: filesSkipped.length,
165
+ total,
166
+ removed,
167
+ files_copied: filesCopied,
168
+ files_skipped: filesSkipped
169
+ }));
170
+ }
171
+
172
+ process.exit(0);
@@ -0,0 +1,206 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * migrate-planning.cjs — Auto-migrate .planning/ from flat layout to v0.27+ hierarchy.
6
+ *
7
+ * Safe to run multiple times (idempotent). Moves files, never deletes.
8
+ * Writes a .planning/.migrated marker on completion.
9
+ *
10
+ * Usage:
11
+ * node bin/migrate-planning.cjs [--dry-run] [--root <dir>]
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+
17
+ function migrate(root, dryRun) {
18
+ const planDir = path.join(root, '.planning');
19
+ if (!fs.existsSync(planDir)) {
20
+ if (!dryRun) console.log('[migrate-planning] No .planning/ directory — nothing to do.');
21
+ return { moved: 0, skipped: 0 };
22
+ }
23
+
24
+ const stats = { moved: 0, skipped: 0, errors: [] };
25
+
26
+ function moveFile(src, dest) {
27
+ if (!fs.existsSync(src)) { stats.skipped++; return; }
28
+ if (fs.existsSync(dest)) { stats.skipped++; return; } // already migrated
29
+ if (dryRun) {
30
+ console.log(` [dry-run] ${path.relative(root, src)} → ${path.relative(root, dest)}`);
31
+ stats.moved++;
32
+ return;
33
+ }
34
+ try {
35
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
36
+ fs.renameSync(src, dest);
37
+ stats.moved++;
38
+ } catch (err) {
39
+ stats.errors.push({ src, dest, error: err.message });
40
+ }
41
+ }
42
+
43
+ const entries = fs.readdirSync(planDir);
44
+
45
+ // ─── 1. Quorum round session logs → quorum/rounds/ ────────────────────────
46
+ for (const e of entries) {
47
+ if (e.startsWith('quorum-rounds-session-') || (e.startsWith('quorum-rounds-') && e.endsWith('.jsonl'))) {
48
+ moveFile(
49
+ path.join(planDir, e),
50
+ path.join(planDir, 'quorum', 'rounds', e)
51
+ );
52
+ }
53
+ }
54
+
55
+ // ─── 2. Quorum slot correlation files → quorum/correlations/ ──────────────
56
+ for (const e of entries) {
57
+ if (e.startsWith('quorum-slot-corr-') && e.endsWith('.json')) {
58
+ moveFile(
59
+ path.join(planDir, e),
60
+ path.join(planDir, 'quorum', 'correlations', e)
61
+ );
62
+ }
63
+ }
64
+
65
+ // ─── 3. Scoreboard + failures → quorum/ ──────────────────────────────────
66
+ moveFile(
67
+ path.join(planDir, 'quorum-scoreboard.json'),
68
+ path.join(planDir, 'quorum', 'scoreboard.json')
69
+ );
70
+ moveFile(
71
+ path.join(planDir, 'quorum-failures.json'),
72
+ path.join(planDir, 'quorum', 'failures.json')
73
+ );
74
+
75
+ // ─── 3b. debates/ → quorum/debates/ ────────────────────────────────────
76
+ const debatesDir = path.join(planDir, 'debates');
77
+ if (fs.existsSync(debatesDir) && fs.statSync(debatesDir).isDirectory()) {
78
+ const debateFiles = fs.readdirSync(debatesDir);
79
+ for (const f of debateFiles) {
80
+ moveFile(
81
+ path.join(debatesDir, f),
82
+ path.join(planDir, 'quorum', 'debates', f)
83
+ );
84
+ }
85
+ // Remove empty debates/ directory
86
+ if (!dryRun) {
87
+ try {
88
+ const remaining = fs.readdirSync(debatesDir);
89
+ if (remaining.length === 0) fs.rmdirSync(debatesDir);
90
+ } catch (_) {}
91
+ }
92
+ }
93
+
94
+ // ─── 4. Telemetry files → telemetry/ ──────────────────────────────────────
95
+ moveFile(
96
+ path.join(planDir, 'conformance-events.jsonl'),
97
+ path.join(planDir, 'telemetry', 'conformance-events.jsonl')
98
+ );
99
+ moveFile(
100
+ path.join(planDir, 'token-usage.jsonl'),
101
+ path.join(planDir, 'telemetry', 'token-usage.jsonl')
102
+ );
103
+
104
+ // ─── 4. Milestone audit/integration files → milestones/ ───────────────────
105
+ for (const e of entries) {
106
+ // v0.X-MILESTONE-AUDIT.md
107
+ const auditMatch = e.match(/^(v[\d.]+)-MILESTONE-AUDIT\.md$/);
108
+ if (auditMatch) {
109
+ // Only move if not already in milestones/
110
+ const dest = path.join(planDir, 'milestones', e);
111
+ moveFile(path.join(planDir, e), dest);
112
+ continue;
113
+ }
114
+
115
+ // v0.X-INTEGRATION-{CHECK,SUMMARY,KEY-FILES,REPORT}.{md,txt}
116
+ const intMatch = e.match(/^(v[\d.]+)-INTEGRATION-/);
117
+ if (intMatch) {
118
+ moveFile(
119
+ path.join(planDir, e),
120
+ path.join(planDir, 'milestones', e)
121
+ );
122
+ continue;
123
+ }
124
+ }
125
+
126
+ // ─── 5. State backups → archive/state-backups/ ────────────────────────────
127
+ for (const e of entries) {
128
+ if (e.startsWith('STATE.md.bak-')) {
129
+ moveFile(
130
+ path.join(planDir, e),
131
+ path.join(planDir, 'archive', 'state-backups', e)
132
+ );
133
+ }
134
+ }
135
+
136
+ // ─── 6. Dated design docs → archive/designs/ ─────────────────────────────
137
+ for (const e of entries) {
138
+ if (/^\d{4}-\d{2}-\d{2}-/.test(e)) {
139
+ moveFile(
140
+ path.join(planDir, e),
141
+ path.join(planDir, 'archive', 'designs', e)
142
+ );
143
+ }
144
+ }
145
+
146
+ // ─── 7. Old roadmap versions → archive/designs/ ──────────────────────────
147
+ for (const e of entries) {
148
+ if (/^ROADMAP-v[\d.]+\.md$/.test(e)) {
149
+ moveFile(
150
+ path.join(planDir, e),
151
+ path.join(planDir, 'archive', 'designs', e)
152
+ );
153
+ }
154
+ }
155
+
156
+ // ─── 8. Write .planning/.gitignore ─────────────────────────────────────────
157
+ if (!dryRun) {
158
+ const gitignorePath = path.join(planDir, '.gitignore');
159
+ const gitignoreContent = [
160
+ '# Auto-generated by migrate-planning.cjs',
161
+ '# Ephemeral quorum runtime data',
162
+ 'quorum/rounds/',
163
+ 'quorum/correlations/',
164
+ 'quorum/scoreboard.json',
165
+ 'quorum/failures.json',
166
+ '',
167
+ '# Telemetry (append-only logs)',
168
+ 'telemetry/conformance-events.jsonl',
169
+ 'telemetry/token-usage.jsonl',
170
+ '',
171
+ '# State backups',
172
+ 'archive/state-backups/',
173
+ '',
174
+ ].join('\n');
175
+ fs.writeFileSync(gitignorePath, gitignoreContent, 'utf8');
176
+ }
177
+
178
+ // ─── 9. Write migration marker ────────────────────────────────────────────
179
+ if (!dryRun && stats.moved > 0) {
180
+ const markerPath = path.join(planDir, '.migrated');
181
+ fs.writeFileSync(markerPath, JSON.stringify({
182
+ version: '0.27',
183
+ migratedAt: new Date().toISOString(),
184
+ filesMoved: stats.moved,
185
+ }, null, 2), 'utf8');
186
+ }
187
+
188
+ return stats;
189
+ }
190
+
191
+ // ─── CLI entry ───────────────────────────────────────────────────────────────
192
+ if (require.main === module) {
193
+ const args = process.argv.slice(2);
194
+ const dryRun = args.includes('--dry-run');
195
+ const rootIdx = args.indexOf('--root');
196
+ const root = rootIdx >= 0 ? args[rootIdx + 1] : process.cwd();
197
+
198
+ console.log(`[migrate-planning] ${dryRun ? 'DRY RUN — ' : ''}Migrating ${root}/.planning/`);
199
+ const stats = migrate(root, dryRun);
200
+ console.log(`[migrate-planning] Done: ${stats.moved} moved, ${stats.skipped} skipped`);
201
+ if (stats.errors && stats.errors.length) {
202
+ console.error(`[migrate-planning] Errors:`, stats.errors);
203
+ }
204
+ }
205
+
206
+ module.exports = { migrate };
@@ -0,0 +1,255 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const os = require('os');
7
+
8
+ // SLOT_MIGRATION_MAP: old model-based names → new slot names
9
+ const SLOT_MIGRATION_MAP = {
10
+ 'codex-cli': 'codex-cli-1',
11
+ 'gemini-cli': 'gemini-cli-1',
12
+ 'opencode': 'opencode-1',
13
+ 'copilot-cli': 'copilot-1',
14
+ 'claude-deepseek': 'claude-1',
15
+ 'claude-minimax': 'claude-2',
16
+ 'claude-qwen-coder': 'claude-3',
17
+ 'claude-kimi': 'claude-4',
18
+ 'claude-llama4': 'claude-5',
19
+ 'claude-glm': 'claude-6',
20
+ };
21
+
22
+ /**
23
+ * Migrate ~/.claude.json mcpServers keys from model-based names to slot names.
24
+ * @param {string} claudeJsonPath - Absolute path to ~/.claude.json
25
+ * @param {boolean} dryRun - If true, do not write changes
26
+ * @returns {{ changed: number, renamed: Array<{from: string, to: string}> }}
27
+ */
28
+ function migrateClaudeJson(claudeJsonPath, dryRun = false) {
29
+ let raw;
30
+ try {
31
+ raw = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8'));
32
+ } catch (e) {
33
+ if (e.code === 'ENOENT') {
34
+ return { changed: 0, renamed: [] };
35
+ }
36
+ throw new Error(`Failed to read ${claudeJsonPath}: ${e.message}`);
37
+ }
38
+
39
+ const servers = raw.mcpServers || {};
40
+ let changed = 0;
41
+ const renamed = [];
42
+
43
+ for (const [oldName, newName] of Object.entries(SLOT_MIGRATION_MAP)) {
44
+ if (servers[oldName] !== undefined && servers[newName] === undefined) {
45
+ // Rename: assign to new key, delete old key
46
+ servers[newName] = servers[oldName];
47
+ delete servers[oldName];
48
+ changed++;
49
+ renamed.push({ from: oldName, to: newName });
50
+ }
51
+ // oldName absent + newName present → already migrated (skip, idempotent)
52
+ // both present → skip (safety — don't overwrite)
53
+ }
54
+
55
+ if (changed > 0) {
56
+ raw.mcpServers = servers;
57
+ if (!dryRun) {
58
+ fs.writeFileSync(claudeJsonPath, JSON.stringify(raw, null, 2) + '\n', 'utf8');
59
+ }
60
+ }
61
+
62
+ return { changed, renamed };
63
+ }
64
+
65
+ // tool_prefix migration map for qgsd.json
66
+ const QGSD_PREFIX_MAP = {
67
+ 'mcp__codex-cli__': 'mcp__codex-cli-1__',
68
+ 'mcp__gemini-cli__': 'mcp__gemini-cli-1__',
69
+ 'mcp__opencode__': 'mcp__opencode-1__',
70
+ 'mcp__copilot-cli__': 'mcp__copilot-1__',
71
+ };
72
+
73
+ /**
74
+ * Migrate ~/.claude/qgsd.json required_models tool_prefix values to slot-based prefixes.
75
+ * @param {string} qgsdJsonPath - Absolute path to ~/.claude/qgsd.json
76
+ * @param {boolean} dryRun - If true, do not write changes
77
+ * @returns {{ changed: number, patched: Array<{key: string, from: string, to: string}> }}
78
+ */
79
+ function migrateQgsdJson(qgsdJsonPath, dryRun = false) {
80
+ if (!fs.existsSync(qgsdJsonPath)) {
81
+ return { changed: 0, patched: [] };
82
+ }
83
+
84
+ let raw;
85
+ try {
86
+ raw = JSON.parse(fs.readFileSync(qgsdJsonPath, 'utf8'));
87
+ } catch (e) {
88
+ throw new Error(`Failed to read ${qgsdJsonPath}: ${e.message}`);
89
+ }
90
+
91
+ const requiredModels = raw.required_models;
92
+ if (!requiredModels || typeof requiredModels !== 'object') {
93
+ return { changed: 0, patched: [] };
94
+ }
95
+
96
+ let changed = 0;
97
+ const patched = [];
98
+
99
+ for (const [modelKey, modelDef] of Object.entries(requiredModels)) {
100
+ if (modelDef && typeof modelDef.tool_prefix === 'string') {
101
+ const newPrefix = QGSD_PREFIX_MAP[modelDef.tool_prefix];
102
+ if (newPrefix) {
103
+ patched.push({ key: modelKey, from: modelDef.tool_prefix, to: newPrefix });
104
+ modelDef.tool_prefix = newPrefix;
105
+ changed++;
106
+ }
107
+ }
108
+ }
109
+
110
+ if (changed > 0 && !dryRun) {
111
+ fs.writeFileSync(qgsdJsonPath, JSON.stringify(raw, null, 2) + '\n', 'utf8');
112
+ }
113
+
114
+ return { changed, patched };
115
+ }
116
+
117
+ /**
118
+ * Populate quorum_active in ~/.claude/qgsd.json from current mcpServers in ~/.claude.json.
119
+ * Idempotent: skips if quorum_active already present and non-empty.
120
+ * @param {string} qgsdJsonPath - Absolute path to ~/.claude/qgsd.json
121
+ * @param {string} claudeJsonPath - Absolute path to ~/.claude.json
122
+ * @param {boolean} dryRun - If true, do not write changes
123
+ * @returns {{ skipped: boolean, slots: string[] }}
124
+ */
125
+ function populateActiveSlots(qgsdJsonPath, claudeJsonPath, dryRun = false) {
126
+ // Read current slot names from ~/.claude.json
127
+ let slotNames = [];
128
+ try {
129
+ const raw = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8'));
130
+ slotNames = Object.keys(raw.mcpServers || {});
131
+ } catch (e) {
132
+ if (e.code !== 'ENOENT') throw new Error(`Failed to read ${claudeJsonPath}: ${e.message}`);
133
+ // ~/.claude.json absent — nothing to populate
134
+ return { skipped: true, slots: [] };
135
+ }
136
+
137
+ // Read or create qgsd.json
138
+ let qgsdConfig = {};
139
+ try {
140
+ qgsdConfig = JSON.parse(fs.readFileSync(qgsdJsonPath, 'utf8'));
141
+ } catch (e) {
142
+ if (e.code !== 'ENOENT') throw new Error(`Failed to read ${qgsdJsonPath}: ${e.message}`);
143
+ // qgsd.json absent — create minimal object
144
+ }
145
+
146
+ // Idempotent: skip if already set and non-empty
147
+ if (Array.isArray(qgsdConfig.quorum_active) && qgsdConfig.quorum_active.length > 0) {
148
+ return { skipped: true, slots: qgsdConfig.quorum_active };
149
+ }
150
+
151
+ // Populate and write
152
+ qgsdConfig.quorum_active = slotNames;
153
+ if (!dryRun) {
154
+ fs.mkdirSync(path.dirname(qgsdJsonPath), { recursive: true });
155
+ fs.writeFileSync(qgsdJsonPath, JSON.stringify(qgsdConfig, null, 2) + '\n', 'utf8');
156
+ }
157
+ return { skipped: false, slots: slotNames };
158
+ }
159
+
160
+ // CLI entrypoint
161
+ if (require.main === module) {
162
+ const dryRun = process.argv.includes('--dry-run');
163
+ const claudeJsonPath = path.join(os.homedir(), '.claude.json');
164
+ const qgsdJsonPath = path.join(os.homedir(), '.claude', 'qgsd.json');
165
+
166
+ let r1, r2;
167
+ try {
168
+ r1 = migrateClaudeJson(claudeJsonPath, dryRun);
169
+ } catch (e) {
170
+ console.error(`Error migrating ~/.claude.json: ${e.message}`);
171
+ process.exit(1);
172
+ }
173
+
174
+ try {
175
+ r2 = migrateQgsdJson(qgsdJsonPath, dryRun);
176
+ } catch (e) {
177
+ console.error(`Error migrating ~/.claude/qgsd.json: ${e.message}`);
178
+ process.exit(1);
179
+ }
180
+
181
+ let r3;
182
+ try {
183
+ r3 = populateActiveSlots(qgsdJsonPath, claudeJsonPath, dryRun);
184
+ if (!r3.skipped) {
185
+ console.log(`[migrate-to-slots] quorum_active populated: ${r3.slots.join(', ')}`);
186
+ } else {
187
+ console.log(`[migrate-to-slots] quorum_active already set (${r3.slots.length} slots) — skipped`);
188
+ }
189
+ } catch (e) {
190
+ console.error(`Error populating quorum_active: ${e.message}`);
191
+ process.exit(1);
192
+ }
193
+
194
+ if (r1.changed === 0 && r2.changed === 0) {
195
+ console.log('Already migrated — no changes needed');
196
+ } else if (dryRun) {
197
+ const totalRenames = r1.renamed.length + r2.patched.length;
198
+ console.log(`[DRY RUN] Would rename ${totalRenames} entries:`);
199
+ for (const { from, to } of r1.renamed) {
200
+ console.log(` mcpServers: ${from} → ${to}`);
201
+ }
202
+ for (const { key, from, to } of r2.patched) {
203
+ console.log(` qgsd.json required_models.${key}.tool_prefix: ${from} → ${to}`);
204
+ }
205
+ } else {
206
+ if (r1.changed > 0) {
207
+ console.log(`Migrated ${r1.changed} mcpServers entries:`);
208
+ for (const { from, to } of r1.renamed) {
209
+ console.log(` ${from} → ${to}`);
210
+ }
211
+ }
212
+ if (r2.changed > 0) {
213
+ console.log(`Patched ${r2.changed} qgsd.json tool_prefix values`);
214
+ for (const { key, from, to } of r2.patched) {
215
+ console.log(` required_models.${key}.tool_prefix: ${from} → ${to}`);
216
+ }
217
+ }
218
+ }
219
+
220
+ process.exit(0);
221
+ }
222
+
223
+ /**
224
+ * Append a single slot name to quorum_active in qgsd.json if not already present.
225
+ * Idempotent: no-op if slot is already in the array.
226
+ * @param {string} slotName - e.g. "copilot-2"
227
+ * @param {string} qgsdJsonPath - path to ~/.claude/qgsd.json
228
+ * @param {boolean} dryRun - if true, report but do not write
229
+ * @returns {{ added: boolean, slot: string, skipped: boolean }}
230
+ */
231
+ function addSlotToQuorumActive(slotName, qgsdJsonPath, dryRun = false) {
232
+ let qgsdConfig = {};
233
+ try {
234
+ if (fs.existsSync(qgsdJsonPath)) {
235
+ qgsdConfig = JSON.parse(fs.readFileSync(qgsdJsonPath, 'utf8'));
236
+ }
237
+ } catch (e) {
238
+ return { added: false, slot: slotName, skipped: true, error: e.message };
239
+ }
240
+
241
+ const active = Array.isArray(qgsdConfig.quorum_active) ? qgsdConfig.quorum_active : [];
242
+ if (active.includes(slotName)) {
243
+ return { added: false, slot: slotName, skipped: true, reason: 'already present' };
244
+ }
245
+
246
+ if (dryRun) {
247
+ return { added: true, slot: slotName, skipped: false, dryRun: true };
248
+ }
249
+
250
+ qgsdConfig.quorum_active = [...active, slotName];
251
+ fs.writeFileSync(qgsdJsonPath, JSON.stringify(qgsdConfig, null, 2) + '\n');
252
+ return { added: true, slot: slotName, skipped: false };
253
+ }
254
+
255
+ module.exports = { migrateClaudeJson, migrateQgsdJson, populateActiveSlots, addSlotToQuorumActive, SLOT_MIGRATION_MAP };