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,180 @@
1
+ 'use strict';
2
+
3
+ // CLI fallback for the session.* MCP tool family.
4
+ // Phase 4.5.4/4.5.6 scope: list / admission / reap / get / heartbeat / close /
5
+ // subscribe. session.spawn is intentionally not in the CLI layer — it
6
+ // requires a long-lived parent process (the child's lifecycle tracks the
7
+ // caller). orchestrator/session-runner.cjs is the proper entry for spawn.
8
+
9
+ const path = require('node:path');
10
+ const { initStateDb, closeStateDb } = require('../../mcp-server/lib/state-db.cjs');
11
+ const ops = require('../../mcp-server/lib/state-ops.cjs');
12
+
13
+ const DEFAULT_DB_PATH = path.join('.ultra', 'state.db');
14
+
15
+ const USAGE = `ultra-tools session <verb> [flags]
16
+
17
+ VERBS:
18
+ list --status running|completed|crashed|orphan [--task-id <id>] [--limit <n>]
19
+ admission --task-id <id> admission_check
20
+ reap [--grace-seconds <n>] orphan sweeper (Phase 4.5.6)
21
+ get --sid <sid>
22
+ heartbeat --sid <sid>
23
+ close --sid <sid> --status completed|crashed|orphan
24
+ subscribe --since-id <n> [--sid <sid>] [--limit <n>]
25
+
26
+ GLOBAL FLAGS:
27
+ --db <path> path to state.db (default: .ultra/state.db)
28
+ -h, --help show this message
29
+ `;
30
+
31
+ function emit(envelope) {
32
+ process.stdout.write(`${JSON.stringify(envelope)}\n`);
33
+ }
34
+
35
+ function parseFlags(args) {
36
+ const flags = { _: [] };
37
+ for (let i = 0; i < args.length; i++) {
38
+ const a = args[i];
39
+ switch (a) {
40
+ case '--db': flags.db = args[++i]; break;
41
+ case '--status': flags.status = args[++i]; break;
42
+ case '--task-id': flags.task_id = args[++i]; break;
43
+ case '--sid': flags.sid = args[++i]; break;
44
+ case '--limit': flags.limit = Number(args[++i]); break;
45
+ case '--since-id': flags.since_id = Number(args[++i]); break;
46
+ case '--grace-seconds': flags.grace_seconds = Number(args[++i]); break;
47
+ case '-h': case '--help': flags.help = true; break;
48
+ default: flags._.push(a);
49
+ }
50
+ }
51
+ return flags;
52
+ }
53
+
54
+ function withDb(flags, fn) {
55
+ const dbPath = path.resolve(flags.db || DEFAULT_DB_PATH);
56
+ const { db } = initStateDb(dbPath);
57
+ try { return fn(db); }
58
+ finally { try { closeStateDb(db); } catch (_) { /* ignore */ } }
59
+ }
60
+
61
+ function cmdList(flags) {
62
+ return withDb(flags, (db) => {
63
+ const status = flags.status || 'running';
64
+ let sessions;
65
+ if (status === 'running') {
66
+ sessions = ops.listActiveSessions(db, { task_id: flags.task_id });
67
+ } else {
68
+ sessions = db.prepare(
69
+ "SELECT * FROM sessions WHERE status = ? AND (? IS NULL OR task_id = ?) ORDER BY started_at ASC LIMIT ?",
70
+ ).all(status, flags.task_id || null, flags.task_id || null, Math.min(flags.limit || 100, 500));
71
+ }
72
+ const limit = Math.min(flags.limit || 100, 500);
73
+ const trimmed = sessions.slice(0, limit);
74
+ emit({ ok: true, data: { sessions: trimmed, count: trimmed.length } });
75
+ return 0;
76
+ });
77
+ }
78
+
79
+ function cmdAdmission(flags) {
80
+ if (!flags.task_id) { emit({ ok: false, error: { code: 'VALIDATION_ERROR', message: 'missing --task-id' } }); return 1; }
81
+ return withDb(flags, (db) => {
82
+ try {
83
+ const v = ops.admissionCheck(db, flags.task_id);
84
+ emit({ ok: true, data: v });
85
+ return 0;
86
+ } catch (err) {
87
+ emit({ ok: false, error: { code: err.code || 'STATE_DB_ERROR', message: err.message, retriable: !!err.retriable } });
88
+ return err.code === 'TASK_NOT_FOUND' ? 1 : 2;
89
+ }
90
+ });
91
+ }
92
+
93
+ function cmdReap(flags) {
94
+ return withDb(flags, (db) => {
95
+ const grace = Number.isFinite(flags.grace_seconds) ? flags.grace_seconds : 300;
96
+ const r = ops.reapOrphanSessions(db, { graceSeconds: grace });
97
+ emit({ ok: true, data: r });
98
+ return 0;
99
+ });
100
+ }
101
+
102
+ function cmdGet(flags) {
103
+ if (!flags.sid) { emit({ ok: false, error: { code: 'VALIDATION_ERROR', message: 'missing --sid' } }); return 1; }
104
+ return withDb(flags, (db) => {
105
+ const s = ops.readSession(db, flags.sid);
106
+ if (!s) { emit({ ok: false, error: { code: 'SESSION_NOT_FOUND', message: `sid ${flags.sid} not found` } }); return 1; }
107
+ emit({ ok: true, data: { session: s } });
108
+ return 0;
109
+ });
110
+ }
111
+
112
+ function cmdHeartbeat(flags) {
113
+ if (!flags.sid) { emit({ ok: false, error: { code: 'VALIDATION_ERROR', message: 'missing --sid' } }); return 1; }
114
+ return withDb(flags, (db) => {
115
+ try {
116
+ const r = ops.heartbeatSession(db, flags.sid);
117
+ emit({ ok: true, data: r });
118
+ return 0;
119
+ } catch (err) {
120
+ emit({ ok: false, error: { code: err.code || 'STATE_DB_ERROR', message: err.message } });
121
+ return err.code === 'LEASE_EXPIRED' ? 3 : 2;
122
+ }
123
+ });
124
+ }
125
+
126
+ function cmdClose(flags) {
127
+ if (!flags.sid || !flags.status) { emit({ ok: false, error: { code: 'VALIDATION_ERROR', message: 'missing --sid or --status' } }); return 1; }
128
+ return withDb(flags, (db) => {
129
+ try {
130
+ ops.updateSession(db, flags.sid, { status: flags.status });
131
+ emit({ ok: true, data: { sid: flags.sid, status: flags.status } });
132
+ return 0;
133
+ } catch (err) {
134
+ emit({ ok: false, error: { code: err.code || 'STATE_DB_ERROR', message: err.message } });
135
+ return 2;
136
+ }
137
+ });
138
+ }
139
+
140
+ function cmdSubscribe(flags) {
141
+ return withDb(flags, (db) => {
142
+ const r = ops.subscribeEventsSince(db, {
143
+ since_id: Number.isFinite(flags.since_id) ? flags.since_id : 0,
144
+ task_id: flags.sid ? undefined : undefined,
145
+ limit: flags.limit,
146
+ });
147
+ emit({ ok: true, data: r });
148
+ return 0;
149
+ });
150
+ }
151
+
152
+ function dispatch(args) {
153
+ const [verb, ...rest] = args;
154
+ if (!verb || verb === '-h' || verb === '--help') {
155
+ process.stdout.write(USAGE);
156
+ return 0;
157
+ }
158
+ const flags = parseFlags(rest);
159
+ if (flags.help) { process.stdout.write(USAGE); return 0; }
160
+
161
+ switch (verb) {
162
+ case 'list': return cmdList(flags);
163
+ case 'admission': return cmdAdmission(flags);
164
+ case 'reap': return cmdReap(flags);
165
+ case 'get': return cmdGet(flags);
166
+ case 'heartbeat': return cmdHeartbeat(flags);
167
+ case 'close': return cmdClose(flags);
168
+ case 'subscribe': return cmdSubscribe(flags);
169
+ case 'spawn': {
170
+ emit({ ok: false, error: { code: 'NOT_SUPPORTED', message: 'session spawn via CLI is not supported; use orchestrator/session-runner.cjs — CLI cannot own the child lifecycle' } });
171
+ return 1;
172
+ }
173
+ default: {
174
+ emit({ ok: false, error: { code: 'UNKNOWN_VERB', message: `unknown session verb '${verb}'; see spec/cli-protocol.md for supported session CLI verbs` } });
175
+ return 1;
176
+ }
177
+ }
178
+ }
179
+
180
+ module.exports = { dispatch, USAGE };
@@ -0,0 +1,158 @@
1
+ 'use strict';
2
+
3
+ // Phase 6.3 — ultra-tools status cost panel.
4
+ //
5
+ // Reads telemetry aggregations from state.db (Phase 6.2) and renders either
6
+ // a human-readable table or a JSON envelope consumable by skills/ultra-status.
7
+ // Does not modify state — read-only command.
8
+
9
+ const fs = require('node:fs');
10
+ const path = require('node:path');
11
+
12
+ const ops = require('../../mcp-server/lib/state-ops.cjs');
13
+
14
+ const USAGE = `ultra-tools status [flags]
15
+
16
+ Reads telemetry from .ultra/state.db and prints a cost + activity summary.
17
+
18
+ FLAGS:
19
+ --cost include per-runtime + top-tasks cost panel (default on)
20
+ --json emit machine-readable envelope on stdout
21
+ --since <period> filter to last N (e.g. 7d, 24h) or ISO8601 cutoff
22
+ --limit <n> top-N tasks (default 3)
23
+ -h, --help show this message
24
+ `;
25
+
26
+ function emit(envelope) {
27
+ process.stdout.write(`${JSON.stringify(envelope)}\n`);
28
+ }
29
+
30
+ function fail(code, message) {
31
+ emit({ ok: false, error: { code, message, retriable: false } });
32
+ return 2;
33
+ }
34
+
35
+ function parseSince(value, nowMs = Date.now()) {
36
+ if (value === null || value === undefined || value === '') return null;
37
+ const s = String(value).trim();
38
+ if (/^\d{4}-\d{2}-\d{2}T/.test(s)) return s;
39
+ const m = s.match(/^(\d+)([dhm])$/i);
40
+ if (!m) throw new Error(`invalid --since value: ${value} (expected 7d, 24h, 30m, or ISO-8601)`);
41
+ const n = Number(m[1]);
42
+ const unit = m[2].toLowerCase();
43
+ const ms = unit === 'd' ? n * 86400000 : unit === 'h' ? n * 3600000 : n * 60000;
44
+ return new Date(nowMs - ms).toISOString();
45
+ }
46
+
47
+ function parseFlags(args) {
48
+ const out = { cost: true, json: false, since: null, limit: 3 };
49
+ for (let i = 0; i < args.length; i++) {
50
+ const a = args[i];
51
+ switch (a) {
52
+ case '--cost': out.cost = true; break;
53
+ case '--json': out.json = true; break;
54
+ case '--since': out.since = args[++i]; break;
55
+ case '--limit': out.limit = Number(args[++i]); break;
56
+ case '-h': case '--help': return { help: true };
57
+ default:
58
+ if (a.startsWith('--since=')) out.since = a.split('=')[1];
59
+ else if (a.startsWith('--limit=')) out.limit = Number(a.split('=')[1]);
60
+ else throw new Error(`unknown flag: ${a}`);
61
+ }
62
+ }
63
+ if (Number.isNaN(out.limit) || out.limit <= 0) out.limit = 3;
64
+ return out;
65
+ }
66
+
67
+ function buildCostPanel(db, { since = null, limit = 3 } = {}) {
68
+ const by_runtime = ops.aggregateTelemetryByRuntime(db, { since });
69
+ const top_tasks = ops.aggregateTelemetryByTask(db, { since, limit });
70
+ const total_cost_usd = by_runtime.reduce((acc, r) => acc + (r.cost_usd || 0), 0);
71
+ return {
72
+ period: { since: since || 'all-time' },
73
+ by_runtime,
74
+ top_tasks,
75
+ total_cost_usd,
76
+ };
77
+ }
78
+
79
+ function formatCost(n) {
80
+ if (!n) return '$0.0000';
81
+ if (n < 0.01) return `$${n.toFixed(6)}`;
82
+ return `$${n.toFixed(4)}`;
83
+ }
84
+
85
+ function renderHuman(panel) {
86
+ const lines = [];
87
+ lines.push(`Period: ${panel.period.since}`);
88
+ lines.push(`Total cost: ${formatCost(panel.total_cost_usd)}`);
89
+ lines.push('');
90
+ lines.push('Cost by runtime:');
91
+ if (panel.by_runtime.length === 0) {
92
+ lines.push(' (no telemetry)');
93
+ } else {
94
+ lines.push(' runtime calls tokens_in tokens_out cost');
95
+ for (const r of panel.by_runtime) {
96
+ lines.push(
97
+ ' ' + r.runtime.padEnd(10) +
98
+ String(r.calls).padStart(6) +
99
+ String(r.tokens_in).padStart(12) +
100
+ String(r.tokens_out).padStart(12) +
101
+ ' ' + formatCost(r.cost_usd),
102
+ );
103
+ }
104
+ }
105
+ lines.push('');
106
+ lines.push('Top tasks by cost:');
107
+ if (panel.top_tasks.length === 0) {
108
+ lines.push(' (no task-scoped telemetry)');
109
+ } else {
110
+ for (const t of panel.top_tasks) {
111
+ lines.push(` ${t.task_id.padEnd(24)} calls=${t.calls} cost=${formatCost(t.cost_usd)}`);
112
+ }
113
+ }
114
+ return lines.join('\n') + '\n';
115
+ }
116
+
117
+ function resolveDbPath() {
118
+ if (process.env.UBP_DB_PATH) return path.resolve(process.env.UBP_DB_PATH);
119
+ return path.resolve('.ultra', 'state.db');
120
+ }
121
+
122
+ function dispatch(args) {
123
+ let flags;
124
+ try { flags = parseFlags(args); } catch (err) { return fail('USAGE_ERROR', err.message); }
125
+ if (flags.help) {
126
+ process.stdout.write(USAGE);
127
+ return 0;
128
+ }
129
+ const dbPath = resolveDbPath();
130
+ if (!fs.existsSync(dbPath)) {
131
+ return fail('STATE_DB_MISSING', `state.db not found at ${dbPath}`);
132
+ }
133
+ let since;
134
+ try { since = parseSince(flags.since); } catch (err) { return fail('USAGE_ERROR', err.message); }
135
+
136
+ const Database = require('better-sqlite3');
137
+ const db = new Database(dbPath, { readonly: true });
138
+ try {
139
+ const panel = buildCostPanel(db, { since, limit: flags.limit });
140
+ if (flags.json) {
141
+ emit({ ok: true, data: panel });
142
+ } else {
143
+ process.stdout.write(renderHuman(panel));
144
+ }
145
+ return 0;
146
+ } finally {
147
+ db.close();
148
+ }
149
+ }
150
+
151
+ module.exports = {
152
+ USAGE,
153
+ dispatch,
154
+ buildCostPanel,
155
+ renderHuman,
156
+ parseSince,
157
+ parseFlags,
158
+ };
@@ -0,0 +1,95 @@
1
+ 'use strict';
2
+
3
+ // CLI dispatcher for the `task` family. Phase 3.1 implements `init-project`
4
+ // as the fallback for /ultra-init when MCP is unreachable. Additional verbs
5
+ // (create / update / list / ...) will be wired in Phase 3.3+ as each command
6
+ // migrates — they share the same stdout envelope contract declared in
7
+ // spec/cli-protocol.md §2.
8
+
9
+ const { initProject, InitProjectError } = require('../../mcp-server/lib/init-project.cjs');
10
+
11
+ const USAGE = `ultra-tools task <verb> [flags]
12
+
13
+ VERBS:
14
+ init-project Bootstrap a fresh .ultra/ skeleton (Phase 3.1)
15
+
16
+ FLAGS (init-project):
17
+ --target-dir <path> target project root (default: cwd)
18
+ --project-name <name> project name (required)
19
+ --project-type <type> web | api | cli | fullstack | other
20
+ --stack <stack> tech stack descriptor (comma-separated ok)
21
+ --overwrite replace existing .ultra/ (backup created)
22
+ --source-template <path> override bundled template source
23
+ -h, --help show this message
24
+ `;
25
+
26
+ function emit(envelope) {
27
+ process.stdout.write(`${JSON.stringify(envelope)}\n`);
28
+ }
29
+
30
+ function parseInitFlags(args) {
31
+ const flags = { _: [] };
32
+ for (let i = 0; i < args.length; i++) {
33
+ const a = args[i];
34
+ switch (a) {
35
+ case '--target-dir': flags.target_dir = args[++i]; break;
36
+ case '--project-name': flags.project_name = args[++i]; break;
37
+ case '--project-type': flags.project_type = args[++i]; break;
38
+ case '--stack': flags.stack = args[++i]; break;
39
+ case '--source-template': flags.source_template = args[++i]; break;
40
+ case '--overwrite': flags.overwrite = true; break;
41
+ case '--no-overwrite': flags.overwrite = false; break;
42
+ case '-h': case '--help': flags.help = true; break;
43
+ default: flags._.push(a);
44
+ }
45
+ }
46
+ return flags;
47
+ }
48
+
49
+ function dispatchInitProject(rawArgs) {
50
+ const flags = parseInitFlags(rawArgs);
51
+ if (flags.help) {
52
+ process.stdout.write(USAGE);
53
+ return 0;
54
+ }
55
+ if (!flags.project_name) {
56
+ emit({ ok: false, error: { code: 'VALIDATION_ERROR', message: 'missing required flag: --project-name' } });
57
+ return 1;
58
+ }
59
+ const input = {
60
+ target_dir: flags.target_dir || process.cwd(),
61
+ project_name: flags.project_name,
62
+ project_type: flags.project_type,
63
+ stack: flags.stack,
64
+ overwrite: !!flags.overwrite,
65
+ source_template: flags.source_template,
66
+ };
67
+ try {
68
+ const data = initProject(input);
69
+ emit({ ok: true, data });
70
+ return 0;
71
+ } catch (err) {
72
+ if (err instanceof InitProjectError) {
73
+ emit({ ok: false, error: { code: err.code, message: err.message, retriable: !!err.retriable } });
74
+ return err.code === 'VALIDATION_ERROR' ? 1 : 2;
75
+ }
76
+ emit({ ok: false, error: { code: 'UNKNOWN', message: err.message, retriable: false } });
77
+ return 2;
78
+ }
79
+ }
80
+
81
+ function dispatch(args) {
82
+ const [verb, ...rest] = args;
83
+ if (!verb || verb === '-h' || verb === '--help') {
84
+ process.stdout.write(USAGE);
85
+ return 0;
86
+ }
87
+ switch (verb) {
88
+ case 'init-project': return dispatchInitProject(rest);
89
+ default:
90
+ emit({ ok: false, error: { code: 'UNKNOWN_VERB', message: `unknown task verb '${verb}'; see spec/cli-protocol.md for supported task CLI verbs` } });
91
+ return 1;
92
+ }
93
+ }
94
+
95
+ module.exports = { dispatch, dispatchInitProject, USAGE };
@@ -0,0 +1,80 @@
1
+ 'use strict';
2
+
3
+ const { test } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const fs = require('node:fs');
6
+ const os = require('node:os');
7
+ const path = require('node:path');
8
+ const Database = require('better-sqlite3');
9
+
10
+ const legacy = require('./commands/legacy-memory.cjs');
11
+
12
+ function fixture() {
13
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-legacy-memory-'));
14
+ const ultra = path.join(root, '.ultra');
15
+ fs.mkdirSync(path.join(ultra, 'memory'), { recursive: true });
16
+ fs.writeFileSync(path.join(ultra, 'memory', 'memory.db'), 'legacy-hook-store');
17
+ const db = new Database(path.join(ultra, 'state.db'));
18
+ db.exec(`
19
+ CREATE TABLE memory_entries (
20
+ id INTEGER PRIMARY KEY, kind TEXT, content TEXT, task_id TEXT,
21
+ session_id TEXT, tag TEXT, source TEXT, ts TEXT
22
+ );
23
+ INSERT INTO memory_entries
24
+ (kind, content, task_id, session_id, tag, source, ts)
25
+ VALUES ('decision', 'legacy decision', 't1', 's1', 'arch', 'test', '2026-01-01T00:00:00Z');
26
+ `);
27
+ db.close();
28
+ return root;
29
+ }
30
+
31
+ test('inspect reports both legacy stores without mutating them', () => {
32
+ const root = fixture();
33
+ try {
34
+ const result = legacy.inspectLegacy(root);
35
+ assert.equal(result.hook_memory_dir, true);
36
+ assert.equal(result.state_memory_table, true);
37
+ assert.equal(result.state_memory_entries, 1);
38
+ assert.ok(fs.existsSync(path.join(root, '.ultra', 'memory', 'memory.db')));
39
+ } finally {
40
+ fs.rmSync(root, { recursive: true, force: true });
41
+ }
42
+ });
43
+ test('archive exports legacy state rows and copies hook memory without deletion', () => {
44
+ const root = fixture();
45
+ try {
46
+ const result = legacy.archiveLegacy(root, { timestamp: '2026-07-15T00-00-00-000Z' });
47
+ assert.equal(result.archived, true);
48
+ assert.ok(fs.existsSync(path.join(result.archive_dir, 'hook-memory', 'memory.db')));
49
+ const exported = JSON.parse(fs.readFileSync(path.join(result.archive_dir, 'state-memory-entries.json'), 'utf8'));
50
+ assert.equal(exported.entries.length, 1);
51
+ assert.equal(exported.entries[0].content, 'legacy decision');
52
+ assert.ok(fs.existsSync(path.join(root, '.ultra', 'memory', 'memory.db')));
53
+ const db = new Database(path.join(root, '.ultra', 'state.db'), { readonly: true });
54
+ assert.ok(db.prepare("SELECT 1 FROM sqlite_master WHERE name = 'memory_entries'").get());
55
+ db.close();
56
+ } finally {
57
+ fs.rmSync(root, { recursive: true, force: true });
58
+ }
59
+ });
60
+
61
+ test('prune requires an exact confirmation and archives before deleting', () => {
62
+ const root = fixture();
63
+ try {
64
+ assert.throws(() => legacy.pruneLegacy(root, { confirm: 'yes' }), /DELETE_ULTRA_LEGACY_MEMORY/);
65
+ assert.ok(fs.existsSync(path.join(root, '.ultra', 'memory', 'memory.db')));
66
+
67
+ const result = legacy.pruneLegacy(root, {
68
+ confirm: 'DELETE_ULTRA_LEGACY_MEMORY',
69
+ timestamp: '2026-07-15T00-00-01-000Z',
70
+ });
71
+ assert.equal(result.pruned, true);
72
+ assert.ok(fs.existsSync(result.archive_dir));
73
+ assert.ok(!fs.existsSync(path.join(root, '.ultra', 'memory')));
74
+ const db = new Database(path.join(root, '.ultra', 'state.db'), { readonly: true });
75
+ assert.equal(db.prepare("SELECT 1 FROM sqlite_master WHERE name = 'memory_entries'").get(), undefined);
76
+ db.close();
77
+ } finally {
78
+ fs.rmSync(root, { recursive: true, force: true });
79
+ }
80
+ });
@@ -0,0 +1,132 @@
1
+ 'use strict';
2
+
3
+ // Phase 6.3 — ultra-tools status cost panel:
4
+ // • `ultra-tools status --cost --json` prints {period, by_runtime,
5
+ // top_tasks, total_cost_usd}.
6
+ // • Default human-readable mode renders a summary table.
7
+ // • --since 7d filters telemetry to the last 7 days.
8
+
9
+ const { test } = require('node:test');
10
+ const assert = require('node:assert/strict');
11
+ const fs = require('node:fs');
12
+ const os = require('node:os');
13
+ const path = require('node:path');
14
+
15
+ const { initStateDb, closeStateDb } = require('../mcp-server/lib/state-db.cjs');
16
+ const ops = require('../mcp-server/lib/state-ops.cjs');
17
+ const telemetry = require('../mcp-server/lib/telemetry.cjs');
18
+ const statusCmd = require('./commands/status.cjs');
19
+
20
+ function freshFixture() {
21
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-status-'));
22
+ const file = path.join(dir, '.ultra', 'state.db');
23
+ const init = initStateDb(file);
24
+ return { dir, db: init.db };
25
+ }
26
+
27
+ function teardown(dir, db) {
28
+ try { closeStateDb(db); } catch (_) { /* ignore */ }
29
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_) { /* ignore */ }
30
+ }
31
+
32
+ function seedCalls(db, dir) {
33
+ ops.createTask(db, { id: 't-cheap', title: 'cheap', type: 'feature', priority: 'P2' });
34
+ ops.createTask(db, { id: 't-pricey', title: 'pricey', type: 'feature', priority: 'P1' });
35
+ ops.createSession(db, { sid: 's-x', task_id: 't-cheap', runtime: 'claude', worktree_path: `${dir}/w1`, artifact_dir: `${dir}/a1` });
36
+ ops.createSession(db, { sid: 's-y', task_id: 't-pricey', runtime: 'codex', worktree_path: `${dir}/w2`, artifact_dir: `${dir}/a2` });
37
+ telemetry.appendTelemetry(db, { event_type: 'token_usage', tool_name: 't', session_id: 's-x', runtime: 'claude', tokens_input: 500, tokens_output: 100, rootDir: dir, payload: { model: 'claude-haiku-4-5' } });
38
+ telemetry.appendTelemetry(db, { event_type: 'token_usage', tool_name: 't', session_id: 's-y', runtime: 'codex', tokens_input: 10000, tokens_output: 2000, rootDir: dir, payload: { model: 'gpt-5.4' } });
39
+ telemetry.appendTelemetry(db, { event_type: 'tool_call', tool_name: 'task.list', session_id: null, rootDir: dir });
40
+ }
41
+
42
+ test('status --json --cost returns by_runtime + top_tasks + total_cost', () => {
43
+ const { dir, db } = freshFixture();
44
+ try {
45
+ seedCalls(db, dir);
46
+ const out = statusCmd.buildCostPanel(db, { since: null, limit: 10 });
47
+ assert.ok(Array.isArray(out.by_runtime));
48
+ assert.ok(Array.isArray(out.top_tasks));
49
+ assert.ok(typeof out.total_cost_usd === 'number');
50
+ const byR = Object.fromEntries(out.by_runtime.map((r) => [r.runtime, r]));
51
+ assert.equal(byR.claude.calls, 1);
52
+ assert.equal(byR.codex.calls, 1);
53
+ assert.equal(byR.unknown.calls, 1); // CLI call with null session
54
+ assert.equal(out.top_tasks[0].task_id, 't-pricey');
55
+ assert.ok(out.total_cost_usd > 0);
56
+ } finally { teardown(dir, db); }
57
+ });
58
+
59
+ test('buildCostPanel: empty db → zero totals, empty arrays', () => {
60
+ const { dir, db } = freshFixture();
61
+ try {
62
+ const out = statusCmd.buildCostPanel(db, {});
63
+ assert.equal(out.total_cost_usd, 0);
64
+ assert.equal(out.by_runtime.length, 0);
65
+ assert.equal(out.top_tasks.length, 0);
66
+ } finally { teardown(dir, db); }
67
+ });
68
+
69
+ test('buildCostPanel: since cutoff in future → all filtered out', () => {
70
+ const { dir, db } = freshFixture();
71
+ try {
72
+ seedCalls(db, dir);
73
+ const future = new Date(Date.now() + 60 * 1000).toISOString();
74
+ const out = statusCmd.buildCostPanel(db, { since: future });
75
+ assert.equal(out.by_runtime.length, 0);
76
+ assert.equal(out.top_tasks.length, 0);
77
+ } finally { teardown(dir, db); }
78
+ });
79
+
80
+ test('parseSince: 7d / 24h / iso passthrough', () => {
81
+ const now = Date.parse('2026-04-17T12:00:00Z');
82
+ const sevenDaysAgo = statusCmd.parseSince('7d', now);
83
+ const expectedDayCutoff = new Date(now - 7 * 24 * 3600 * 1000).toISOString();
84
+ assert.equal(sevenDaysAgo, expectedDayCutoff);
85
+ assert.equal(statusCmd.parseSince('24h', now), new Date(now - 24 * 3600 * 1000).toISOString());
86
+ assert.equal(statusCmd.parseSince('2026-04-10T00:00:00Z'), '2026-04-10T00:00:00Z');
87
+ assert.equal(statusCmd.parseSince(null), null);
88
+ });
89
+
90
+ test('parseSince: invalid → throws', () => {
91
+ assert.throws(() => statusCmd.parseSince('bogus'), /since/i);
92
+ });
93
+
94
+ test('renderHuman: includes key section headers', () => {
95
+ const { dir, db } = freshFixture();
96
+ try {
97
+ seedCalls(db, dir);
98
+ const panel = statusCmd.buildCostPanel(db, {});
99
+ const text = statusCmd.renderHuman(panel);
100
+ assert.match(text, /Cost by runtime/i);
101
+ assert.match(text, /Top tasks/i);
102
+ assert.match(text, /claude/);
103
+ assert.match(text, /codex/);
104
+ } finally { teardown(dir, db); }
105
+ });
106
+
107
+ test('dispatch: --json echoes buildCostPanel output shape', () => {
108
+ const { dir, db } = freshFixture();
109
+ try {
110
+ seedCalls(db, dir);
111
+ closeStateDb(db);
112
+ const captured = [];
113
+ const origWrite = process.stdout.write.bind(process.stdout);
114
+ process.stdout.write = (chunk) => { captured.push(String(chunk)); return true; };
115
+ const envBackup = process.env.UBP_DB_PATH;
116
+ process.env.UBP_DB_PATH = path.join(dir, '.ultra', 'state.db');
117
+ try {
118
+ const code = statusCmd.dispatch(['--cost', '--json']);
119
+ assert.equal(code, 0);
120
+ } finally {
121
+ process.stdout.write = origWrite;
122
+ if (envBackup === undefined) delete process.env.UBP_DB_PATH; else process.env.UBP_DB_PATH = envBackup;
123
+ }
124
+ const joined = captured.join('');
125
+ const parsed = JSON.parse(joined);
126
+ assert.ok(parsed.ok);
127
+ assert.ok(parsed.data.by_runtime);
128
+ assert.ok(parsed.data.top_tasks);
129
+ } finally {
130
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_) { /* ignore */ }
131
+ }
132
+ });