ultra-builder-pro-cli 0.14.0 → 0.15.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 (154) hide show
  1. package/.ultra-template/docs/research/README.md +11 -7
  2. package/.ultra-template/specs/architecture.md +36 -36
  3. package/.ultra-template/specs/discovery.md +46 -33
  4. package/.ultra-template/specs/product.md +38 -29
  5. package/.ultra-template/specs/research-distillate.md +48 -0
  6. package/.ultra-template/tasks/contexts/TEMPLATE.md +11 -28
  7. package/CHANGELOG.md +44 -1
  8. package/README.md +34 -19
  9. package/adapters/_shared/codex-assets.cjs +6 -2
  10. package/adapters/_shared/file-ops.cjs +6 -2
  11. package/adapters/claude.js +3 -2
  12. package/adapters/codex.js +1 -0
  13. package/adapters/kimi.js +3 -2
  14. package/adapters/opencode.js +3 -2
  15. package/agents/review-coordinator.md +9 -6
  16. package/docs/AGENT-CONTEXT.md +15 -8
  17. package/docs/ARCHITECTURE.md +33 -12
  18. package/docs/PLAN.zh-CN.md +3 -2
  19. package/docs/ROADMAP.md +8 -4
  20. package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
  21. package/docs/STATE-DB-ACCESS-POLICY.md +27 -25
  22. package/docs/WORKFLOW-LIFECYCLE.md +266 -0
  23. package/hooks/README.md +6 -1
  24. package/hooks/health_check.py +12 -3
  25. package/hooks/subagent_tracker.py +52 -77
  26. package/mcp-server/hook-event.cjs +34 -0
  27. package/mcp-server/lib/archive-journal.cjs +17 -5
  28. package/mcp-server/lib/baseline-workflow.cjs +158 -18
  29. package/mcp-server/lib/change-workflow.cjs +550 -85
  30. package/mcp-server/lib/context-spine.cjs +242 -24
  31. package/mcp-server/lib/doctor.cjs +33 -3
  32. package/mcp-server/lib/init-project.cjs +210 -16
  33. package/mcp-server/lib/plan-store.cjs +4 -1
  34. package/mcp-server/lib/pricing.cjs +7 -10
  35. package/mcp-server/lib/project-breadcrumb.cjs +6 -0
  36. package/mcp-server/lib/projector.cjs +57 -3
  37. package/mcp-server/lib/spec-learning.cjs +79 -2
  38. package/mcp-server/lib/state-db.cjs +145 -8
  39. package/mcp-server/lib/state-ops.cjs +160 -9
  40. package/mcp-server/lib/workflow-state.cjs +2292 -0
  41. package/mcp-server/server.cjs +161 -15
  42. package/orchestrator/daemon.cjs +1 -2
  43. package/orchestrator/dispatch-rules.cjs +5 -31
  44. package/orchestrator/planner/plan-builder.cjs +21 -12
  45. package/package.json +13 -3
  46. package/skills/ultra-change/SKILL.md +53 -68
  47. package/skills/ultra-change/references/change-contract.md +36 -0
  48. package/skills/ultra-deliver/SKILL.md +69 -79
  49. package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
  50. package/skills/ultra-dev/SKILL.md +54 -59
  51. package/skills/ultra-doctor/SKILL.md +40 -71
  52. package/skills/ultra-init/SKILL.md +96 -103
  53. package/skills/ultra-init/references/brownfield-adoption.md +2 -2
  54. package/skills/ultra-plan/SKILL.md +79 -85
  55. package/skills/ultra-plan/references/semantic-preflight.md +18 -0
  56. package/skills/ultra-research/SKILL.md +87 -60
  57. package/skills/ultra-research/references/00-problem-validation.md +36 -0
  58. package/skills/ultra-research/references/01-opportunity-discovery.md +33 -0
  59. package/skills/ultra-research/references/02-market-assessment.md +30 -0
  60. package/skills/ultra-research/references/03-competitive-landscape.md +31 -0
  61. package/skills/ultra-research/references/04-product-strategy.md +32 -0
  62. package/skills/ultra-research/references/05-assumptions-validation.md +31 -0
  63. package/skills/ultra-research/references/10-user-personas.md +29 -0
  64. package/skills/ultra-research/references/11-user-scenarios.md +28 -0
  65. package/skills/ultra-research/references/20-user-stories.md +30 -0
  66. package/skills/ultra-research/references/21-features-scope.md +29 -0
  67. package/skills/ultra-research/references/22-success-metrics.md +28 -0
  68. package/skills/ultra-research/references/30-architecture-context.md +30 -0
  69. package/skills/ultra-research/references/31-solution-strategy.md +28 -0
  70. package/skills/ultra-research/references/32-building-blocks.md +28 -0
  71. package/skills/ultra-research/references/40-deployment.md +29 -0
  72. package/skills/ultra-research/references/41-quality-risks.md +28 -0
  73. package/skills/ultra-research/references/99-synthesis.md +35 -0
  74. package/skills/ultra-review/SKILL.md +57 -73
  75. package/skills/ultra-review/references/review-modes.md +27 -0
  76. package/skills/ultra-review/references/unified-schema.md +25 -0
  77. package/skills/ultra-review/scripts/review_wait.py +40 -3
  78. package/skills/ultra-status/SKILL.md +41 -45
  79. package/skills/ultra-test/SKILL.md +48 -55
  80. package/skills/ultra-verify/SKILL.md +1 -1
  81. package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
  82. package/spec/cli-protocol.md +7 -2
  83. package/spec/mcp-tools.yaml +384 -63
  84. package/spec/schemas/baseline-reconciliation.v1.schema.json +73 -0
  85. package/spec/schemas/delivery-report.v1.schema.json +111 -0
  86. package/spec/schemas/mcp-tools.schema.json +1 -1
  87. package/spec/schemas/state-db.sql +82 -1
  88. package/spec/schemas/tasks.v4.5.schema.json +8 -1
  89. package/spec/schemas/test-report.v1.schema.json +194 -0
  90. package/ultra-tools/cli.cjs +1 -1
  91. package/ultra-tools/commands/migrate.cjs +3 -3
  92. package/ultra-tools/commands/status.cjs +213 -12
  93. package/.ultra-template/delivery-report.json +0 -9
  94. package/.ultra-template/test-report.json +0 -8
  95. package/adapters/_shared/tests/file-ops.test.cjs +0 -54
  96. package/adapters/_shared/tests/frontmatter.test.cjs +0 -47
  97. package/adapters/_shared/tests/handbook.test.cjs +0 -101
  98. package/adapters/_shared/tests/path-rewrite.test.cjs +0 -60
  99. package/adapters/_shared/tests/provenance.test.cjs +0 -133
  100. package/adapters/_shared/tests/resolve-target.test.cjs +0 -75
  101. package/adapters/_shared/tests/runtime-assets.test.cjs +0 -149
  102. package/adapters/_shared/tests/settings-merge.test.cjs +0 -85
  103. package/adapters/_shared/tests/validate.test.cjs +0 -30
  104. package/adapters/tests/claude.test.cjs +0 -138
  105. package/adapters/tests/codex-hook.test.cjs +0 -142
  106. package/adapters/tests/codex.test.cjs +0 -430
  107. package/adapters/tests/kimi-hook.test.cjs +0 -149
  108. package/adapters/tests/kimi.test.cjs +0 -259
  109. package/adapters/tests/opencode.test.cjs +0 -357
  110. package/mcp-server/lib/archive-journal.test.cjs +0 -35
  111. package/mcp-server/lib/baseline-workflow.test.cjs +0 -477
  112. package/mcp-server/lib/change-workflow.test.cjs +0 -971
  113. package/mcp-server/lib/context-spine.test.cjs +0 -93
  114. package/mcp-server/lib/doctor.test.cjs +0 -148
  115. package/mcp-server/lib/plan-store.test.cjs +0 -132
  116. package/mcp-server/lib/prd-parser.test.cjs +0 -82
  117. package/mcp-server/lib/project-breadcrumb.test.cjs +0 -112
  118. package/mcp-server/lib/runtime-state.test.cjs +0 -92
  119. package/mcp-server/lib/spec-learning.test.cjs +0 -67
  120. package/mcp-server/lib/state-db.test.cjs +0 -314
  121. package/mcp-server/lib/task-expander.test.cjs +0 -169
  122. package/mcp-server/lib/topo.test.cjs +0 -158
  123. package/mcp-server/tests/circuit-breaker.test.cjs +0 -125
  124. package/mcp-server/tests/commit-hash-flow.test.cjs +0 -117
  125. package/mcp-server/tests/concurrency-update.test.cjs +0 -96
  126. package/mcp-server/tests/concurrency.test.cjs +0 -112
  127. package/mcp-server/tests/db-ops.test.cjs +0 -121
  128. package/mcp-server/tests/init-project.test.cjs +0 -393
  129. package/mcp-server/tests/migrate.test.cjs +0 -232
  130. package/mcp-server/tests/projector.test.cjs +0 -162
  131. package/mcp-server/tests/server-contract.test.cjs +0 -1467
  132. package/mcp-server/tests/staleness.test.cjs +0 -190
  133. package/mcp-server/tests/state-db.test.cjs +0 -106
  134. package/mcp-server/tests/state-ops.test.cjs +0 -449
  135. package/mcp-server/tests/tagged-tasks.test.cjs +0 -163
  136. package/mcp-server/tests/telemetry.test.cjs +0 -219
  137. package/orchestrator/planner/plan-builder.test.cjs +0 -165
  138. package/orchestrator/tests/auto-merge.test.cjs +0 -216
  139. package/orchestrator/tests/daemon.test.cjs +0 -311
  140. package/orchestrator/tests/dispatch-rules.test.cjs +0 -124
  141. package/orchestrator/tests/parallel-orchestrator.test.cjs +0 -233
  142. package/orchestrator/tests/phase-8b-gate.test.cjs +0 -183
  143. package/orchestrator/tests/recovery.test.cjs +0 -190
  144. package/orchestrator/tests/session-runner.test.cjs +0 -314
  145. package/orchestrator/tests/skill-miner.test.cjs +0 -147
  146. package/orchestrator/tests/worktree-manager.test.cjs +0 -148
  147. package/skills/ultra-research/references/architecture.md +0 -38
  148. package/skills/ultra-research/references/problem-product.md +0 -39
  149. package/skills/ultra-research/references/quality-delivery.md +0 -34
  150. package/skills/ultra-research/references/synthesis.md +0 -38
  151. package/skills/ultra-research/references/users-scope.md +0 -40
  152. package/ultra-tools/legacy-memory.test.cjs +0 -80
  153. package/ultra-tools/status-command.test.cjs +0 -181
  154. package/ultra-tools/system-command.test.cjs +0 -309
@@ -1,20 +1,23 @@
1
1
  'use strict';
2
2
 
3
- // Phase 6.3 — ultra-tools status cost panel.
3
+ // Ultra project status panel.
4
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.
5
+ // Reads project authority and telemetry from state.db and renders either a
6
+ // human-readable report or a JSON envelope consumable by skills/ultra-status.
7
+ // Does not modify state.
8
8
 
9
9
  const fs = require('node:fs');
10
10
  const path = require('node:path');
11
11
 
12
12
  const ops = require('../../mcp-server/lib/state-ops.cjs');
13
13
  const baselines = require('../../mcp-server/lib/baseline-workflow.cjs');
14
+ const workflowState = require('../../mcp-server/lib/workflow-state.cjs');
15
+ const { readBreadcrumb } = require('../../mcp-server/lib/context-spine.cjs');
14
16
 
15
17
  const USAGE = `ultra-tools status [flags]
16
18
 
17
- Reads telemetry from .ultra/state.db and prints a cost + activity summary.
19
+ Reads .ultra/state.db and prints baseline, workflow, task, session, route, and
20
+ cost-observability status.
18
21
 
19
22
  FLAGS:
20
23
  --cost include per-runtime + top-tasks cost panel (default on)
@@ -65,10 +68,25 @@ function parseFlags(args) {
65
68
  return out;
66
69
  }
67
70
 
68
- function buildCostPanel(db, { since = null, limit = 3, rootDir = process.cwd() } = {}) {
69
- const by_runtime = ops.aggregateTelemetryByRuntime(db, { since });
70
- const top_tasks = ops.aggregateTelemetryByTask(db, { since, limit });
71
- const total_cost_usd = by_runtime.reduce((acc, r) => acc + (r.cost_usd || 0), 0);
71
+ function buildStatusPanel(db, { since = null, limit = 3, rootDir = process.cwd() } = {}) {
72
+ const tables = new Set(db.prepare(
73
+ "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'",
74
+ ).all().map((row) => row.name));
75
+ const by_runtime = tables.has('telemetry') && tables.has('sessions')
76
+ ? ops.aggregateTelemetryByRuntime(db, { since }) : [];
77
+ const top_tasks = tables.has('telemetry') && tables.has('sessions')
78
+ ? ops.aggregateTelemetryByTask(db, { since, limit }) : [];
79
+ const knownTotalCost = by_runtime.reduce((acc, r) => acc + (r.cost_usd || 0), 0);
80
+ const unpricedUsageEvents = by_runtime.reduce(
81
+ (acc, row) => acc + Number(row.unpriced_usage_events || 0), 0,
82
+ );
83
+ const pricedUsageEvents = by_runtime.reduce(
84
+ (acc, row) => acc + Number(row.priced_usage_events || 0), 0,
85
+ );
86
+ const costStatus = unpricedUsageEvents > 0
87
+ ? (pricedUsageEvents > 0 ? 'partial' : 'unavailable')
88
+ : (pricedUsageEvents > 0 ? 'complete' : 'none');
89
+ const total_cost_usd = unpricedUsageEvents > 0 ? null : knownTotalCost;
72
90
  const baselineHealth = baselines.inspectBaseline(db, { rootDir });
73
91
  const current = baselineHealth.baseline;
74
92
  const baselineStatus = current?.status
@@ -87,16 +105,165 @@ function buildCostPanel(db, { since = null, limit = 3, rootDir = process.cwd() }
87
105
  baseline.repository_branch = current.repository_branch || null;
88
106
  baseline.worktree_state = current.worktree_state || 'unavailable';
89
107
  baseline.gaps = baselines.summarizeGaps(current.gaps);
108
+ if (current.research_run_id && tables.has('workflow_runs') && tables.has('workflow_steps')) {
109
+ const research = workflowState.readWorkflow(db, current.research_run_id, { rootDir });
110
+ baseline.research = research ? {
111
+ id: research.id,
112
+ status: research.status,
113
+ mode: research.mode,
114
+ output_health: research.artifact_health.status,
115
+ blockers: research.artifact_health.blockers,
116
+ } : {
117
+ id: current.research_run_id, status: 'missing', mode: null,
118
+ output_health: 'fail', blockers: ['BASELINE_RESEARCH_RECORD_INVALID'],
119
+ };
120
+ }
121
+ }
122
+ const countStatuses = (table, statuses, where = '') => {
123
+ if (!tables.has(table)) {
124
+ return {
125
+ status: 'unavailable',
126
+ ...Object.fromEntries(statuses.map((status) => [status, 0])),
127
+ };
128
+ }
129
+ const rows = db.prepare(
130
+ `SELECT status, COUNT(*) AS count FROM ${table} ${where} GROUP BY status`,
131
+ ).all();
132
+ const counts = Object.fromEntries(statuses.map((status) => [status, 0]));
133
+ for (const row of rows) if (row.status in counts) counts[row.status] = Number(row.count);
134
+ return counts;
135
+ };
136
+ const taskCounts = countStatuses(
137
+ 'tasks', ['pending', 'in_progress', 'completed', 'blocked', 'expanded'],
138
+ );
139
+ taskCounts.stale = tables.has('tasks')
140
+ ? Number(db.prepare('SELECT COUNT(*) AS count FROM tasks WHERE stale = 1').get().count) : 0;
141
+ const sessions = countStatuses('sessions', ['running', 'completed', 'crashed', 'orphan']);
142
+ const changes = countStatuses('changes', ['active', 'blocked', 'ready', 'archived', 'cancelled']);
143
+ let workflows;
144
+ try {
145
+ if (!tables.has('workflow_runs') || !tables.has('workflow_steps')) {
146
+ workflows = {
147
+ status: 'unavailable', active: 0, blocked: 0, ready: 0,
148
+ stale_outputs: [], historical_stale_outputs: [],
149
+ terminal_authority_runs: [], untracked_active_changes: [],
150
+ };
151
+ } else {
152
+ workflows = workflowState.inspectWorkflowHealth(db, { rootDir });
153
+ workflows.current = workflowState.listWorkflows(db, { limit: 500 }, { rootDir })
154
+ .filter((run) => ['active', 'blocked', 'ready'].includes(run.status))
155
+ .map((run) => ({
156
+ id: run.id, kind: run.kind, status: run.status, current_step: run.current_step,
157
+ baseline_id: run.baseline_id, change_id: run.change_id, task_id: run.task_id,
158
+ blockers: run.blockers, output_health: run.artifact_health.status,
159
+ }));
160
+ }
161
+ } catch (error) {
162
+ workflows = {
163
+ status: 'fail', active: 0, blocked: 0, ready: 0,
164
+ stale_outputs: [], historical_stale_outputs: [],
165
+ terminal_authority_runs: [], untracked_active_changes: [],
166
+ error: error.code || error.message,
167
+ };
168
+ }
169
+ if (!Array.isArray(workflows.current)) workflows.current = [];
170
+ let next;
171
+ let breadcrumb = null;
172
+ try {
173
+ if (!tables.has('baselines')) {
174
+ next = {
175
+ action: 'Resume project initialization to migrate and establish baseline authority.',
176
+ recommended_workflow: 'ultra-init', readiness: 'blocked',
177
+ blockers: ['BASELINE_SCHEMA_MIGRATION_REQUIRED'], warnings: [],
178
+ };
179
+ } else {
180
+ breadcrumb = readBreadcrumb(db, {}, { rootDir });
181
+ next = {
182
+ action: breadcrumb.next_action,
183
+ recommended_workflow: breadcrumb.recommended_workflow,
184
+ readiness: breadcrumb.readiness,
185
+ blockers: breadcrumb.blockers,
186
+ warnings: breadcrumb.warnings,
187
+ change_id: breadcrumb.change_id || null,
188
+ task_id: breadcrumb.task_id || null,
189
+ workflow: breadcrumb.workflow || null,
190
+ };
191
+ }
192
+ } catch (error) {
193
+ next = {
194
+ action: 'Run ultra-doctor to inspect authoritative state.',
195
+ recommended_workflow: 'ultra-doctor', readiness: 'blocked',
196
+ blockers: [`STATUS_AUTHORITY_UNAVAILABLE:${error.code || 'unknown'}`], warnings: [],
197
+ };
198
+ }
199
+
200
+ const currentChange = tables.has('changes')
201
+ ? db.prepare(
202
+ `SELECT id, title, kind, status, intent, base_commit, artifact_root, updated_at
203
+ FROM changes
204
+ WHERE status IN ('active', 'blocked', 'ready')
205
+ ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
206
+ ).get() || null
207
+ : null;
208
+ const evidenceChangeId = breadcrumb?.change_id || currentChange?.id
209
+ || (tables.has('changes') ? db.prepare(
210
+ `SELECT id FROM changes WHERE status = 'archived'
211
+ ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
212
+ ).get()?.id : null);
213
+ let currentTask = null;
214
+ if (tables.has('tasks')) {
215
+ const taskId = breadcrumb?.task_id || (currentChange?.id ? db.prepare(
216
+ `SELECT id FROM tasks WHERE change_id = ?
217
+ ORDER BY CASE status WHEN 'in_progress' THEN 0 WHEN 'blocked' THEN 1
218
+ WHEN 'pending' THEN 2 ELSE 3 END, priority ASC, created_at ASC LIMIT 1`,
219
+ ).get(currentChange.id)?.id : null);
220
+ if (taskId) currentTask = ops.readTask(db, taskId);
221
+ }
222
+ const evidence = {};
223
+ for (const kind of ['test', 'review', 'deliver']) {
224
+ const run = evidenceChangeId && tables.has('workflow_runs') && tables.has('workflow_steps')
225
+ ? workflowState.listWorkflows(
226
+ db, { kind, status: 'completed', change_id: evidenceChangeId, limit: 1 }, { rootDir },
227
+ )[0]
228
+ : null;
229
+ let status = 'missing';
230
+ if (run && kind === 'test') status = run.summary.passed === true ? 'pass' : 'fail';
231
+ if (run && kind === 'review') status = run.summary.verdict || 'unknown';
232
+ if (run && kind === 'deliver') {
233
+ status = run.summary.archive_status === 'archived' ? 'delivered' : 'incomplete';
234
+ }
235
+ const evidenceKey = kind === 'deliver' ? 'delivery' : kind;
236
+ evidence[evidenceKey] = run ? {
237
+ workflow_id: run.id, status, output_health: run.artifact_health.status,
238
+ report_path: run.summary.report_path || null,
239
+ } : { workflow_id: null, status: 'missing', output_health: 'missing', report_path: null };
90
240
  }
91
241
  return {
92
242
  period: { since: since || 'all-time' },
93
243
  baseline,
244
+ workflows,
245
+ current_change: currentChange,
246
+ current_task: currentTask,
247
+ evidence,
248
+ changes,
249
+ tasks: taskCounts,
250
+ sessions,
251
+ next,
94
252
  by_runtime,
95
253
  top_tasks,
96
254
  total_cost_usd,
255
+ cost: {
256
+ status: costStatus,
257
+ known_total_usd: knownTotalCost,
258
+ priced_usage_events: pricedUsageEvents,
259
+ unpriced_usage_events: unpricedUsageEvents,
260
+ },
97
261
  };
98
262
  }
99
263
 
264
+ // Backward-compatible internal export for consumers that used the old name.
265
+ const buildCostPanel = buildStatusPanel;
266
+
100
267
  function formatCost(n) {
101
268
  if (!n) return '$0.0000';
102
269
  if (n < 0.01) return `$${n.toFixed(6)}`;
@@ -107,11 +274,44 @@ function renderHuman(panel) {
107
274
  const lines = [];
108
275
  const baseline = panel.baseline;
109
276
  lines.push(
110
- `Baseline: ${baseline.mode || 'none'}/${baseline.status} · ${baseline.health === 'pass' ? 'ready' : 'blocked'}`,
277
+ `Baseline: ${baseline.mode || 'none'}/${baseline.status} · ${baseline.health === 'pass' ? 'ready' : 'blocked'}`
278
+ + (baseline.research ? ` · research=${baseline.research.id}/${baseline.research.status}/${baseline.research.output_health}` : ''),
111
279
  );
112
280
  if (baseline.blockers.length > 0) lines.push(`Baseline blockers: ${baseline.blockers.join(', ')}`);
281
+ lines.push(
282
+ `Workflows: active=${panel.workflows.active} blocked=${panel.workflows.blocked} ready=${panel.workflows.ready} health=${panel.workflows.status}`,
283
+ );
284
+ if (panel.workflows.current.length > 0) {
285
+ const current = panel.workflows.current[0];
286
+ lines.push(
287
+ `Workflow: ${current.id} (${current.kind}/${current.status}) · step=${current.current_step || 'finalize'} · outputs=${current.output_health}`,
288
+ );
289
+ }
290
+ lines.push(
291
+ `Changes: active=${panel.changes.active} blocked=${panel.changes.blocked} ready=${panel.changes.ready} archived=${panel.changes.archived}`,
292
+ );
293
+ lines.push(
294
+ `Tasks: pending=${panel.tasks.pending} in_progress=${panel.tasks.in_progress} completed=${panel.tasks.completed} blocked=${panel.tasks.blocked} stale=${panel.tasks.stale}`,
295
+ );
296
+ if (panel.current_change) {
297
+ lines.push(`Current change: ${panel.current_change.id} (${panel.current_change.status})`);
298
+ }
299
+ if (panel.current_task) lines.push(`Current task: ${panel.current_task.id} (${panel.current_task.status})`);
300
+ lines.push(
301
+ `Sessions: running=${panel.sessions.running} crashed=${panel.sessions.crashed} orphan=${panel.sessions.orphan}`,
302
+ );
303
+ lines.push(`Next: ${panel.next.action} (${panel.next.recommended_workflow})`);
304
+ lines.push(
305
+ `Evidence: test=${panel.evidence.test.status} review=${panel.evidence.review.status} delivery=${panel.evidence.delivery.status}`,
306
+ );
113
307
  lines.push(`Period: ${panel.period.since}`);
114
- lines.push(`Total cost: ${formatCost(panel.total_cost_usd)}`);
308
+ if (panel.total_cost_usd === null) {
309
+ lines.push(
310
+ `Total cost: unavailable (${panel.cost.unpriced_usage_events} unpriced usage events; known cost ${formatCost(panel.cost.known_total_usd)})`,
311
+ );
312
+ } else {
313
+ lines.push(`Total cost: ${formatCost(panel.total_cost_usd)}`);
314
+ }
115
315
  lines.push('');
116
316
  lines.push('Cost by runtime:');
117
317
  if (panel.by_runtime.length === 0) {
@@ -163,7 +363,7 @@ function dispatch(args) {
163
363
  const db = new Database(dbPath, { readonly: true });
164
364
  try {
165
365
  const rootDir = path.dirname(path.dirname(dbPath));
166
- const panel = buildCostPanel(db, { since, limit: flags.limit, rootDir });
366
+ const panel = buildStatusPanel(db, { since, limit: flags.limit, rootDir });
167
367
  if (flags.json) {
168
368
  emit({ ok: true, data: panel });
169
369
  } else {
@@ -178,6 +378,7 @@ function dispatch(args) {
178
378
  module.exports = {
179
379
  USAGE,
180
380
  dispatch,
381
+ buildStatusPanel,
181
382
  buildCostPanel,
182
383
  renderHuman,
183
384
  parseSince,
@@ -1,9 +0,0 @@
1
- {
2
- "timestamp": null,
3
- "version": null,
4
- "git_tag": null,
5
- "git_commit": null,
6
- "changelog_updated": false,
7
- "build_success": false,
8
- "pushed": false
9
- }
@@ -1,8 +0,0 @@
1
- {
2
- "passed": false,
3
- "run_count": 0,
4
- "timestamp": null,
5
- "git_commit": null,
6
- "gates": {},
7
- "blocking_issues": ["Not yet run"]
8
- }
@@ -1,54 +0,0 @@
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
-
9
- const { copyTree, writeAtomic, removeTree } = require('../file-ops.cjs');
10
-
11
- function mk(prefix = 'ubp-shared-') {
12
- return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
13
- }
14
-
15
- test('copyTree copies recursively and skips .DS_Store', () => {
16
- const src = mk();
17
- const dst = mk();
18
- try {
19
- fs.mkdirSync(path.join(src, 'nested'));
20
- fs.writeFileSync(path.join(src, 'a.txt'), 'A');
21
- fs.writeFileSync(path.join(src, '.DS_Store'), 'junk');
22
- fs.writeFileSync(path.join(src, 'nested', 'b.txt'), 'B');
23
-
24
- const files = copyTree(src, dst);
25
- assert.deepEqual(files.sort(), ['a.txt', path.join('nested', 'b.txt')]);
26
- assert.equal(fs.readFileSync(path.join(dst, 'a.txt'), 'utf8'), 'A');
27
- assert.equal(fs.readFileSync(path.join(dst, 'nested', 'b.txt'), 'utf8'), 'B');
28
- assert.ok(!fs.existsSync(path.join(dst, '.DS_Store')));
29
- } finally {
30
- removeTree(src);
31
- removeTree(dst);
32
- }
33
- });
34
-
35
- test('writeAtomic never leaves a torn write — tmp file is renamed', () => {
36
- const dir = mk();
37
- try {
38
- const target = path.join(dir, 'out', 'file.txt');
39
- writeAtomic(target, 'hello');
40
- assert.equal(fs.readFileSync(target, 'utf8'), 'hello');
41
-
42
- const siblings = fs.readdirSync(path.dirname(target));
43
- assert.deepEqual(siblings, ['file.txt']); // no .tmp-* left behind
44
- } finally {
45
- removeTree(dir);
46
- }
47
- });
48
-
49
- test('removeTree refuses to wipe filesystem root without allowRoot', () => {
50
- assert.throws(
51
- () => removeTree('/', { allowRoot: false }),
52
- /refusing to remove filesystem root/,
53
- );
54
- });
@@ -1,47 +0,0 @@
1
- 'use strict';
2
-
3
- const { test } = require('node:test');
4
- const assert = require('node:assert/strict');
5
-
6
- const { parse, serialize, lowercaseKeys, patch } = require('../frontmatter.cjs');
7
-
8
- test('parse extracts frontmatter and body; degraded inputs survive', () => {
9
- const withFm = '---\ndescription: hi\nmodel: opus\n---\n\n# Hello\n';
10
- const out = parse(withFm);
11
- assert.equal(out.fm.description, 'hi');
12
- assert.equal(out.fm.model, 'opus');
13
- assert.ok(out.body.startsWith('# Hello'));
14
-
15
- // no delimiter at all
16
- const plain = parse('# No frontmatter\n');
17
- assert.equal(plain.fm, null);
18
- assert.equal(plain.body, '# No frontmatter\n');
19
-
20
- // delimiter open but never closed — treat as plain text
21
- const unclosed = parse('---\nfoo: 1\nno closing delimiter\n');
22
- assert.equal(unclosed.fm, null);
23
- assert.ok(unclosed.body.includes('no closing delimiter'));
24
-
25
- // type guard
26
- assert.throws(() => parse(42), /expects string/);
27
- });
28
-
29
- test('serialize → parse round-trip preserves data', () => {
30
- const fm = { description: 'round-trip', tags: ['a', 'b'] };
31
- const body = '# Body\n\nparagraph\n';
32
- const text = serialize(fm, body);
33
- const { fm: got, body: gotBody } = parse(text);
34
- assert.deepEqual(got, fm);
35
- assert.equal(gotBody, body);
36
- });
37
-
38
- test('lowercaseKeys normalizes nested frontmatter (OpenCode contract)', () => {
39
- const input = { Name: 'X', TAGS: ['T1'], Nested: { Key: 1 } };
40
- const out = lowercaseKeys(input);
41
- assert.deepEqual(out, { name: 'X', tags: ['T1'], nested: { key: 1 } });
42
-
43
- // patch() should let callers transform fm in-place
44
- const patched = patch('---\nFoo: 1\n---\nbody', (fm) => ({ foo: fm.Foo }));
45
- const { fm } = parse(patched);
46
- assert.deepEqual(fm, { foo: 1 });
47
- });
@@ -1,101 +0,0 @@
1
- 'use strict';
2
-
3
- const assert = require('node:assert/strict');
4
- const fs = require('node:fs');
5
- const os = require('node:os');
6
- const path = require('node:path');
7
- const test = require('node:test');
8
-
9
- const {
10
- BEGIN_MARKER,
11
- END_MARKER,
12
- applyHandbook,
13
- mergeHandbook,
14
- renderHandbook,
15
- resolveHandbookFile,
16
- } = require('../handbook.cjs');
17
-
18
- test('renders one common contract with host-native invocation syntax', () => {
19
- const claude = renderHandbook('claude');
20
- const codex = renderHandbook('codex');
21
- const opencode = renderHandbook('opencode');
22
- const kimi = renderHandbook('kimi');
23
-
24
- assert.match(claude, /\/ultra-plan/);
25
- assert.match(codex, /\$ultra-builder-pro:ultra-plan/);
26
- assert.match(opencode, /\/ultra-plan/);
27
- assert.match(kimi, /\/ultra-builder-pro:ultra-plan/);
28
- assert.match(kimi, /Kimi Code/);
29
- assert.match(kimi, /AgentSwarm/);
30
- for (const rendered of [claude, codex, opencode, kimi]) {
31
- assert.match(rendered, /\.ultra\/state\.db/);
32
- assert.match(rendered, /Separately installed providers/);
33
- assert.match(rendered, /ultra-change/);
34
- assert.match(rendered, /ultra-doctor/);
35
- assert.match(rendered, /metadata references/);
36
- assert.match(rendered, /Context Spine boundary/);
37
- assert.match(rendered, /budgets are advisory attention signals/);
38
- assert.match(rendered, /blocks change convergence/);
39
- assert.match(rendered, /health-checked atomically at archive/);
40
- assert.match(rendered, /never traps session stop/);
41
- assert.match(rendered, /change\.breadcrumb/);
42
- assert.match(rendered, /Specification learning boundary/);
43
- assert.doesNotMatch(rendered, /\.ultra\/memory|memory\.retain|memory\.recall/);
44
- assert.match(rendered, new RegExp(BEGIN_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
45
- assert.match(rendered, new RegExp(END_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
46
- }
47
- });
48
-
49
- test('merge preserves user content and is idempotent', () => {
50
- const existing = '# User rules\n\nKeep this.\n';
51
- const once = mergeHandbook(existing, 'claude');
52
- const twice = mergeHandbook(once, 'claude');
53
-
54
- assert.match(once, /^# User rules/m);
55
- assert.match(once, /Keep this\./);
56
- assert.equal(once, twice);
57
- assert.equal(once.split(BEGIN_MARKER).length - 1, 1);
58
- });
59
-
60
- test('Codex legacy Ultra section is replaced without touching the next section', () => {
61
- const existing = [
62
- '# Handbook',
63
- '',
64
- '## Ultra Builder Pro Runtime Contract',
65
- '',
66
- 'Old memory contract under `.ultra/memory/`.',
67
- '',
68
- '## Standard Operating Workflow',
69
- '',
70
- 'Keep this workflow.',
71
- '',
72
- ].join('\n');
73
-
74
- const merged = mergeHandbook(existing, 'codex');
75
- assert.doesNotMatch(merged, /Old memory contract/);
76
- assert.match(merged, /## Standard Operating Workflow\n\nKeep this workflow\./);
77
- assert.equal(merged.split(BEGIN_MARKER).length - 1, 1);
78
- });
79
-
80
- test('apply creates a backup and reports a no-op on the second run', () => {
81
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-handbook-'));
82
- const file = path.join(root, 'AGENTS.md');
83
- fs.writeFileSync(file, '# Existing\n');
84
-
85
- const first = applyHandbook({ runtime: 'codex', file, now: '20260715T120000Z' });
86
- assert.equal(first.changed, true);
87
- assert.equal(first.backup, `${file}.ubp-backup-20260715T120000Z`);
88
- assert.equal(fs.readFileSync(first.backup, 'utf8'), '# Existing\n');
89
-
90
- const second = applyHandbook({ runtime: 'codex', file, now: '20260715T120001Z' });
91
- assert.equal(second.changed, false);
92
- assert.equal(second.backup, null);
93
- });
94
-
95
- test('default handbook paths are host-specific', () => {
96
- const homeDir = '/tmp/example-home';
97
- assert.equal(resolveHandbookFile('claude', { homeDir }), path.join(homeDir, '.claude', 'CLAUDE.md'));
98
- assert.equal(resolveHandbookFile('codex', { homeDir }), path.join(homeDir, '.codex', 'AGENTS.md'));
99
- assert.equal(resolveHandbookFile('opencode', { homeDir }), path.join(homeDir, '.config', 'opencode', 'AGENTS.md'));
100
- assert.equal(resolveHandbookFile('kimi', { homeDir }), path.join(homeDir, '.kimi-code', 'AGENTS.md'));
101
- });
@@ -1,60 +0,0 @@
1
- 'use strict';
2
-
3
- const { test } = require('node:test');
4
- const assert = require('node:assert/strict');
5
- const path = require('node:path');
6
-
7
- const {
8
- parseSkillRef,
9
- resolveSkillRef,
10
- rewriteWorkflowRefInText,
11
- resolveAssetTarget,
12
- } = require('../path-rewrite.cjs');
13
-
14
- test('parseSkillRef accepts canonical @skills/<slug>/SKILL.md only', () => {
15
- assert.deepEqual(parseSkillRef('@skills/ultra-init/SKILL.md'), {
16
- name: 'ultra-init',
17
- filename: 'SKILL.md',
18
- });
19
- assert.equal(parseSkillRef('skills/ultra-init/SKILL.md'), null); // missing @
20
- assert.equal(parseSkillRef('@skills/Bad-Case/SKILL.md'), null); // uppercase slug
21
- assert.equal(parseSkillRef(null), null);
22
- });
23
-
24
- test('resolveSkillRef routes to runtime-specific root under homeDir', () => {
25
- const home = '/Users/test';
26
- const claude = resolveSkillRef('@skills/ultra-init/SKILL.md', { runtime: 'claude', homeDir: home });
27
- const opencode = resolveSkillRef('@skills/ultra-init/SKILL.md', { runtime: 'opencode', homeDir: home });
28
- const codex = resolveSkillRef('@skills/ultra-init/SKILL.md', { runtime: 'codex', homeDir: home });
29
- const kimi = resolveSkillRef('@skills/ultra-init/SKILL.md', { runtime: 'kimi', homeDir: home });
30
-
31
- assert.equal(claude, path.join(home, '.claude/skills/ultra-init/SKILL.md'));
32
- assert.equal(opencode, path.join(home, '.config/opencode/skills/ultra-init/SKILL.md'));
33
- assert.equal(codex, path.join(home, '.agents/skills/ultra-init/SKILL.md'));
34
- assert.equal(kimi, path.join(home, '.kimi-code/skills/ultra-init/SKILL.md'));
35
-
36
- assert.throws(
37
- () => resolveSkillRef('@skills/ultra-init/SKILL.md', { runtime: 'unknown', homeDir: home }),
38
- /unknown runtime/,
39
- );
40
- });
41
-
42
- test('rewriteWorkflowRefInText + resolveAssetTarget give predictable paths', () => {
43
- const home = '/h';
44
- const cwd = '/w';
45
- const input = 'see @skills/ultra-init/SKILL.md for details';
46
- const out = rewriteWorkflowRefInText(input, { runtime: 'claude', homeDir: home });
47
- assert.equal(out, `see ${path.join(home, '.claude/skills/ultra-init/SKILL.md')} for details`);
48
-
49
- // Cover every runtime × every scope so runtimeRoot/localRoot branches hit
50
- for (const rt of ['claude', 'opencode', 'codex', 'kimi']) {
51
- const g = resolveAssetTarget('x', { runtime: rt, scope: 'global', homeDir: home, cwd });
52
- const l = resolveAssetTarget('x', { runtime: rt, scope: 'local', homeDir: home, cwd });
53
- assert.ok(g.startsWith(home));
54
- assert.ok(l.startsWith(cwd));
55
- }
56
- assert.throws(
57
- () => resolveAssetTarget('x', { runtime: 'unknown', scope: 'global', homeDir: home, cwd }),
58
- /unknown runtime/,
59
- );
60
- });