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.
- package/.ultra-template/docs/research/README.md +11 -7
- package/.ultra-template/specs/architecture.md +36 -36
- package/.ultra-template/specs/discovery.md +46 -33
- package/.ultra-template/specs/product.md +38 -29
- package/.ultra-template/specs/research-distillate.md +48 -0
- package/.ultra-template/tasks/contexts/TEMPLATE.md +11 -28
- package/CHANGELOG.md +77 -1
- package/README.md +46 -24
- package/adapters/_shared/codex-assets.cjs +6 -2
- package/adapters/_shared/file-ops.cjs +6 -2
- package/adapters/_shared/handbook.cjs +2 -1
- package/adapters/claude.js +3 -2
- package/adapters/codex.js +1 -0
- package/adapters/kimi.js +3 -2
- package/adapters/opencode.js +3 -2
- package/agents/review-coordinator.md +9 -6
- package/commands/ultra-change.md +1 -1
- package/commands/ultra-plan.md +1 -1
- package/commands/ultra-research.md +1 -1
- package/commands/ultra-think.md +2 -2
- package/docs/AGENT-CONTEXT.md +43 -19
- package/docs/ARCHITECTURE.md +54 -13
- package/docs/PLAN.zh-CN.md +4 -2
- package/docs/ROADMAP.md +10 -4
- package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
- package/docs/STATE-DB-ACCESS-POLICY.md +33 -29
- package/docs/USER-HANDBOOK-CONTRACT.md +8 -2
- package/docs/WORKFLOW-LIFECYCLE.md +342 -0
- package/hooks/README.md +6 -1
- package/hooks/health_check.py +12 -3
- package/hooks/subagent_tracker.py +52 -77
- package/mcp-server/hook-event.cjs +34 -0
- package/mcp-server/lib/archive-journal.cjs +17 -5
- package/mcp-server/lib/baseline-workflow.cjs +158 -18
- package/mcp-server/lib/change-workflow.cjs +598 -85
- package/mcp-server/lib/context-spine.cjs +305 -25
- package/mcp-server/lib/decision-dialogue.cjs +713 -0
- package/mcp-server/lib/doctor.cjs +42 -3
- package/mcp-server/lib/init-project.cjs +210 -16
- package/mcp-server/lib/plan-store.cjs +4 -1
- package/mcp-server/lib/pricing.cjs +7 -10
- package/mcp-server/lib/project-breadcrumb.cjs +15 -0
- package/mcp-server/lib/projector.cjs +57 -3
- package/mcp-server/lib/spec-learning.cjs +79 -2
- package/mcp-server/lib/state-db.cjs +168 -8
- package/mcp-server/lib/state-ops.cjs +160 -9
- package/mcp-server/lib/workflow-state.cjs +2305 -0
- package/mcp-server/server.cjs +215 -15
- package/orchestrator/daemon.cjs +1 -2
- package/orchestrator/dispatch-rules.cjs +5 -31
- package/orchestrator/planner/plan-builder.cjs +21 -12
- package/package.json +13 -3
- package/skills/ultra-change/SKILL.md +94 -80
- package/skills/ultra-change/references/change-contract.md +36 -0
- package/skills/ultra-deliver/SKILL.md +76 -79
- package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
- package/skills/ultra-dev/SKILL.md +62 -59
- package/skills/ultra-doctor/SKILL.md +61 -82
- package/skills/ultra-init/SKILL.md +101 -103
- package/skills/ultra-init/references/brownfield-adoption.md +2 -2
- package/skills/ultra-plan/SKILL.md +99 -85
- package/skills/ultra-plan/references/semantic-preflight.md +18 -0
- package/skills/ultra-research/SKILL.md +102 -60
- package/skills/ultra-research/references/00-problem-validation.md +36 -0
- package/skills/ultra-research/references/01-opportunity-discovery.md +33 -0
- package/skills/ultra-research/references/02-market-assessment.md +30 -0
- package/skills/ultra-research/references/03-competitive-landscape.md +31 -0
- package/skills/ultra-research/references/04-product-strategy.md +32 -0
- package/skills/ultra-research/references/05-assumptions-validation.md +31 -0
- package/skills/ultra-research/references/10-user-personas.md +29 -0
- package/skills/ultra-research/references/11-user-scenarios.md +28 -0
- package/skills/ultra-research/references/20-user-stories.md +30 -0
- package/skills/ultra-research/references/21-features-scope.md +29 -0
- package/skills/ultra-research/references/22-success-metrics.md +28 -0
- package/skills/ultra-research/references/30-architecture-context.md +30 -0
- package/skills/ultra-research/references/31-solution-strategy.md +28 -0
- package/skills/ultra-research/references/32-building-blocks.md +28 -0
- package/skills/ultra-research/references/40-deployment.md +29 -0
- package/skills/ultra-research/references/41-quality-risks.md +28 -0
- package/skills/ultra-research/references/99-synthesis.md +35 -0
- package/skills/ultra-review/SKILL.md +63 -73
- package/skills/ultra-review/references/review-modes.md +27 -0
- package/skills/ultra-review/references/unified-schema.md +25 -0
- package/skills/ultra-review/scripts/review_wait.py +40 -3
- package/skills/ultra-status/SKILL.md +41 -41
- package/skills/ultra-test/SKILL.md +54 -55
- package/skills/ultra-think/SKILL.md +26 -12
- package/skills/ultra-think/references/decision-dialogue.md +130 -0
- package/skills/ultra-verify/SKILL.md +1 -1
- package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
- package/spec/cli-protocol.md +17 -3
- package/spec/mcp-tools.yaml +683 -64
- package/spec/schemas/baseline-reconciliation.v1.schema.json +73 -0
- package/spec/schemas/delivery-report.v1.schema.json +111 -0
- package/spec/schemas/mcp-tools.schema.json +1 -1
- package/spec/schemas/state-db.sql +140 -1
- package/spec/schemas/tasks.v4.5.schema.json +8 -1
- package/spec/schemas/test-report.v1.schema.json +194 -0
- package/ultra-tools/cli.cjs +1 -1
- package/ultra-tools/commands/migrate.cjs +3 -3
- package/ultra-tools/commands/status.cjs +228 -12
- package/.ultra-template/delivery-report.json +0 -9
- package/.ultra-template/test-report.json +0 -8
- package/adapters/_shared/tests/file-ops.test.cjs +0 -54
- package/adapters/_shared/tests/frontmatter.test.cjs +0 -47
- package/adapters/_shared/tests/handbook.test.cjs +0 -101
- package/adapters/_shared/tests/path-rewrite.test.cjs +0 -60
- package/adapters/_shared/tests/provenance.test.cjs +0 -133
- package/adapters/_shared/tests/resolve-target.test.cjs +0 -75
- package/adapters/_shared/tests/runtime-assets.test.cjs +0 -149
- package/adapters/_shared/tests/settings-merge.test.cjs +0 -85
- package/adapters/_shared/tests/validate.test.cjs +0 -30
- package/adapters/tests/claude.test.cjs +0 -138
- package/adapters/tests/codex-hook.test.cjs +0 -142
- package/adapters/tests/codex.test.cjs +0 -430
- package/adapters/tests/kimi-hook.test.cjs +0 -149
- package/adapters/tests/kimi.test.cjs +0 -259
- package/adapters/tests/opencode.test.cjs +0 -357
- package/mcp-server/lib/archive-journal.test.cjs +0 -35
- package/mcp-server/lib/baseline-workflow.test.cjs +0 -477
- package/mcp-server/lib/change-workflow.test.cjs +0 -971
- package/mcp-server/lib/context-spine.test.cjs +0 -93
- package/mcp-server/lib/doctor.test.cjs +0 -148
- package/mcp-server/lib/plan-store.test.cjs +0 -132
- package/mcp-server/lib/prd-parser.test.cjs +0 -82
- package/mcp-server/lib/project-breadcrumb.test.cjs +0 -112
- package/mcp-server/lib/runtime-state.test.cjs +0 -92
- package/mcp-server/lib/spec-learning.test.cjs +0 -67
- package/mcp-server/lib/state-db.test.cjs +0 -314
- package/mcp-server/lib/task-expander.test.cjs +0 -169
- package/mcp-server/lib/topo.test.cjs +0 -158
- package/mcp-server/tests/circuit-breaker.test.cjs +0 -125
- package/mcp-server/tests/commit-hash-flow.test.cjs +0 -117
- package/mcp-server/tests/concurrency-update.test.cjs +0 -96
- package/mcp-server/tests/concurrency.test.cjs +0 -112
- package/mcp-server/tests/db-ops.test.cjs +0 -121
- package/mcp-server/tests/init-project.test.cjs +0 -393
- package/mcp-server/tests/migrate.test.cjs +0 -232
- package/mcp-server/tests/projector.test.cjs +0 -162
- package/mcp-server/tests/server-contract.test.cjs +0 -1467
- package/mcp-server/tests/staleness.test.cjs +0 -190
- package/mcp-server/tests/state-db.test.cjs +0 -106
- package/mcp-server/tests/state-ops.test.cjs +0 -449
- package/mcp-server/tests/tagged-tasks.test.cjs +0 -163
- package/mcp-server/tests/telemetry.test.cjs +0 -219
- package/orchestrator/planner/plan-builder.test.cjs +0 -165
- package/orchestrator/tests/auto-merge.test.cjs +0 -216
- package/orchestrator/tests/daemon.test.cjs +0 -311
- package/orchestrator/tests/dispatch-rules.test.cjs +0 -124
- package/orchestrator/tests/parallel-orchestrator.test.cjs +0 -233
- package/orchestrator/tests/phase-8b-gate.test.cjs +0 -183
- package/orchestrator/tests/recovery.test.cjs +0 -190
- package/orchestrator/tests/session-runner.test.cjs +0 -314
- package/orchestrator/tests/skill-miner.test.cjs +0 -147
- package/orchestrator/tests/worktree-manager.test.cjs +0 -148
- package/skills/ultra-research/references/architecture.md +0 -38
- package/skills/ultra-research/references/problem-product.md +0 -39
- package/skills/ultra-research/references/quality-delivery.md +0 -34
- package/skills/ultra-research/references/synthesis.md +0 -38
- package/skills/ultra-research/references/users-scope.md +0 -40
- package/ultra-tools/legacy-memory.test.cjs +0 -80
- package/ultra-tools/status-command.test.cjs +0 -181
- package/ultra-tools/system-command.test.cjs +0 -309
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
//
|
|
3
|
+
// Ultra project status panel.
|
|
4
4
|
//
|
|
5
|
-
// Reads telemetry
|
|
6
|
-
//
|
|
7
|
-
// Does not modify state
|
|
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
|
|
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
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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 =
|
|
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,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
|
-
});
|