ultra-builder-pro-cli 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/.ultra-template/delivery-report.json +9 -0
  2. package/.ultra-template/docs/research/README.md +53 -0
  3. package/.ultra-template/specs/architecture.md +250 -0
  4. package/.ultra-template/specs/discovery.md +248 -0
  5. package/.ultra-template/specs/product.md +223 -0
  6. package/.ultra-template/tasks/contexts/TEMPLATE.md +57 -0
  7. package/.ultra-template/tasks/tasks.json +33 -0
  8. package/.ultra-template/test-report.json +8 -0
  9. package/CHANGELOG.md +194 -0
  10. package/LICENSE +21 -0
  11. package/README.md +187 -0
  12. package/adapters/_shared/codex-assets.cjs +1128 -0
  13. package/adapters/_shared/codex-ultra-tools-entry.cjs +11 -0
  14. package/adapters/_shared/file-ops.cjs +115 -0
  15. package/adapters/_shared/frontmatter.cjs +52 -0
  16. package/adapters/_shared/handbook.cjs +144 -0
  17. package/adapters/_shared/md-to-toml.cjs +54 -0
  18. package/adapters/_shared/path-rewrite.cjs +79 -0
  19. package/adapters/_shared/rtk-detect.cjs +86 -0
  20. package/adapters/_shared/runtime-assets.cjs +70 -0
  21. package/adapters/_shared/settings-merge.cjs +76 -0
  22. package/adapters/_shared/tests/file-ops.test.cjs +54 -0
  23. package/adapters/_shared/tests/frontmatter.test.cjs +47 -0
  24. package/adapters/_shared/tests/handbook.test.cjs +86 -0
  25. package/adapters/_shared/tests/md-to-toml.test.cjs +48 -0
  26. package/adapters/_shared/tests/path-rewrite.test.cjs +60 -0
  27. package/adapters/_shared/tests/resolve-target.test.cjs +85 -0
  28. package/adapters/_shared/tests/rtk-detect.test.cjs +116 -0
  29. package/adapters/_shared/tests/runtime-assets.test.cjs +107 -0
  30. package/adapters/_shared/tests/settings-merge.test.cjs +85 -0
  31. package/adapters/_shared/tests/validate.test.cjs +30 -0
  32. package/adapters/_shared/validate.cjs +24 -0
  33. package/adapters/claude.js +182 -0
  34. package/adapters/codex.js +385 -0
  35. package/adapters/gemini.js +204 -0
  36. package/adapters/opencode.js +506 -0
  37. package/adapters/tests/claude.test.cjs +117 -0
  38. package/adapters/tests/codex-hook.test.cjs +90 -0
  39. package/adapters/tests/codex.test.cjs +356 -0
  40. package/adapters/tests/gemini.test.cjs +112 -0
  41. package/adapters/tests/opencode.test.cjs +246 -0
  42. package/agents/code-reviewer.md +102 -0
  43. package/agents/debugger.md +127 -0
  44. package/agents/review-code.md +125 -0
  45. package/agents/review-comments.md +98 -0
  46. package/agents/review-coordinator.md +146 -0
  47. package/agents/review-design.md +133 -0
  48. package/agents/review-errors.md +87 -0
  49. package/agents/review-tests.md +93 -0
  50. package/agents/tdd-runner.md +92 -0
  51. package/bin/handbook.js +54 -0
  52. package/bin/install.js +254 -0
  53. package/bin/orchestrator.js +214 -0
  54. package/commands/learn.md +41 -0
  55. package/commands/ultra-deliver.md +45 -0
  56. package/commands/ultra-dev.md +50 -0
  57. package/commands/ultra-init.md +51 -0
  58. package/commands/ultra-plan.md +49 -0
  59. package/commands/ultra-research.md +59 -0
  60. package/commands/ultra-status.md +46 -0
  61. package/commands/ultra-test.md +48 -0
  62. package/commands/ultra-think.md +45 -0
  63. package/docs/AGENT-CONTEXT.md +143 -0
  64. package/docs/ARCHITECTURE.md +197 -0
  65. package/docs/COMMIT-HASH-BACKFILL.md +101 -0
  66. package/docs/LEGACY-HERMES.md +1175 -0
  67. package/docs/PLAN.zh-CN.md +1683 -0
  68. package/docs/ROADMAP.md +102 -0
  69. package/docs/RUNTIME-COMPAT-MATRIX.md +98 -0
  70. package/docs/STATE-DB-ACCESS-POLICY.md +109 -0
  71. package/docs/USER-HANDBOOK-CONTRACT.md +61 -0
  72. package/hooks/README.md +24 -0
  73. package/hooks/active_task_context.py +44 -0
  74. package/hooks/adapters/codex.py +302 -0
  75. package/hooks/health_check.py +61 -0
  76. package/hooks/pre_stop_check.py +56 -0
  77. package/hooks/subagent_tracker.py +116 -0
  78. package/hooks/workflow_checkpoint.py +56 -0
  79. package/hooks/workflow_context.py +63 -0
  80. package/hooks/workflow_resume.py +50 -0
  81. package/mcp-server/lib/init-project.cjs +162 -0
  82. package/mcp-server/lib/llm-client.cjs +157 -0
  83. package/mcp-server/lib/llm-client.test.cjs +156 -0
  84. package/mcp-server/lib/plan-store.cjs +96 -0
  85. package/mcp-server/lib/plan-store.test.cjs +132 -0
  86. package/mcp-server/lib/prd-parser.cjs +110 -0
  87. package/mcp-server/lib/prd-parser.test.cjs +171 -0
  88. package/mcp-server/lib/pricing.cjs +53 -0
  89. package/mcp-server/lib/projector.cjs +158 -0
  90. package/mcp-server/lib/state-db.cjs +113 -0
  91. package/mcp-server/lib/state-db.test.cjs +116 -0
  92. package/mcp-server/lib/state-ops.cjs +725 -0
  93. package/mcp-server/lib/task-expander.cjs +123 -0
  94. package/mcp-server/lib/task-expander.test.cjs +253 -0
  95. package/mcp-server/lib/telemetry.cjs +72 -0
  96. package/mcp-server/lib/topo.cjs +131 -0
  97. package/mcp-server/lib/topo.test.cjs +158 -0
  98. package/mcp-server/server.cjs +482 -0
  99. package/mcp-server/tests/circuit-breaker.test.cjs +125 -0
  100. package/mcp-server/tests/commit-hash-flow.test.cjs +117 -0
  101. package/mcp-server/tests/concurrency-update.test.cjs +96 -0
  102. package/mcp-server/tests/concurrency.test.cjs +112 -0
  103. package/mcp-server/tests/db-ops.test.cjs +117 -0
  104. package/mcp-server/tests/init-project.test.cjs +141 -0
  105. package/mcp-server/tests/migrate.test.cjs +144 -0
  106. package/mcp-server/tests/projector.test.cjs +130 -0
  107. package/mcp-server/tests/server-contract.test.cjs +750 -0
  108. package/mcp-server/tests/staleness.test.cjs +190 -0
  109. package/mcp-server/tests/state-ops.test.cjs +286 -0
  110. package/mcp-server/tests/tagged-tasks.test.cjs +163 -0
  111. package/mcp-server/tests/telemetry.test.cjs +205 -0
  112. package/orchestrator/auto-merge.cjs +109 -0
  113. package/orchestrator/code-graph-watcher.cjs +121 -0
  114. package/orchestrator/daemon.cjs +112 -0
  115. package/orchestrator/dispatch-rules.cjs +93 -0
  116. package/orchestrator/parallel-orchestrator.cjs +195 -0
  117. package/orchestrator/planner/expand-prompt.cjs +62 -0
  118. package/orchestrator/planner/expand-prompt.test.cjs +52 -0
  119. package/orchestrator/planner/plan-builder.cjs +116 -0
  120. package/orchestrator/planner/plan-builder.test.cjs +165 -0
  121. package/orchestrator/planner/prd-prompt.cjs +44 -0
  122. package/orchestrator/planner/prd-prompt.test.cjs +30 -0
  123. package/orchestrator/recovery.cjs +70 -0
  124. package/orchestrator/session-runner.cjs +224 -0
  125. package/orchestrator/skill-miner.cjs +133 -0
  126. package/orchestrator/tests/auto-merge.test.cjs +216 -0
  127. package/orchestrator/tests/code-graph-watcher.test.cjs +183 -0
  128. package/orchestrator/tests/daemon.test.cjs +260 -0
  129. package/orchestrator/tests/dispatch-rules.test.cjs +122 -0
  130. package/orchestrator/tests/parallel-orchestrator.test.cjs +233 -0
  131. package/orchestrator/tests/phase-8b-gate.test.cjs +183 -0
  132. package/orchestrator/tests/recovery.test.cjs +190 -0
  133. package/orchestrator/tests/session-runner.test.cjs +314 -0
  134. package/orchestrator/tests/skill-miner.test.cjs +147 -0
  135. package/orchestrator/tests/worktree-manager.test.cjs +148 -0
  136. package/orchestrator/worktree-manager.cjs +131 -0
  137. package/output-styles/coding-vibes.md +67 -0
  138. package/output-styles/structural-thinking.md +61 -0
  139. package/package.json +109 -0
  140. package/settings.json +8 -0
  141. package/skills/cc-collab/SKILL.md +41 -0
  142. package/skills/code-review-expert/SKILL.md +118 -0
  143. package/skills/code-review-expert/references/code-quality-checklist.md +112 -0
  144. package/skills/code-review-expert/references/integration-checklist.md +46 -0
  145. package/skills/code-review-expert/references/removal-plan.md +52 -0
  146. package/skills/code-review-expert/references/security-checklist.md +100 -0
  147. package/skills/code-review-expert/references/solid-checklist.md +65 -0
  148. package/skills/codex-collab/SKILL.md +52 -0
  149. package/skills/codex-collab/evals/evals.json +40 -0
  150. package/skills/gemini-collab/SKILL.md +51 -0
  151. package/skills/gemini-collab/evals/evals.json +42 -0
  152. package/skills/integration-rules/SKILL.md +71 -0
  153. package/skills/learn/SKILL.md +117 -0
  154. package/skills/security-rules/SKILL.md +60 -0
  155. package/skills/testing-rules/SKILL.md +56 -0
  156. package/skills/ultra-deliver/SKILL.md +171 -0
  157. package/skills/ultra-dev/SKILL.md +324 -0
  158. package/skills/ultra-init/SKILL.md +148 -0
  159. package/skills/ultra-plan/SKILL.md +314 -0
  160. package/skills/ultra-research/SKILL.md +156 -0
  161. package/skills/ultra-research/steps/step-00-problem-validation.md +154 -0
  162. package/skills/ultra-research/steps/step-01-opportunity-discovery.md +172 -0
  163. package/skills/ultra-research/steps/step-02-market-assessment.md +195 -0
  164. package/skills/ultra-research/steps/step-03-competitive-landscape.md +197 -0
  165. package/skills/ultra-research/steps/step-04-product-strategy.md +196 -0
  166. package/skills/ultra-research/steps/step-05-assumptions-validation.md +234 -0
  167. package/skills/ultra-research/steps/step-10-user-personas.md +169 -0
  168. package/skills/ultra-research/steps/step-11-user-scenarios.md +181 -0
  169. package/skills/ultra-research/steps/step-20-user-stories.md +156 -0
  170. package/skills/ultra-research/steps/step-21-features-scope.md +157 -0
  171. package/skills/ultra-research/steps/step-22-success-metrics.md +195 -0
  172. package/skills/ultra-research/steps/step-30-architecture-context.md +154 -0
  173. package/skills/ultra-research/steps/step-31-solution-strategy.md +172 -0
  174. package/skills/ultra-research/steps/step-32-building-blocks.md +188 -0
  175. package/skills/ultra-research/steps/step-40-deployment.md +123 -0
  176. package/skills/ultra-research/steps/step-41-quality-risks.md +135 -0
  177. package/skills/ultra-research/steps/step-99-synthesis.md +188 -0
  178. package/skills/ultra-review/SKILL.md +437 -0
  179. package/skills/ultra-review/references/unified-schema.md +127 -0
  180. package/skills/ultra-review/scripts/review_verdict_update.py +99 -0
  181. package/skills/ultra-review/scripts/review_wait.py +120 -0
  182. package/skills/ultra-status/SKILL.md +162 -0
  183. package/skills/ultra-test/SKILL.md +195 -0
  184. package/skills/ultra-think/SKILL.md +109 -0
  185. package/skills/ultra-verify/SKILL.md +111 -0
  186. package/skills/ultra-verify/evals/evals.json +51 -0
  187. package/skills/ultra-verify/references/confidence-system.md +38 -0
  188. package/skills/ultra-verify/references/cross-verify-modes.md +73 -0
  189. package/skills/ultra-verify/scripts/verify_wait.py +131 -0
  190. package/spec/cli-protocol.md +118 -0
  191. package/spec/mcp-tools.yaml +658 -0
  192. package/spec/schemas/command-manifest.schema.json +47 -0
  193. package/spec/schemas/context-file.v4.5.schema.json +54 -0
  194. package/spec/schemas/mcp-tools.schema.json +136 -0
  195. package/spec/schemas/skill-manifest.schema.json +94 -0
  196. package/spec/schemas/state-db.sql +154 -0
  197. package/spec/schemas/tasks.v4.5.schema.json +66 -0
  198. package/ultra-tools/cli.cjs +121 -0
  199. package/ultra-tools/commands/db.cjs +175 -0
  200. package/ultra-tools/commands/legacy-memory.cjs +159 -0
  201. package/ultra-tools/commands/migrate.cjs +354 -0
  202. package/ultra-tools/commands/session.cjs +180 -0
  203. package/ultra-tools/commands/status.cjs +158 -0
  204. package/ultra-tools/commands/task.cjs +95 -0
  205. package/ultra-tools/legacy-memory.test.cjs +80 -0
  206. package/ultra-tools/status-command.test.cjs +132 -0
@@ -0,0 +1,175 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const { initStateDb, openStateDb, closeStateDb } = require('../../mcp-server/lib/state-db.cjs');
7
+
8
+ const DEFAULT_DB_PATH = path.join('.ultra', 'state.db');
9
+ const DEFAULT_BACKUP_DIR = path.join('.ultra', 'backups');
10
+
11
+ function parseFlags(args) {
12
+ const flags = { _: [] };
13
+ for (let i = 0; i < args.length; i++) {
14
+ const a = args[i];
15
+ if (a === '--path') {
16
+ flags.path = args[++i];
17
+ } else if (a === '--to') {
18
+ flags.to = args[++i];
19
+ } else if (a === '--help' || a === '-h') {
20
+ flags.help = true;
21
+ } else {
22
+ flags._.push(a);
23
+ }
24
+ }
25
+ return flags;
26
+ }
27
+
28
+ function emit(envelope) {
29
+ process.stdout.write(`${JSON.stringify(envelope)}\n`);
30
+ }
31
+
32
+ function dispatch(args) {
33
+ const [verb, ...rest] = args;
34
+ const flags = parseFlags(rest);
35
+
36
+ if (!verb || flags.help) {
37
+ process.stdout.write(USAGE);
38
+ return 0;
39
+ }
40
+
41
+ switch (verb) {
42
+ case 'init': return cmdInit(flags);
43
+ case 'checkpoint': return cmdCheckpoint(flags);
44
+ case 'vacuum': return cmdVacuum(flags);
45
+ case 'integrity': return cmdIntegrity(flags);
46
+ case 'backup': return cmdBackup(flags);
47
+ default:
48
+ emit({ ok: false, error: { code: 'UNKNOWN_VERB', message: `unknown db verb '${verb}'; supported: init, checkpoint, vacuum, integrity, backup` } });
49
+ return 1;
50
+ }
51
+ }
52
+
53
+ function cmdInit(flags) {
54
+ const dbPath = path.resolve(flags.path || DEFAULT_DB_PATH);
55
+ try {
56
+ const { db, schema_version, created, tables, path: actualPath } = initStateDb(dbPath);
57
+ closeStateDb(db);
58
+ emit({
59
+ ok: true,
60
+ data: {
61
+ path: actualPath,
62
+ schema_version,
63
+ created,
64
+ tables,
65
+ size_bytes: fs.statSync(actualPath).size,
66
+ },
67
+ });
68
+ return 0;
69
+ } catch (err) {
70
+ emit({
71
+ ok: false,
72
+ error: {
73
+ code: 'INIT_FAILED',
74
+ message: err.message,
75
+ retriable: false,
76
+ },
77
+ });
78
+ return 2;
79
+ }
80
+ }
81
+
82
+ function withOpenDb(flags, fn, errorCode) {
83
+ const dbPath = path.resolve(flags.path || DEFAULT_DB_PATH);
84
+ if (!fs.existsSync(dbPath)) {
85
+ emit({ ok: false, error: { code: 'DB_NOT_FOUND', message: `state.db missing at ${dbPath}; run 'db init' first` } });
86
+ return 2;
87
+ }
88
+ let db;
89
+ try {
90
+ db = openStateDb(dbPath);
91
+ const data = fn(db, dbPath);
92
+ emit({ ok: true, data: { path: dbPath, ...data } });
93
+ return 0;
94
+ } catch (err) {
95
+ emit({ ok: false, error: { code: errorCode, message: err.message, retriable: false } });
96
+ return 2;
97
+ } finally {
98
+ if (db) closeStateDb(db);
99
+ }
100
+ }
101
+
102
+ function cmdCheckpoint(flags) {
103
+ return withOpenDb(flags, (db) => {
104
+ const result = db.pragma('wal_checkpoint(TRUNCATE)');
105
+ // better-sqlite3 returns [{ busy, log, checkpointed }]
106
+ const row = Array.isArray(result) && result[0] ? result[0] : { busy: 0, log: 0, checkpointed: 0 };
107
+ return { busy: row.busy, log: row.log, checkpointed: row.checkpointed };
108
+ }, 'CHECKPOINT_FAILED');
109
+ }
110
+
111
+ function cmdVacuum(flags) {
112
+ return withOpenDb(flags, (db) => {
113
+ const before = db.prepare('SELECT page_count * page_size AS bytes FROM pragma_page_count(), pragma_page_size()').get();
114
+ db.prepare('VACUUM').run();
115
+ const after = db.prepare('SELECT page_count * page_size AS bytes FROM pragma_page_count(), pragma_page_size()').get();
116
+ return { bytes_before: before.bytes, bytes_after: after.bytes, reclaimed_bytes: before.bytes - after.bytes };
117
+ }, 'VACUUM_FAILED');
118
+ }
119
+
120
+ function cmdIntegrity(flags) {
121
+ return withOpenDb(flags, (db) => {
122
+ const result = db.pragma('integrity_check');
123
+ const messages = result.map((r) => r.integrity_check);
124
+ const ok = messages.length === 1 && messages[0] === 'ok';
125
+ return { integrity_ok: ok, messages };
126
+ }, 'INTEGRITY_FAILED');
127
+ }
128
+
129
+ function defaultBackupPath() {
130
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
131
+ return path.join(DEFAULT_BACKUP_DIR, `state-db-${ts}.db`);
132
+ }
133
+
134
+ function cmdBackup(flags) {
135
+ const dbPath = path.resolve(flags.path || DEFAULT_DB_PATH);
136
+ const targetPath = path.resolve(flags.to || defaultBackupPath());
137
+ if (!fs.existsSync(dbPath)) {
138
+ emit({ ok: false, error: { code: 'DB_NOT_FOUND', message: `state.db missing at ${dbPath}` } });
139
+ return 2;
140
+ }
141
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
142
+ let db;
143
+ try {
144
+ // Flush WAL into the main file so a plain copy captures all committed
145
+ // writes; fall back to copy-only on a fresh file with no WAL.
146
+ db = openStateDb(dbPath);
147
+ db.pragma('wal_checkpoint(TRUNCATE)');
148
+ closeStateDb(db);
149
+ db = null;
150
+ fs.copyFileSync(dbPath, targetPath);
151
+ const size = fs.statSync(targetPath).size;
152
+ emit({ ok: true, data: { source: dbPath, target: targetPath, size_bytes: size } });
153
+ return 0;
154
+ } catch (err) {
155
+ emit({ ok: false, error: { code: 'BACKUP_FAILED', message: err.message, retriable: true } });
156
+ return 2;
157
+ } finally {
158
+ if (db) closeStateDb(db);
159
+ }
160
+ }
161
+
162
+ const USAGE = `ultra-tools db <verb> [flags]
163
+
164
+ Verbs (Phase 2):
165
+ init [--path <db>] create or open state.db, apply schema
166
+ checkpoint [--path <db>] run PRAGMA wal_checkpoint(TRUNCATE)
167
+ vacuum [--path <db>] run VACUUM and report reclaimed bytes
168
+ integrity [--path <db>] run PRAGMA integrity_check
169
+ backup [--path <db>] [--to <out>] online .backup() to a file
170
+
171
+ Default db: .ultra/state.db
172
+ Default backup: .ultra/backups/state-db-{iso}.db
173
+ `;
174
+
175
+ module.exports = { dispatch, USAGE, DEFAULT_DB_PATH, DEFAULT_BACKUP_DIR };
@@ -0,0 +1,159 @@
1
+ 'use strict';
2
+
3
+ /** Explicit migration boundary for retired Ultra memory stores. */
4
+
5
+ const fs = require('node:fs');
6
+ const path = require('node:path');
7
+ const Database = require('better-sqlite3');
8
+
9
+ const CONFIRMATION = 'DELETE_ULTRA_LEGACY_MEMORY';
10
+
11
+ function tableExists(db, name) {
12
+ return !!db.prepare("SELECT 1 FROM sqlite_master WHERE type IN ('table', 'view') AND name = ?").get(name);
13
+ }
14
+
15
+ function inspectLegacy(sourceDir = '.') {
16
+ const root = path.resolve(sourceDir);
17
+ const ultra = path.join(root, '.ultra');
18
+ const hookMemory = path.join(ultra, 'memory');
19
+ const dbPath = path.join(ultra, 'state.db');
20
+ let stateMemoryTable = false;
21
+ let stateMemoryEntries = 0;
22
+ if (fs.existsSync(dbPath)) {
23
+ const db = new Database(dbPath, { readonly: true, fileMustExist: true });
24
+ try {
25
+ stateMemoryTable = tableExists(db, 'memory_entries');
26
+ if (stateMemoryTable) {
27
+ stateMemoryEntries = db.prepare('SELECT COUNT(*) AS count FROM memory_entries').get().count;
28
+ }
29
+ } finally {
30
+ db.close();
31
+ }
32
+ }
33
+ return {
34
+ source_dir: root,
35
+ hook_memory_dir: fs.existsSync(hookMemory),
36
+ hook_memory_path: hookMemory,
37
+ state_db_path: dbPath,
38
+ state_memory_table: stateMemoryTable,
39
+ state_memory_entries: stateMemoryEntries,
40
+ };
41
+ }
42
+
43
+ function archiveLegacy(sourceDir = '.', { timestamp } = {}) {
44
+ const status = inspectLegacy(sourceDir);
45
+ if (!status.hook_memory_dir && !status.state_memory_table) {
46
+ return { ...status, archived: false, archive_dir: null };
47
+ }
48
+ const stamp = timestamp || new Date().toISOString().replace(/[:.]/g, '-');
49
+ const archiveDir = path.join(status.source_dir, '.ultra', `legacy-memory-archive-${stamp}`);
50
+ if (fs.existsSync(archiveDir)) throw new Error(`legacy memory archive already exists: ${archiveDir}`);
51
+ fs.mkdirSync(archiveDir, { recursive: true });
52
+
53
+ if (status.hook_memory_dir) {
54
+ fs.cpSync(status.hook_memory_path, path.join(archiveDir, 'hook-memory'), {
55
+ recursive: true,
56
+ dereference: false,
57
+ errorOnExist: true,
58
+ });
59
+ }
60
+
61
+ if (status.state_memory_table) {
62
+ const db = new Database(status.state_db_path, { readonly: true, fileMustExist: true });
63
+ let entries;
64
+ try {
65
+ entries = db.prepare('SELECT * FROM memory_entries ORDER BY id ASC').all();
66
+ } finally {
67
+ db.close();
68
+ }
69
+ fs.writeFileSync(
70
+ path.join(archiveDir, 'state-memory-entries.json'),
71
+ JSON.stringify({ schema: 1, source: status.state_db_path, entries }, null, 2) + '\n',
72
+ { encoding: 'utf8', flag: 'wx' },
73
+ );
74
+ }
75
+
76
+ fs.writeFileSync(
77
+ path.join(archiveDir, 'manifest.json'),
78
+ JSON.stringify({ schema: 1, archived_at: new Date().toISOString(), ...status }, null, 2) + '\n',
79
+ { encoding: 'utf8', flag: 'wx' },
80
+ );
81
+ return { ...status, archived: true, archive_dir: archiveDir };
82
+ }
83
+
84
+ function pruneLegacy(sourceDir = '.', { confirm, timestamp } = {}) {
85
+ if (confirm !== CONFIRMATION) {
86
+ throw new Error(`prune requires --confirm ${CONFIRMATION}`);
87
+ }
88
+ const archived = archiveLegacy(sourceDir, { timestamp });
89
+ if (!archived.archived) return { ...archived, pruned: false };
90
+
91
+ if (archived.hook_memory_dir) {
92
+ fs.rmSync(archived.hook_memory_path, { recursive: true, force: false });
93
+ }
94
+ if (archived.state_memory_table) {
95
+ const db = new Database(archived.state_db_path, { fileMustExist: true });
96
+ try {
97
+ db.transaction(() => {
98
+ db.exec('DROP TRIGGER IF EXISTS memory_ai');
99
+ db.exec('DROP TRIGGER IF EXISTS memory_ad');
100
+ db.exec('DROP TRIGGER IF EXISTS memory_au');
101
+ db.exec('DROP TABLE IF EXISTS memory_fts');
102
+ db.exec('DROP TABLE IF EXISTS memory_entries');
103
+ })();
104
+ } finally {
105
+ db.close();
106
+ }
107
+ }
108
+ return { ...archived, pruned: true };
109
+ }
110
+
111
+ function parseFlags(args) {
112
+ const flags = { _: [] };
113
+ for (let i = 0; i < args.length; i += 1) {
114
+ const token = args[i];
115
+ if (token === '--source-dir') {
116
+ flags.sourceDir = args[i + 1];
117
+ i += 1;
118
+ } else if (token.startsWith('--source-dir=')) {
119
+ flags.sourceDir = token.slice('--source-dir='.length);
120
+ } else if (token === '--confirm') {
121
+ flags.confirm = args[i + 1];
122
+ i += 1;
123
+ } else if (token.startsWith('--confirm=')) {
124
+ flags.confirm = token.slice('--confirm='.length);
125
+ } else {
126
+ flags._.push(token);
127
+ }
128
+ }
129
+ return flags;
130
+ }
131
+
132
+ function dispatch(args) {
133
+ const flags = parseFlags(args);
134
+ const action = flags._[0] || 'inspect';
135
+ try {
136
+ let data;
137
+ if (action === 'inspect') data = inspectLegacy(flags.sourceDir);
138
+ else if (action === 'archive') data = archiveLegacy(flags.sourceDir);
139
+ else if (action === 'prune') data = pruneLegacy(flags.sourceDir, { confirm: flags.confirm });
140
+ else throw new Error(`unknown legacy-memory action: ${action}`);
141
+ process.stdout.write(`${JSON.stringify({ ok: true, data })}\n`);
142
+ return 0;
143
+ } catch (error) {
144
+ process.stdout.write(`${JSON.stringify({
145
+ ok: false,
146
+ error: { code: 'LEGACY_MEMORY_MIGRATION_FAILED', message: error.message, retriable: false },
147
+ })}\n`);
148
+ return 2;
149
+ }
150
+ }
151
+
152
+ module.exports = {
153
+ CONFIRMATION,
154
+ inspectLegacy,
155
+ archiveLegacy,
156
+ pruneLegacy,
157
+ parseFlags,
158
+ dispatch,
159
+ };
@@ -0,0 +1,354 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const { initStateDb, openStateDb, closeStateDb } = require('../../mcp-server/lib/state-db.cjs');
7
+ const ops = require('../../mcp-server/lib/state-ops.cjs');
8
+
9
+ const SUPPORTED_FROM = '4.4';
10
+ const SUPPORTED_TO = '4.5';
11
+
12
+ // Frozen SQL — values flow through parameter bindings.
13
+ const RECORD_MIGRATION_SQL = "INSERT INTO migration_history (from_version, to_version, direction, status, notes) VALUES (@from, @to, @direction, @status, @notes)";
14
+
15
+ function emit(envelope) {
16
+ process.stdout.write(`${JSON.stringify(envelope)}\n`);
17
+ }
18
+
19
+ function parseFlags(args) {
20
+ const flags = { _: [] };
21
+ // Accept both "--from 4.4" and "--from=4.4" styles.
22
+ const valueOf = (token, i) => {
23
+ const eq = token.indexOf('=');
24
+ return eq >= 0 ? { value: token.slice(eq + 1), nextI: i } : { value: args[i + 1], nextI: i + 1 };
25
+ };
26
+ for (let i = 0; i < args.length; i++) {
27
+ const a = args[i];
28
+ if (a === '--dry') { flags.dry = true; continue; }
29
+ if (a === '--rollback') { flags.rollback = true; continue; }
30
+ if (a === '--help' || a === '-h') { flags.help = true; continue; }
31
+ if (a.startsWith('--from')) { const r = valueOf(a, i); flags.from = r.value; i = r.nextI; continue; }
32
+ if (a.startsWith('--to')) { const r = valueOf(a, i); flags.to = r.value; i = r.nextI; continue; }
33
+ if (a.startsWith('--source-dir')) { const r = valueOf(a, i); flags.sourceDir = r.value; i = r.nextI; continue; }
34
+ if (a.startsWith('--db-path')) { const r = valueOf(a, i); flags.dbPath = r.value; i = r.nextI; continue; }
35
+ flags._.push(a);
36
+ }
37
+ return flags;
38
+ }
39
+
40
+ function parseFrontmatter(text) {
41
+ if (!text.startsWith('---')) return null;
42
+ const end = text.indexOf('\n---', 3);
43
+ if (end === -1) return null;
44
+ const block = text.slice(3, end).trim();
45
+ const out = {};
46
+ for (const line of block.split('\n')) {
47
+ const idx = line.indexOf(':');
48
+ if (idx === -1) continue;
49
+ const key = line.slice(0, idx).trim();
50
+ const value = line.slice(idx + 1).trim().replace(/^["']|["']$/g, '');
51
+ out[key] = value;
52
+ }
53
+ return out;
54
+ }
55
+
56
+ function readJsonOptional(p) {
57
+ if (!fs.existsSync(p)) return null;
58
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
59
+ }
60
+
61
+ function findContexts(rootDir) {
62
+ const dir = path.join(rootDir, '.ultra', 'tasks', 'contexts');
63
+ if (!fs.existsSync(dir)) return [];
64
+ return fs.readdirSync(dir)
65
+ .filter((f) => f.endsWith('.md'))
66
+ .map((f) => path.join(dir, f));
67
+ }
68
+
69
+ function copyDirSync(src, dst) {
70
+ fs.mkdirSync(dst, { recursive: true });
71
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
72
+ const s = path.join(src, entry.name);
73
+ const d = path.join(dst, entry.name);
74
+ if (entry.name === 'state.db' || entry.name === 'state.db-wal' || entry.name === 'state.db-shm') continue;
75
+ if (entry.name.startsWith('backup-v4.4-')) continue;
76
+ if (entry.isDirectory()) copyDirSync(s, d);
77
+ else fs.copyFileSync(s, d);
78
+ }
79
+ }
80
+
81
+ function planForward(sourceDir) {
82
+ const tasksPath = path.join(sourceDir, '.ultra', 'tasks', 'tasks.json');
83
+ const tasksJson = readJsonOptional(tasksPath);
84
+ if (!tasksJson || !Array.isArray(tasksJson.tasks)) {
85
+ throw new Error(`migrate: tasks.json missing or malformed at ${tasksPath}`);
86
+ }
87
+
88
+ const contextHeaders = {};
89
+ for (const file of findContexts(sourceDir)) {
90
+ const fm = parseFrontmatter(fs.readFileSync(file, 'utf8'));
91
+ if (fm && fm.task_id) contextHeaders[fm.task_id] = { ...fm, _file: file };
92
+ }
93
+
94
+ const warnings = [];
95
+ for (const task of tasksJson.tasks) {
96
+ const ctx = contextHeaders[task.id];
97
+ if (ctx && ctx.status && ctx.status !== task.status) {
98
+ warnings.push({
99
+ task_id: task.id,
100
+ json_status: task.status,
101
+ context_status: ctx.status,
102
+ resolution: 'tasks.json wins (v4.4 → v4.5 D21 rule)',
103
+ });
104
+ }
105
+ }
106
+
107
+ const eventsPath = path.join(sourceDir, '.ultra', 'activity-log.json');
108
+ const events = readJsonOptional(eventsPath);
109
+ const eventList = Array.isArray(events) ? events : [];
110
+
111
+ return {
112
+ tasks: tasksJson.tasks,
113
+ events: eventList,
114
+ contextHeaders,
115
+ warnings,
116
+ };
117
+ }
118
+
119
+ function applyForward(db, plan) {
120
+ const insertTask = db.prepare(
121
+ "INSERT INTO tasks (id, title, type, priority, complexity, status, deps, tag, trace_to, context_file, created_at, updated_at) VALUES (@id, @title, @type, @priority, @complexity, @status, @deps, @tag, @trace_to, @context_file, @created_at, @updated_at)",
122
+ );
123
+ const insertEvent = db.prepare(
124
+ "INSERT INTO events (ts, type, task_id, session_id, runtime, payload_json) VALUES (@ts, @type, @task_id, @session_id, @runtime, @payload)",
125
+ );
126
+
127
+ const taskInserted = ops.tx(db, () => {
128
+ let n = 0;
129
+ for (const t of plan.tasks) {
130
+ const ctx = plan.contextHeaders[t.id];
131
+ insertTask.run({
132
+ id: t.id,
133
+ title: t.title,
134
+ type: t.type,
135
+ priority: t.priority,
136
+ complexity: t.complexity ?? null,
137
+ status: t.status,
138
+ deps: t.deps ? JSON.stringify(t.deps) : null,
139
+ tag: t.tag ?? null,
140
+ trace_to: t.trace_to ?? null,
141
+ context_file: ctx ? ctx._file : null,
142
+ created_at: t.created_at,
143
+ updated_at: t.updated_at,
144
+ });
145
+ n++;
146
+ }
147
+ return n;
148
+ });
149
+
150
+ const eventsInserted = ops.tx(db, () => {
151
+ let n = 0;
152
+ for (const e of plan.events) {
153
+ insertEvent.run({
154
+ ts: e.ts || new Date().toISOString(),
155
+ type: e.type,
156
+ task_id: e.task_id ?? null,
157
+ session_id: e.session_id ?? null,
158
+ runtime: e.runtime ?? null,
159
+ payload: e.payload === undefined ? null : JSON.stringify(e.payload),
160
+ });
161
+ n++;
162
+ }
163
+ return n;
164
+ });
165
+
166
+ return { taskInserted, eventsInserted };
167
+ }
168
+
169
+ function recordMigration(db, { from, to, direction, status, notes }) {
170
+ db.prepare(RECORD_MIGRATION_SQL).run({ from, to, direction, status, notes });
171
+ }
172
+
173
+ function ensureBackupName(sourceDir) {
174
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
175
+ return path.join(sourceDir, '.ultra', `backup-v4.4-${ts}`);
176
+ }
177
+
178
+ function findLatestBackup(sourceDir) {
179
+ const ultra = path.join(sourceDir, '.ultra');
180
+ if (!fs.existsSync(ultra)) return null;
181
+ const candidates = fs.readdirSync(ultra)
182
+ .filter((n) => n.startsWith('backup-v4.4-'))
183
+ .sort();
184
+ return candidates.length === 0 ? null : path.join(ultra, candidates[candidates.length - 1]);
185
+ }
186
+
187
+ function cmdForward(flags) {
188
+ const sourceDir = path.resolve(flags.sourceDir || '.');
189
+ const dbPath = path.resolve(flags.dbPath || path.join(sourceDir, '.ultra', 'state.db'));
190
+ let plan;
191
+ try {
192
+ plan = planForward(sourceDir);
193
+ } catch (err) {
194
+ emit({ ok: false, error: { code: 'MIGRATE_FAILED', message: err.message, retriable: false } });
195
+ return 2;
196
+ }
197
+
198
+ if (flags.dry) {
199
+ emit({
200
+ ok: true,
201
+ data: {
202
+ mode: 'dry',
203
+ from: SUPPORTED_FROM,
204
+ to: SUPPORTED_TO,
205
+ source_dir: sourceDir,
206
+ db_path: dbPath,
207
+ tasks_to_insert: plan.tasks.length,
208
+ events_to_insert: plan.events.length,
209
+ warnings: plan.warnings,
210
+ },
211
+ });
212
+ return 0;
213
+ }
214
+
215
+ // Real run: backup the entire .ultra subtree first.
216
+ const backupDir = ensureBackupName(sourceDir);
217
+ const ultraSource = path.join(sourceDir, '.ultra');
218
+ copyDirSync(ultraSource, backupDir);
219
+
220
+ let db;
221
+ try {
222
+ db = initStateDb(dbPath).db;
223
+ const counts = applyForward(db, plan);
224
+ recordMigration(db, {
225
+ from: SUPPORTED_FROM,
226
+ to: SUPPORTED_TO,
227
+ direction: 'forward',
228
+ status: 'success',
229
+ notes: `tasks=${counts.taskInserted} events=${counts.eventsInserted} warnings=${plan.warnings.length}`,
230
+ });
231
+ emit({
232
+ ok: true,
233
+ data: {
234
+ mode: 'apply',
235
+ from: SUPPORTED_FROM,
236
+ to: SUPPORTED_TO,
237
+ source_dir: sourceDir,
238
+ db_path: dbPath,
239
+ backup_dir: backupDir,
240
+ tasks_inserted: counts.taskInserted,
241
+ events_inserted: counts.eventsInserted,
242
+ warnings: plan.warnings,
243
+ },
244
+ });
245
+ return 0;
246
+ } catch (err) {
247
+ if (db) {
248
+ try {
249
+ recordMigration(db, {
250
+ from: SUPPORTED_FROM,
251
+ to: SUPPORTED_TO,
252
+ direction: 'forward',
253
+ status: 'failed',
254
+ notes: err.message,
255
+ });
256
+ } catch (_) { /* swallow secondary failure */ }
257
+ }
258
+ emit({ ok: false, error: { code: 'MIGRATE_FAILED', message: err.message, retriable: false } });
259
+ return 2;
260
+ } finally {
261
+ if (db) closeStateDb(db);
262
+ }
263
+ }
264
+
265
+ function cmdRollback(flags) {
266
+ const sourceDir = path.resolve(flags.sourceDir || '.');
267
+ const dbPath = path.resolve(flags.dbPath || path.join(sourceDir, '.ultra', 'state.db'));
268
+ const backupDir = findLatestBackup(sourceDir);
269
+ if (!backupDir) {
270
+ emit({ ok: false, error: { code: 'NO_BACKUP', message: 'no backup-v4.4-* directory found' } });
271
+ return 2;
272
+ }
273
+
274
+ // Record rollback BEFORE removing the db; if the file is gone we still want an audit row.
275
+ let db;
276
+ try {
277
+ if (fs.existsSync(dbPath)) {
278
+ db = openStateDb(dbPath);
279
+ recordMigration(db, {
280
+ from: SUPPORTED_TO,
281
+ to: SUPPORTED_FROM,
282
+ direction: 'rollback',
283
+ status: 'success',
284
+ notes: `restored from ${backupDir}`,
285
+ });
286
+ closeStateDb(db);
287
+ db = null;
288
+ fs.unlinkSync(dbPath);
289
+ const wal = `${dbPath}-wal`;
290
+ const shm = `${dbPath}-shm`;
291
+ if (fs.existsSync(wal)) fs.unlinkSync(wal);
292
+ if (fs.existsSync(shm)) fs.unlinkSync(shm);
293
+ }
294
+
295
+ // Restore .ultra contents from backup, leaving the backup itself in place.
296
+ const ultraDir = path.join(sourceDir, '.ultra');
297
+ for (const entry of fs.readdirSync(ultraDir, { withFileTypes: true })) {
298
+ if (entry.name.startsWith('backup-v4.4-')) continue;
299
+ const target = path.join(ultraDir, entry.name);
300
+ fs.rmSync(target, { recursive: true, force: true });
301
+ }
302
+ copyDirSync(backupDir, ultraDir);
303
+
304
+ emit({
305
+ ok: true,
306
+ data: {
307
+ mode: 'rollback',
308
+ from: SUPPORTED_TO,
309
+ to: SUPPORTED_FROM,
310
+ backup_dir: backupDir,
311
+ source_dir: sourceDir,
312
+ },
313
+ });
314
+ return 0;
315
+ } catch (err) {
316
+ if (db) closeStateDb(db);
317
+ emit({ ok: false, error: { code: 'ROLLBACK_FAILED', message: err.message, retriable: false } });
318
+ return 2;
319
+ }
320
+ }
321
+
322
+ function dispatch(args) {
323
+ const flags = parseFlags(args);
324
+ if (flags.help) {
325
+ process.stdout.write(USAGE);
326
+ return 0;
327
+ }
328
+ if (flags.from && flags.from !== SUPPORTED_FROM) {
329
+ emit({ ok: false, error: { code: 'UNSUPPORTED_VERSION', message: `--from ${flags.from} unsupported (only ${SUPPORTED_FROM})` } });
330
+ return 1;
331
+ }
332
+ if (flags.to && flags.to !== SUPPORTED_TO) {
333
+ emit({ ok: false, error: { code: 'UNSUPPORTED_VERSION', message: `--to ${flags.to} unsupported (only ${SUPPORTED_TO})` } });
334
+ return 1;
335
+ }
336
+ return flags.rollback ? cmdRollback(flags) : cmdForward(flags);
337
+ }
338
+
339
+ const USAGE = `ultra-tools migrate --from=4.4 --to=4.5 [flags]
340
+
341
+ Flags:
342
+ --source-dir <dir> project root containing .ultra/ (default: .)
343
+ --db-path <path> state.db destination (default: <source-dir>/.ultra/state.db)
344
+ --dry print the migration plan without writing
345
+ --rollback restore the most recent backup-v4.4-* and drop state.db
346
+
347
+ The forward flow: backup .ultra/ → init state.db → insert tasks from
348
+ tasks.json → merge context md status (tasks.json wins on conflict, warnings
349
+ recorded) → insert activity-log events → record migration_history.
350
+ Rollback restores from the latest backup-v4.4-* directory and writes a
351
+ matching migration_history rollback row before dropping state.db.
352
+ `;
353
+
354
+ module.exports = { dispatch, USAGE, parseFlags, planForward, parseFrontmatter };