ultra-builder-pro-cli 0.14.0 → 0.16.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 (163) 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 +77 -1
  8. package/README.md +46 -24
  9. package/adapters/_shared/codex-assets.cjs +6 -2
  10. package/adapters/_shared/file-ops.cjs +6 -2
  11. package/adapters/_shared/handbook.cjs +2 -1
  12. package/adapters/claude.js +3 -2
  13. package/adapters/codex.js +1 -0
  14. package/adapters/kimi.js +3 -2
  15. package/adapters/opencode.js +3 -2
  16. package/agents/review-coordinator.md +9 -6
  17. package/commands/ultra-change.md +1 -1
  18. package/commands/ultra-plan.md +1 -1
  19. package/commands/ultra-research.md +1 -1
  20. package/commands/ultra-think.md +2 -2
  21. package/docs/AGENT-CONTEXT.md +43 -19
  22. package/docs/ARCHITECTURE.md +54 -13
  23. package/docs/PLAN.zh-CN.md +4 -2
  24. package/docs/ROADMAP.md +10 -4
  25. package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
  26. package/docs/STATE-DB-ACCESS-POLICY.md +33 -29
  27. package/docs/USER-HANDBOOK-CONTRACT.md +8 -2
  28. package/docs/WORKFLOW-LIFECYCLE.md +342 -0
  29. package/hooks/README.md +6 -1
  30. package/hooks/health_check.py +12 -3
  31. package/hooks/subagent_tracker.py +52 -77
  32. package/mcp-server/hook-event.cjs +34 -0
  33. package/mcp-server/lib/archive-journal.cjs +17 -5
  34. package/mcp-server/lib/baseline-workflow.cjs +158 -18
  35. package/mcp-server/lib/change-workflow.cjs +598 -85
  36. package/mcp-server/lib/context-spine.cjs +305 -25
  37. package/mcp-server/lib/decision-dialogue.cjs +713 -0
  38. package/mcp-server/lib/doctor.cjs +42 -3
  39. package/mcp-server/lib/init-project.cjs +210 -16
  40. package/mcp-server/lib/plan-store.cjs +4 -1
  41. package/mcp-server/lib/pricing.cjs +7 -10
  42. package/mcp-server/lib/project-breadcrumb.cjs +15 -0
  43. package/mcp-server/lib/projector.cjs +57 -3
  44. package/mcp-server/lib/spec-learning.cjs +79 -2
  45. package/mcp-server/lib/state-db.cjs +168 -8
  46. package/mcp-server/lib/state-ops.cjs +160 -9
  47. package/mcp-server/lib/workflow-state.cjs +2305 -0
  48. package/mcp-server/server.cjs +215 -15
  49. package/orchestrator/daemon.cjs +1 -2
  50. package/orchestrator/dispatch-rules.cjs +5 -31
  51. package/orchestrator/planner/plan-builder.cjs +21 -12
  52. package/package.json +13 -3
  53. package/skills/ultra-change/SKILL.md +94 -80
  54. package/skills/ultra-change/references/change-contract.md +36 -0
  55. package/skills/ultra-deliver/SKILL.md +76 -79
  56. package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
  57. package/skills/ultra-dev/SKILL.md +62 -59
  58. package/skills/ultra-doctor/SKILL.md +61 -82
  59. package/skills/ultra-init/SKILL.md +101 -103
  60. package/skills/ultra-init/references/brownfield-adoption.md +2 -2
  61. package/skills/ultra-plan/SKILL.md +99 -85
  62. package/skills/ultra-plan/references/semantic-preflight.md +18 -0
  63. package/skills/ultra-research/SKILL.md +102 -60
  64. package/skills/ultra-research/references/00-problem-validation.md +36 -0
  65. package/skills/ultra-research/references/01-opportunity-discovery.md +33 -0
  66. package/skills/ultra-research/references/02-market-assessment.md +30 -0
  67. package/skills/ultra-research/references/03-competitive-landscape.md +31 -0
  68. package/skills/ultra-research/references/04-product-strategy.md +32 -0
  69. package/skills/ultra-research/references/05-assumptions-validation.md +31 -0
  70. package/skills/ultra-research/references/10-user-personas.md +29 -0
  71. package/skills/ultra-research/references/11-user-scenarios.md +28 -0
  72. package/skills/ultra-research/references/20-user-stories.md +30 -0
  73. package/skills/ultra-research/references/21-features-scope.md +29 -0
  74. package/skills/ultra-research/references/22-success-metrics.md +28 -0
  75. package/skills/ultra-research/references/30-architecture-context.md +30 -0
  76. package/skills/ultra-research/references/31-solution-strategy.md +28 -0
  77. package/skills/ultra-research/references/32-building-blocks.md +28 -0
  78. package/skills/ultra-research/references/40-deployment.md +29 -0
  79. package/skills/ultra-research/references/41-quality-risks.md +28 -0
  80. package/skills/ultra-research/references/99-synthesis.md +35 -0
  81. package/skills/ultra-review/SKILL.md +63 -73
  82. package/skills/ultra-review/references/review-modes.md +27 -0
  83. package/skills/ultra-review/references/unified-schema.md +25 -0
  84. package/skills/ultra-review/scripts/review_wait.py +40 -3
  85. package/skills/ultra-status/SKILL.md +41 -41
  86. package/skills/ultra-test/SKILL.md +54 -55
  87. package/skills/ultra-think/SKILL.md +26 -12
  88. package/skills/ultra-think/references/decision-dialogue.md +130 -0
  89. package/skills/ultra-verify/SKILL.md +1 -1
  90. package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
  91. package/spec/cli-protocol.md +17 -3
  92. package/spec/mcp-tools.yaml +683 -64
  93. package/spec/schemas/baseline-reconciliation.v1.schema.json +73 -0
  94. package/spec/schemas/delivery-report.v1.schema.json +111 -0
  95. package/spec/schemas/mcp-tools.schema.json +1 -1
  96. package/spec/schemas/state-db.sql +140 -1
  97. package/spec/schemas/tasks.v4.5.schema.json +8 -1
  98. package/spec/schemas/test-report.v1.schema.json +194 -0
  99. package/ultra-tools/cli.cjs +1 -1
  100. package/ultra-tools/commands/migrate.cjs +3 -3
  101. package/ultra-tools/commands/status.cjs +228 -12
  102. package/.ultra-template/delivery-report.json +0 -9
  103. package/.ultra-template/test-report.json +0 -8
  104. package/adapters/_shared/tests/file-ops.test.cjs +0 -54
  105. package/adapters/_shared/tests/frontmatter.test.cjs +0 -47
  106. package/adapters/_shared/tests/handbook.test.cjs +0 -101
  107. package/adapters/_shared/tests/path-rewrite.test.cjs +0 -60
  108. package/adapters/_shared/tests/provenance.test.cjs +0 -133
  109. package/adapters/_shared/tests/resolve-target.test.cjs +0 -75
  110. package/adapters/_shared/tests/runtime-assets.test.cjs +0 -149
  111. package/adapters/_shared/tests/settings-merge.test.cjs +0 -85
  112. package/adapters/_shared/tests/validate.test.cjs +0 -30
  113. package/adapters/tests/claude.test.cjs +0 -138
  114. package/adapters/tests/codex-hook.test.cjs +0 -142
  115. package/adapters/tests/codex.test.cjs +0 -430
  116. package/adapters/tests/kimi-hook.test.cjs +0 -149
  117. package/adapters/tests/kimi.test.cjs +0 -259
  118. package/adapters/tests/opencode.test.cjs +0 -357
  119. package/mcp-server/lib/archive-journal.test.cjs +0 -35
  120. package/mcp-server/lib/baseline-workflow.test.cjs +0 -477
  121. package/mcp-server/lib/change-workflow.test.cjs +0 -971
  122. package/mcp-server/lib/context-spine.test.cjs +0 -93
  123. package/mcp-server/lib/doctor.test.cjs +0 -148
  124. package/mcp-server/lib/plan-store.test.cjs +0 -132
  125. package/mcp-server/lib/prd-parser.test.cjs +0 -82
  126. package/mcp-server/lib/project-breadcrumb.test.cjs +0 -112
  127. package/mcp-server/lib/runtime-state.test.cjs +0 -92
  128. package/mcp-server/lib/spec-learning.test.cjs +0 -67
  129. package/mcp-server/lib/state-db.test.cjs +0 -314
  130. package/mcp-server/lib/task-expander.test.cjs +0 -169
  131. package/mcp-server/lib/topo.test.cjs +0 -158
  132. package/mcp-server/tests/circuit-breaker.test.cjs +0 -125
  133. package/mcp-server/tests/commit-hash-flow.test.cjs +0 -117
  134. package/mcp-server/tests/concurrency-update.test.cjs +0 -96
  135. package/mcp-server/tests/concurrency.test.cjs +0 -112
  136. package/mcp-server/tests/db-ops.test.cjs +0 -121
  137. package/mcp-server/tests/init-project.test.cjs +0 -393
  138. package/mcp-server/tests/migrate.test.cjs +0 -232
  139. package/mcp-server/tests/projector.test.cjs +0 -162
  140. package/mcp-server/tests/server-contract.test.cjs +0 -1467
  141. package/mcp-server/tests/staleness.test.cjs +0 -190
  142. package/mcp-server/tests/state-db.test.cjs +0 -106
  143. package/mcp-server/tests/state-ops.test.cjs +0 -449
  144. package/mcp-server/tests/tagged-tasks.test.cjs +0 -163
  145. package/mcp-server/tests/telemetry.test.cjs +0 -219
  146. package/orchestrator/planner/plan-builder.test.cjs +0 -165
  147. package/orchestrator/tests/auto-merge.test.cjs +0 -216
  148. package/orchestrator/tests/daemon.test.cjs +0 -311
  149. package/orchestrator/tests/dispatch-rules.test.cjs +0 -124
  150. package/orchestrator/tests/parallel-orchestrator.test.cjs +0 -233
  151. package/orchestrator/tests/phase-8b-gate.test.cjs +0 -183
  152. package/orchestrator/tests/recovery.test.cjs +0 -190
  153. package/orchestrator/tests/session-runner.test.cjs +0 -314
  154. package/orchestrator/tests/skill-miner.test.cjs +0 -147
  155. package/orchestrator/tests/worktree-manager.test.cjs +0 -148
  156. package/skills/ultra-research/references/architecture.md +0 -38
  157. package/skills/ultra-research/references/problem-product.md +0 -39
  158. package/skills/ultra-research/references/quality-delivery.md +0 -34
  159. package/skills/ultra-research/references/synthesis.md +0 -38
  160. package/skills/ultra-research/references/users-scope.md +0 -40
  161. package/ultra-tools/legacy-memory.test.cjs +0 -80
  162. package/ultra-tools/status-command.test.cjs +0 -181
  163. package/ultra-tools/system-command.test.cjs +0 -309
@@ -1,20 +1,24 @@
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 decisionDialogue = require('../../mcp-server/lib/decision-dialogue.cjs');
16
+ const { readBreadcrumb } = require('../../mcp-server/lib/context-spine.cjs');
14
17
 
15
18
  const USAGE = `ultra-tools status [flags]
16
19
 
17
- Reads telemetry from .ultra/state.db and prints a cost + activity summary.
20
+ Reads .ultra/state.db and prints baseline, workflow, task, session, route, and
21
+ cost-observability status.
18
22
 
19
23
  FLAGS:
20
24
  --cost include per-runtime + top-tasks cost panel (default on)
@@ -65,10 +69,25 @@ function parseFlags(args) {
65
69
  return out;
66
70
  }
67
71
 
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);
72
+ function buildStatusPanel(db, { since = null, limit = 3, rootDir = process.cwd() } = {}) {
73
+ const tables = new Set(db.prepare(
74
+ "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'",
75
+ ).all().map((row) => row.name));
76
+ const by_runtime = tables.has('telemetry') && tables.has('sessions')
77
+ ? ops.aggregateTelemetryByRuntime(db, { since }) : [];
78
+ const top_tasks = tables.has('telemetry') && tables.has('sessions')
79
+ ? ops.aggregateTelemetryByTask(db, { since, limit }) : [];
80
+ const knownTotalCost = by_runtime.reduce((acc, r) => acc + (r.cost_usd || 0), 0);
81
+ const unpricedUsageEvents = by_runtime.reduce(
82
+ (acc, row) => acc + Number(row.unpriced_usage_events || 0), 0,
83
+ );
84
+ const pricedUsageEvents = by_runtime.reduce(
85
+ (acc, row) => acc + Number(row.priced_usage_events || 0), 0,
86
+ );
87
+ const costStatus = unpricedUsageEvents > 0
88
+ ? (pricedUsageEvents > 0 ? 'partial' : 'unavailable')
89
+ : (pricedUsageEvents > 0 ? 'complete' : 'none');
90
+ const total_cost_usd = unpricedUsageEvents > 0 ? null : knownTotalCost;
72
91
  const baselineHealth = baselines.inspectBaseline(db, { rootDir });
73
92
  const current = baselineHealth.baseline;
74
93
  const baselineStatus = current?.status
@@ -87,16 +106,172 @@ function buildCostPanel(db, { since = null, limit = 3, rootDir = process.cwd() }
87
106
  baseline.repository_branch = current.repository_branch || null;
88
107
  baseline.worktree_state = current.worktree_state || 'unavailable';
89
108
  baseline.gaps = baselines.summarizeGaps(current.gaps);
109
+ if (current.research_run_id && tables.has('workflow_runs') && tables.has('workflow_steps')) {
110
+ const research = workflowState.readWorkflow(db, current.research_run_id, { rootDir });
111
+ baseline.research = research ? {
112
+ id: research.id,
113
+ status: research.status,
114
+ mode: research.mode,
115
+ output_health: research.artifact_health.status,
116
+ blockers: research.artifact_health.blockers,
117
+ } : {
118
+ id: current.research_run_id, status: 'missing', mode: null,
119
+ output_health: 'fail', blockers: ['BASELINE_RESEARCH_RECORD_INVALID'],
120
+ };
121
+ }
122
+ }
123
+ const countStatuses = (table, statuses, where = '') => {
124
+ if (!tables.has(table)) {
125
+ return {
126
+ status: 'unavailable',
127
+ ...Object.fromEntries(statuses.map((status) => [status, 0])),
128
+ };
129
+ }
130
+ const rows = db.prepare(
131
+ `SELECT status, COUNT(*) AS count FROM ${table} ${where} GROUP BY status`,
132
+ ).all();
133
+ const counts = Object.fromEntries(statuses.map((status) => [status, 0]));
134
+ for (const row of rows) if (row.status in counts) counts[row.status] = Number(row.count);
135
+ return counts;
136
+ };
137
+ const taskCounts = countStatuses(
138
+ 'tasks', ['pending', 'in_progress', 'completed', 'blocked', 'expanded'],
139
+ );
140
+ taskCounts.stale = tables.has('tasks')
141
+ ? Number(db.prepare('SELECT COUNT(*) AS count FROM tasks WHERE stale = 1').get().count) : 0;
142
+ const sessions = countStatuses('sessions', ['running', 'completed', 'crashed', 'orphan']);
143
+ const changes = countStatuses('changes', ['active', 'blocked', 'ready', 'archived', 'cancelled']);
144
+ let workflows;
145
+ try {
146
+ if (!tables.has('workflow_runs') || !tables.has('workflow_steps')) {
147
+ workflows = {
148
+ status: 'unavailable', active: 0, blocked: 0, ready: 0,
149
+ stale_outputs: [], historical_stale_outputs: [],
150
+ terminal_authority_runs: [], untracked_active_changes: [],
151
+ };
152
+ } else {
153
+ workflows = workflowState.inspectWorkflowHealth(db, { rootDir });
154
+ workflows.current = workflowState.listWorkflows(db, { limit: 500 }, { rootDir })
155
+ .filter((run) => ['active', 'blocked', 'ready'].includes(run.status))
156
+ .map((run) => ({
157
+ id: run.id, kind: run.kind, status: run.status, current_step: run.current_step,
158
+ baseline_id: run.baseline_id, change_id: run.change_id, task_id: run.task_id,
159
+ blockers: run.blockers, output_health: run.artifact_health.status,
160
+ }));
161
+ }
162
+ } catch (error) {
163
+ workflows = {
164
+ status: 'fail', active: 0, blocked: 0, ready: 0,
165
+ stale_outputs: [], historical_stale_outputs: [],
166
+ terminal_authority_runs: [], untracked_active_changes: [],
167
+ error: error.code || error.message,
168
+ };
169
+ }
170
+ if (!Array.isArray(workflows.current)) workflows.current = [];
171
+ const decisions = tables.has('decision_threads') && tables.has('decision_items')
172
+ ? decisionDialogue.inspectDecisionHealth(db, { rootDir })
173
+ : {
174
+ status: 'unavailable', active: 0, awaiting_owner: 0, checkpoint_ready: 0,
175
+ deferred_blocking: 0, stale_artifacts: [], current: null, current_thread_id: null,
176
+ };
177
+ let next;
178
+ let breadcrumb = null;
179
+ try {
180
+ if (!tables.has('baselines')) {
181
+ next = {
182
+ action: 'Resume project initialization to migrate and establish baseline authority.',
183
+ recommended_workflow: 'ultra-init', readiness: 'blocked',
184
+ blockers: ['BASELINE_SCHEMA_MIGRATION_REQUIRED'], warnings: [],
185
+ };
186
+ } else {
187
+ breadcrumb = readBreadcrumb(db, {}, { rootDir });
188
+ next = {
189
+ action: breadcrumb.next_action,
190
+ recommended_workflow: breadcrumb.recommended_workflow,
191
+ readiness: breadcrumb.readiness,
192
+ blockers: breadcrumb.blockers,
193
+ warnings: breadcrumb.warnings,
194
+ change_id: breadcrumb.change_id || null,
195
+ task_id: breadcrumb.task_id || null,
196
+ workflow: breadcrumb.workflow || null,
197
+ };
198
+ }
199
+ } catch (error) {
200
+ next = {
201
+ action: 'Run ultra-doctor to inspect authoritative state.',
202
+ recommended_workflow: 'ultra-doctor', readiness: 'blocked',
203
+ blockers: [`STATUS_AUTHORITY_UNAVAILABLE:${error.code || 'unknown'}`], warnings: [],
204
+ };
205
+ }
206
+
207
+ const currentChange = tables.has('changes')
208
+ ? db.prepare(
209
+ `SELECT id, title, kind, status, intent, base_commit, artifact_root, updated_at
210
+ FROM changes
211
+ WHERE status IN ('active', 'blocked', 'ready')
212
+ ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
213
+ ).get() || null
214
+ : null;
215
+ const evidenceChangeId = breadcrumb?.change_id || currentChange?.id
216
+ || (tables.has('changes') ? db.prepare(
217
+ `SELECT id FROM changes WHERE status = 'archived'
218
+ ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
219
+ ).get()?.id : null);
220
+ let currentTask = null;
221
+ if (tables.has('tasks')) {
222
+ const taskId = breadcrumb?.task_id || (currentChange?.id ? db.prepare(
223
+ `SELECT id FROM tasks WHERE change_id = ?
224
+ ORDER BY CASE status WHEN 'in_progress' THEN 0 WHEN 'blocked' THEN 1
225
+ WHEN 'pending' THEN 2 ELSE 3 END, priority ASC, created_at ASC LIMIT 1`,
226
+ ).get(currentChange.id)?.id : null);
227
+ if (taskId) currentTask = ops.readTask(db, taskId);
228
+ }
229
+ const evidence = {};
230
+ for (const kind of ['test', 'review', 'deliver']) {
231
+ const run = evidenceChangeId && tables.has('workflow_runs') && tables.has('workflow_steps')
232
+ ? workflowState.listWorkflows(
233
+ db, { kind, status: 'completed', change_id: evidenceChangeId, limit: 1 }, { rootDir },
234
+ )[0]
235
+ : null;
236
+ let status = 'missing';
237
+ if (run && kind === 'test') status = run.summary.passed === true ? 'pass' : 'fail';
238
+ if (run && kind === 'review') status = run.summary.verdict || 'unknown';
239
+ if (run && kind === 'deliver') {
240
+ status = run.summary.archive_status === 'archived' ? 'delivered' : 'incomplete';
241
+ }
242
+ const evidenceKey = kind === 'deliver' ? 'delivery' : kind;
243
+ evidence[evidenceKey] = run ? {
244
+ workflow_id: run.id, status, output_health: run.artifact_health.status,
245
+ report_path: run.summary.report_path || null,
246
+ } : { workflow_id: null, status: 'missing', output_health: 'missing', report_path: null };
90
247
  }
91
248
  return {
92
249
  period: { since: since || 'all-time' },
93
250
  baseline,
251
+ workflows,
252
+ decisions,
253
+ current_change: currentChange,
254
+ current_task: currentTask,
255
+ evidence,
256
+ changes,
257
+ tasks: taskCounts,
258
+ sessions,
259
+ next,
94
260
  by_runtime,
95
261
  top_tasks,
96
262
  total_cost_usd,
263
+ cost: {
264
+ status: costStatus,
265
+ known_total_usd: knownTotalCost,
266
+ priced_usage_events: pricedUsageEvents,
267
+ unpriced_usage_events: unpricedUsageEvents,
268
+ },
97
269
  };
98
270
  }
99
271
 
272
+ // Backward-compatible internal export for consumers that used the old name.
273
+ const buildCostPanel = buildStatusPanel;
274
+
100
275
  function formatCost(n) {
101
276
  if (!n) return '$0.0000';
102
277
  if (n < 0.01) return `$${n.toFixed(6)}`;
@@ -107,11 +282,51 @@ function renderHuman(panel) {
107
282
  const lines = [];
108
283
  const baseline = panel.baseline;
109
284
  lines.push(
110
- `Baseline: ${baseline.mode || 'none'}/${baseline.status} · ${baseline.health === 'pass' ? 'ready' : 'blocked'}`,
285
+ `Baseline: ${baseline.mode || 'none'}/${baseline.status} · ${baseline.health === 'pass' ? 'ready' : 'blocked'}`
286
+ + (baseline.research ? ` · research=${baseline.research.id}/${baseline.research.status}/${baseline.research.output_health}` : ''),
111
287
  );
112
288
  if (baseline.blockers.length > 0) lines.push(`Baseline blockers: ${baseline.blockers.join(', ')}`);
289
+ lines.push(
290
+ `Workflows: active=${panel.workflows.active} blocked=${panel.workflows.blocked} ready=${panel.workflows.ready} health=${panel.workflows.status}`,
291
+ );
292
+ if (panel.workflows.current.length > 0) {
293
+ const current = panel.workflows.current[0];
294
+ lines.push(
295
+ `Workflow: ${current.id} (${current.kind}/${current.status}) · step=${current.current_step || 'finalize'} · outputs=${current.output_health}`,
296
+ );
297
+ }
298
+ if (panel.decisions.current) {
299
+ lines.push(
300
+ `Decision: ${panel.decisions.current.id} (${panel.decisions.current.phase}) — ${panel.decisions.current.question}`,
301
+ );
302
+ } else if (panel.decisions.checkpoint_ready > 0) {
303
+ lines.push(`Decision checkpoint: ${panel.decisions.current_thread_id} awaits confirmation`);
304
+ }
305
+ lines.push(
306
+ `Changes: active=${panel.changes.active} blocked=${panel.changes.blocked} ready=${panel.changes.ready} archived=${panel.changes.archived}`,
307
+ );
308
+ lines.push(
309
+ `Tasks: pending=${panel.tasks.pending} in_progress=${panel.tasks.in_progress} completed=${panel.tasks.completed} blocked=${panel.tasks.blocked} stale=${panel.tasks.stale}`,
310
+ );
311
+ if (panel.current_change) {
312
+ lines.push(`Current change: ${panel.current_change.id} (${panel.current_change.status})`);
313
+ }
314
+ if (panel.current_task) lines.push(`Current task: ${panel.current_task.id} (${panel.current_task.status})`);
315
+ lines.push(
316
+ `Sessions: running=${panel.sessions.running} crashed=${panel.sessions.crashed} orphan=${panel.sessions.orphan}`,
317
+ );
318
+ lines.push(`Next: ${panel.next.action} (${panel.next.recommended_workflow})`);
319
+ lines.push(
320
+ `Evidence: test=${panel.evidence.test.status} review=${panel.evidence.review.status} delivery=${panel.evidence.delivery.status}`,
321
+ );
113
322
  lines.push(`Period: ${panel.period.since}`);
114
- lines.push(`Total cost: ${formatCost(panel.total_cost_usd)}`);
323
+ if (panel.total_cost_usd === null) {
324
+ lines.push(
325
+ `Total cost: unavailable (${panel.cost.unpriced_usage_events} unpriced usage events; known cost ${formatCost(panel.cost.known_total_usd)})`,
326
+ );
327
+ } else {
328
+ lines.push(`Total cost: ${formatCost(panel.total_cost_usd)}`);
329
+ }
115
330
  lines.push('');
116
331
  lines.push('Cost by runtime:');
117
332
  if (panel.by_runtime.length === 0) {
@@ -163,7 +378,7 @@ function dispatch(args) {
163
378
  const db = new Database(dbPath, { readonly: true });
164
379
  try {
165
380
  const rootDir = path.dirname(path.dirname(dbPath));
166
- const panel = buildCostPanel(db, { since, limit: flags.limit, rootDir });
381
+ const panel = buildStatusPanel(db, { since, limit: flags.limit, rootDir });
167
382
  if (flags.json) {
168
383
  emit({ ok: true, data: panel });
169
384
  } else {
@@ -178,6 +393,7 @@ function dispatch(args) {
178
393
  module.exports = {
179
394
  USAGE,
180
395
  dispatch,
396
+ buildStatusPanel,
181
397
  buildCostPanel,
182
398
  renderHuman,
183
399
  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
- });