ultra-builder-pro-cli 0.13.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 (172) 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 +86 -1
  8. package/README.md +40 -26
  9. package/adapters/_shared/codex-assets.cjs +29 -165
  10. package/adapters/_shared/file-ops.cjs +6 -2
  11. package/adapters/_shared/runtime-assets.cjs +1 -0
  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 +33 -122
  16. package/agents/review-coordinator.md +9 -6
  17. package/docs/AGENT-CONTEXT.md +21 -11
  18. package/docs/ARCHITECTURE.md +41 -13
  19. package/docs/PLAN.zh-CN.md +3 -2
  20. package/docs/ROADMAP.md +9 -5
  21. package/docs/RUNTIME-COMPAT-MATRIX.md +6 -6
  22. package/docs/STATE-DB-ACCESS-POLICY.md +27 -25
  23. package/docs/WORKFLOW-LIFECYCLE.md +266 -0
  24. package/hooks/README.md +6 -1
  25. package/hooks/active_task_context.py +22 -50
  26. package/hooks/context_spine.py +61 -332
  27. package/hooks/health_check.py +29 -18
  28. package/hooks/pre_stop_check.py +28 -33
  29. package/hooks/subagent_tracker.py +54 -85
  30. package/hooks/workflow_checkpoint.py +39 -47
  31. package/hooks/workflow_context.py +4 -9
  32. package/hooks/workflow_resume.py +48 -128
  33. package/mcp-server/breadcrumb.cjs +22 -0
  34. package/mcp-server/hook-event.cjs +34 -0
  35. package/mcp-server/lib/archive-journal.cjs +17 -5
  36. package/mcp-server/lib/baseline-workflow.cjs +158 -18
  37. package/mcp-server/lib/change-workflow.cjs +550 -85
  38. package/mcp-server/lib/context-spine.cjs +242 -24
  39. package/mcp-server/lib/doctor.cjs +33 -3
  40. package/mcp-server/lib/init-project.cjs +210 -16
  41. package/mcp-server/lib/plan-store.cjs +4 -1
  42. package/mcp-server/lib/prd-parser.cjs +14 -41
  43. package/mcp-server/lib/pricing.cjs +7 -10
  44. package/mcp-server/lib/project-breadcrumb.cjs +145 -0
  45. package/mcp-server/lib/projector.cjs +57 -3
  46. package/mcp-server/lib/spec-learning.cjs +79 -2
  47. package/mcp-server/lib/state-db.cjs +145 -8
  48. package/mcp-server/lib/state-ops.cjs +160 -9
  49. package/mcp-server/lib/task-expander.cjs +9 -38
  50. package/mcp-server/lib/workflow-state.cjs +2292 -0
  51. package/mcp-server/server.cjs +182 -50
  52. package/orchestrator/daemon.cjs +1 -2
  53. package/orchestrator/dispatch-rules.cjs +5 -31
  54. package/orchestrator/planner/plan-builder.cjs +21 -12
  55. package/package.json +14 -6
  56. package/skills/cc-collab/SKILL.md +2 -2
  57. package/skills/codex-collab/SKILL.md +2 -3
  58. package/skills/ultra-change/SKILL.md +53 -68
  59. package/skills/ultra-change/references/change-contract.md +36 -0
  60. package/skills/ultra-deliver/SKILL.md +69 -79
  61. package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
  62. package/skills/ultra-dev/SKILL.md +54 -59
  63. package/skills/ultra-doctor/SKILL.md +40 -71
  64. package/skills/ultra-init/SKILL.md +96 -103
  65. package/skills/ultra-init/references/brownfield-adoption.md +2 -2
  66. package/skills/ultra-plan/SKILL.md +79 -75
  67. package/skills/ultra-plan/references/semantic-preflight.md +18 -0
  68. package/skills/ultra-research/SKILL.md +87 -60
  69. package/skills/ultra-research/references/00-problem-validation.md +36 -0
  70. package/skills/ultra-research/references/01-opportunity-discovery.md +33 -0
  71. package/skills/ultra-research/references/02-market-assessment.md +30 -0
  72. package/skills/ultra-research/references/03-competitive-landscape.md +31 -0
  73. package/skills/ultra-research/references/04-product-strategy.md +32 -0
  74. package/skills/ultra-research/references/05-assumptions-validation.md +31 -0
  75. package/skills/ultra-research/references/10-user-personas.md +29 -0
  76. package/skills/ultra-research/references/11-user-scenarios.md +28 -0
  77. package/skills/ultra-research/references/20-user-stories.md +30 -0
  78. package/skills/ultra-research/references/21-features-scope.md +29 -0
  79. package/skills/ultra-research/references/22-success-metrics.md +28 -0
  80. package/skills/ultra-research/references/30-architecture-context.md +30 -0
  81. package/skills/ultra-research/references/31-solution-strategy.md +28 -0
  82. package/skills/ultra-research/references/32-building-blocks.md +28 -0
  83. package/skills/ultra-research/references/40-deployment.md +29 -0
  84. package/skills/ultra-research/references/41-quality-risks.md +28 -0
  85. package/skills/ultra-research/references/99-synthesis.md +35 -0
  86. package/skills/ultra-review/SKILL.md +57 -73
  87. package/skills/ultra-review/references/review-modes.md +27 -0
  88. package/skills/ultra-review/references/unified-schema.md +25 -0
  89. package/skills/ultra-review/scripts/review_wait.py +40 -3
  90. package/skills/ultra-status/SKILL.md +41 -45
  91. package/skills/ultra-test/SKILL.md +48 -55
  92. package/skills/ultra-verify/SKILL.md +22 -31
  93. package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
  94. package/skills/ultra-verify/scripts/verify_wait.py +54 -54
  95. package/spec/cli-protocol.md +7 -2
  96. package/spec/mcp-tools.yaml +422 -73
  97. package/spec/schemas/baseline-reconciliation.v1.schema.json +73 -0
  98. package/spec/schemas/delivery-report.v1.schema.json +111 -0
  99. package/spec/schemas/mcp-tools.schema.json +1 -1
  100. package/spec/schemas/state-db.sql +82 -1
  101. package/spec/schemas/tasks.v4.5.schema.json +8 -1
  102. package/spec/schemas/test-report.v1.schema.json +194 -0
  103. package/ultra-tools/cli.cjs +1 -1
  104. package/ultra-tools/commands/migrate.cjs +3 -3
  105. package/ultra-tools/commands/status.cjs +213 -12
  106. package/.ultra-template/delivery-report.json +0 -9
  107. package/.ultra-template/test-report.json +0 -8
  108. package/adapters/_shared/tests/file-ops.test.cjs +0 -54
  109. package/adapters/_shared/tests/frontmatter.test.cjs +0 -47
  110. package/adapters/_shared/tests/handbook.test.cjs +0 -101
  111. package/adapters/_shared/tests/path-rewrite.test.cjs +0 -60
  112. package/adapters/_shared/tests/provenance.test.cjs +0 -133
  113. package/adapters/_shared/tests/resolve-target.test.cjs +0 -75
  114. package/adapters/_shared/tests/runtime-assets.test.cjs +0 -131
  115. package/adapters/_shared/tests/settings-merge.test.cjs +0 -85
  116. package/adapters/_shared/tests/validate.test.cjs +0 -30
  117. package/adapters/tests/claude.test.cjs +0 -137
  118. package/adapters/tests/codex-hook.test.cjs +0 -115
  119. package/adapters/tests/codex.test.cjs +0 -423
  120. package/adapters/tests/kimi-hook.test.cjs +0 -122
  121. package/adapters/tests/kimi.test.cjs +0 -259
  122. package/adapters/tests/opencode.test.cjs +0 -371
  123. package/mcp-server/lib/archive-journal.test.cjs +0 -35
  124. package/mcp-server/lib/baseline-workflow.test.cjs +0 -477
  125. package/mcp-server/lib/change-workflow.test.cjs +0 -971
  126. package/mcp-server/lib/context-spine.test.cjs +0 -93
  127. package/mcp-server/lib/doctor.test.cjs +0 -148
  128. package/mcp-server/lib/llm-client.cjs +0 -157
  129. package/mcp-server/lib/llm-client.test.cjs +0 -156
  130. package/mcp-server/lib/plan-store.test.cjs +0 -132
  131. package/mcp-server/lib/prd-parser.test.cjs +0 -171
  132. package/mcp-server/lib/runtime-state.test.cjs +0 -92
  133. package/mcp-server/lib/spec-learning.test.cjs +0 -67
  134. package/mcp-server/lib/state-db.test.cjs +0 -314
  135. package/mcp-server/lib/task-expander.test.cjs +0 -324
  136. package/mcp-server/lib/topo.test.cjs +0 -158
  137. package/mcp-server/tests/circuit-breaker.test.cjs +0 -125
  138. package/mcp-server/tests/commit-hash-flow.test.cjs +0 -117
  139. package/mcp-server/tests/concurrency-update.test.cjs +0 -96
  140. package/mcp-server/tests/concurrency.test.cjs +0 -112
  141. package/mcp-server/tests/db-ops.test.cjs +0 -121
  142. package/mcp-server/tests/init-project.test.cjs +0 -393
  143. package/mcp-server/tests/migrate.test.cjs +0 -232
  144. package/mcp-server/tests/projector.test.cjs +0 -162
  145. package/mcp-server/tests/server-contract.test.cjs +0 -1471
  146. package/mcp-server/tests/staleness.test.cjs +0 -190
  147. package/mcp-server/tests/state-db.test.cjs +0 -106
  148. package/mcp-server/tests/state-ops.test.cjs +0 -449
  149. package/mcp-server/tests/tagged-tasks.test.cjs +0 -163
  150. package/mcp-server/tests/telemetry.test.cjs +0 -219
  151. package/orchestrator/planner/expand-prompt.cjs +0 -62
  152. package/orchestrator/planner/expand-prompt.test.cjs +0 -52
  153. package/orchestrator/planner/plan-builder.test.cjs +0 -165
  154. package/orchestrator/planner/prd-prompt.cjs +0 -44
  155. package/orchestrator/planner/prd-prompt.test.cjs +0 -30
  156. package/orchestrator/tests/auto-merge.test.cjs +0 -216
  157. package/orchestrator/tests/daemon.test.cjs +0 -311
  158. package/orchestrator/tests/dispatch-rules.test.cjs +0 -124
  159. package/orchestrator/tests/parallel-orchestrator.test.cjs +0 -233
  160. package/orchestrator/tests/phase-8b-gate.test.cjs +0 -183
  161. package/orchestrator/tests/recovery.test.cjs +0 -190
  162. package/orchestrator/tests/session-runner.test.cjs +0 -314
  163. package/orchestrator/tests/skill-miner.test.cjs +0 -147
  164. package/orchestrator/tests/worktree-manager.test.cjs +0 -148
  165. package/skills/ultra-research/references/architecture.md +0 -38
  166. package/skills/ultra-research/references/problem-product.md +0 -39
  167. package/skills/ultra-research/references/quality-delivery.md +0 -34
  168. package/skills/ultra-research/references/synthesis.md +0 -38
  169. package/skills/ultra-research/references/users-scope.md +0 -40
  170. package/ultra-tools/legacy-memory.test.cjs +0 -80
  171. package/ultra-tools/status-command.test.cjs +0 -181
  172. 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
- });