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.
- 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 +86 -1
- package/README.md +40 -26
- package/adapters/_shared/codex-assets.cjs +29 -165
- package/adapters/_shared/file-ops.cjs +6 -2
- package/adapters/_shared/runtime-assets.cjs +1 -0
- package/adapters/claude.js +3 -2
- package/adapters/codex.js +1 -0
- package/adapters/kimi.js +3 -2
- package/adapters/opencode.js +33 -122
- package/agents/review-coordinator.md +9 -6
- package/docs/AGENT-CONTEXT.md +21 -11
- package/docs/ARCHITECTURE.md +41 -13
- package/docs/PLAN.zh-CN.md +3 -2
- package/docs/ROADMAP.md +9 -5
- package/docs/RUNTIME-COMPAT-MATRIX.md +6 -6
- package/docs/STATE-DB-ACCESS-POLICY.md +27 -25
- package/docs/WORKFLOW-LIFECYCLE.md +266 -0
- package/hooks/README.md +6 -1
- package/hooks/active_task_context.py +22 -50
- package/hooks/context_spine.py +61 -332
- package/hooks/health_check.py +29 -18
- package/hooks/pre_stop_check.py +28 -33
- package/hooks/subagent_tracker.py +54 -85
- package/hooks/workflow_checkpoint.py +39 -47
- package/hooks/workflow_context.py +4 -9
- package/hooks/workflow_resume.py +48 -128
- package/mcp-server/breadcrumb.cjs +22 -0
- 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 +550 -85
- package/mcp-server/lib/context-spine.cjs +242 -24
- package/mcp-server/lib/doctor.cjs +33 -3
- package/mcp-server/lib/init-project.cjs +210 -16
- package/mcp-server/lib/plan-store.cjs +4 -1
- package/mcp-server/lib/prd-parser.cjs +14 -41
- package/mcp-server/lib/pricing.cjs +7 -10
- package/mcp-server/lib/project-breadcrumb.cjs +145 -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 +145 -8
- package/mcp-server/lib/state-ops.cjs +160 -9
- package/mcp-server/lib/task-expander.cjs +9 -38
- package/mcp-server/lib/workflow-state.cjs +2292 -0
- package/mcp-server/server.cjs +182 -50
- package/orchestrator/daemon.cjs +1 -2
- package/orchestrator/dispatch-rules.cjs +5 -31
- package/orchestrator/planner/plan-builder.cjs +21 -12
- package/package.json +14 -6
- package/skills/cc-collab/SKILL.md +2 -2
- package/skills/codex-collab/SKILL.md +2 -3
- package/skills/ultra-change/SKILL.md +53 -68
- package/skills/ultra-change/references/change-contract.md +36 -0
- package/skills/ultra-deliver/SKILL.md +69 -79
- package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
- package/skills/ultra-dev/SKILL.md +54 -59
- package/skills/ultra-doctor/SKILL.md +40 -71
- package/skills/ultra-init/SKILL.md +96 -103
- package/skills/ultra-init/references/brownfield-adoption.md +2 -2
- package/skills/ultra-plan/SKILL.md +79 -75
- package/skills/ultra-plan/references/semantic-preflight.md +18 -0
- package/skills/ultra-research/SKILL.md +87 -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 +57 -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 -45
- package/skills/ultra-test/SKILL.md +48 -55
- package/skills/ultra-verify/SKILL.md +22 -31
- package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
- package/skills/ultra-verify/scripts/verify_wait.py +54 -54
- package/spec/cli-protocol.md +7 -2
- package/spec/mcp-tools.yaml +422 -73
- 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 +82 -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 +213 -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 -131
- 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 -137
- package/adapters/tests/codex-hook.test.cjs +0 -115
- package/adapters/tests/codex.test.cjs +0 -423
- package/adapters/tests/kimi-hook.test.cjs +0 -122
- package/adapters/tests/kimi.test.cjs +0 -259
- package/adapters/tests/opencode.test.cjs +0 -371
- 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/llm-client.cjs +0 -157
- package/mcp-server/lib/llm-client.test.cjs +0 -156
- package/mcp-server/lib/plan-store.test.cjs +0 -132
- package/mcp-server/lib/prd-parser.test.cjs +0 -171
- 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 -324
- 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 -1471
- 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/expand-prompt.cjs +0 -62
- package/orchestrator/planner/expand-prompt.test.cjs +0 -52
- package/orchestrator/planner/plan-builder.test.cjs +0 -165
- package/orchestrator/planner/prd-prompt.cjs +0 -44
- package/orchestrator/planner/prd-prompt.test.cjs +0 -30
- 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,219 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Phase 6.2 — Telemetry collectors:
|
|
4
|
-
// • appendTelemetry writes to state.db.telemetry AND .ultra/telemetry/
|
|
5
|
-
// {YYYY-MM-DD}.jsonl (double-write; jsonl keeps full payload the table
|
|
6
|
-
// can't hold).
|
|
7
|
-
// • computeCost(runtime, model, ti, to) returns USD from 2026-04 pricing.
|
|
8
|
-
// • aggregateTelemetryByRuntime / byTask / bySession drive /ultra-status.
|
|
9
|
-
|
|
10
|
-
const { test } = require('node:test');
|
|
11
|
-
const assert = require('node:assert/strict');
|
|
12
|
-
const fs = require('node:fs');
|
|
13
|
-
const os = require('node:os');
|
|
14
|
-
const path = require('node:path');
|
|
15
|
-
|
|
16
|
-
const { initStateDb, closeStateDb } = require('../lib/state-db.cjs');
|
|
17
|
-
const ops = require('../lib/state-ops.cjs');
|
|
18
|
-
const telemetry = require('../lib/telemetry.cjs');
|
|
19
|
-
const pricing = require('../lib/pricing.cjs');
|
|
20
|
-
|
|
21
|
-
function freshFixture() {
|
|
22
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-tele-'));
|
|
23
|
-
const file = path.join(dir, '.ultra', 'state.db');
|
|
24
|
-
const init = initStateDb(file);
|
|
25
|
-
return { dir, db: init.db };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function teardown(dir, db) {
|
|
29
|
-
try { closeStateDb(db); } catch (_) { /* ignore */ }
|
|
30
|
-
try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_) { /* ignore */ }
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function seedSession(db, { sid = 's-1', task_id = 't-1', runtime = 'claude' } = {}) {
|
|
34
|
-
ops.createTask(db, { id: task_id, title: 'tele target', type: 'feature', priority: 'P1' });
|
|
35
|
-
ops.createSession(db, {
|
|
36
|
-
sid, task_id, runtime, pid: null,
|
|
37
|
-
worktree_path: `/tmp/${sid}/wt`,
|
|
38
|
-
artifact_dir: `/tmp/${sid}/art`,
|
|
39
|
-
});
|
|
40
|
-
return { sid, task_id, runtime };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// ─── pricing ──────────────────────────────────────────────────────────────
|
|
44
|
-
|
|
45
|
-
test('computeCost: claude opus produces non-zero USD for realistic tokens', () => {
|
|
46
|
-
const cost = pricing.computeCost('claude', 'claude-opus-4-7', 1000, 500);
|
|
47
|
-
assert.ok(cost > 0 && cost < 1, `expected reasonable cost, got ${cost}`);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test('computeCost: unknown runtime → null', () => {
|
|
51
|
-
assert.equal(pricing.computeCost('madeup', 'model-x', 100, 100), null);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test('computeCost: zero tokens → zero', () => {
|
|
55
|
-
assert.equal(pricing.computeCost('claude', 'claude-sonnet-4-6', 0, 0), 0);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
// ─── appendTelemetry ─────────────────────────────────────────────────────
|
|
59
|
-
|
|
60
|
-
test('appendTelemetry writes to telemetry table', () => {
|
|
61
|
-
const { dir, db } = freshFixture();
|
|
62
|
-
try {
|
|
63
|
-
seedSession(db, { sid: 's-t1' });
|
|
64
|
-
const out = telemetry.appendTelemetry(db, {
|
|
65
|
-
event_type: 'tool_call',
|
|
66
|
-
tool_name: 'task.list',
|
|
67
|
-
session_id: 's-t1',
|
|
68
|
-
rootDir: dir,
|
|
69
|
-
});
|
|
70
|
-
assert.ok(out.id > 0);
|
|
71
|
-
const row = db.prepare('SELECT * FROM telemetry WHERE id = ?').get(out.id);
|
|
72
|
-
assert.equal(row.event_type, 'tool_call');
|
|
73
|
-
assert.equal(row.tool_name, 'task.list');
|
|
74
|
-
assert.equal(row.session_id, 's-t1');
|
|
75
|
-
} finally { teardown(dir, db); }
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test('appendTelemetry writes a jsonl line under .ultra/telemetry/YYYY-MM-DD.jsonl', () => {
|
|
79
|
-
const { dir, db } = freshFixture();
|
|
80
|
-
try {
|
|
81
|
-
seedSession(db, { sid: 's-t2' });
|
|
82
|
-
telemetry.appendTelemetry(db, {
|
|
83
|
-
event_type: 'tool_call',
|
|
84
|
-
tool_name: 'task.create',
|
|
85
|
-
session_id: 's-t2',
|
|
86
|
-
rootDir: dir,
|
|
87
|
-
payload: { duration_ms: 42 },
|
|
88
|
-
});
|
|
89
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
90
|
-
const jsonl = path.join(dir, '.ultra', 'telemetry', `${today}.jsonl`);
|
|
91
|
-
assert.ok(fs.existsSync(jsonl), `expected jsonl at ${jsonl}`);
|
|
92
|
-
const lines = fs.readFileSync(jsonl, 'utf8').trim().split('\n');
|
|
93
|
-
assert.equal(lines.length, 1);
|
|
94
|
-
const parsed = JSON.parse(lines[0]);
|
|
95
|
-
assert.equal(parsed.tool_name, 'task.create');
|
|
96
|
-
assert.equal(parsed.payload.duration_ms, 42);
|
|
97
|
-
} finally { teardown(dir, db); }
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test('appendTelemetry auto-computes cost_usd when runtime + tokens supplied', () => {
|
|
101
|
-
const { dir, db } = freshFixture();
|
|
102
|
-
try {
|
|
103
|
-
seedSession(db, { sid: 's-t3' });
|
|
104
|
-
const out = telemetry.appendTelemetry(db, {
|
|
105
|
-
event_type: 'token_usage',
|
|
106
|
-
tool_name: 'session.close',
|
|
107
|
-
session_id: 's-t3',
|
|
108
|
-
runtime: 'claude',
|
|
109
|
-
tokens_input: 2000,
|
|
110
|
-
tokens_output: 500,
|
|
111
|
-
rootDir: dir,
|
|
112
|
-
payload: { model: 'claude-sonnet-4-6' },
|
|
113
|
-
});
|
|
114
|
-
const row = db.prepare('SELECT cost_usd FROM telemetry WHERE id = ?').get(out.id);
|
|
115
|
-
assert.ok(row.cost_usd > 0, 'cost should be computed from tokens');
|
|
116
|
-
} finally { teardown(dir, db); }
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
test('appendTelemetry tolerates null session_id (pre-session CLI calls)', () => {
|
|
120
|
-
const { dir, db } = freshFixture();
|
|
121
|
-
try {
|
|
122
|
-
const out = telemetry.appendTelemetry(db, {
|
|
123
|
-
event_type: 'tool_call',
|
|
124
|
-
tool_name: 'cli.task.list',
|
|
125
|
-
session_id: null,
|
|
126
|
-
rootDir: dir,
|
|
127
|
-
});
|
|
128
|
-
const row = db.prepare('SELECT * FROM telemetry WHERE id = ?').get(out.id);
|
|
129
|
-
assert.equal(row.session_id, null);
|
|
130
|
-
} finally { teardown(dir, db); }
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test('appendTelemetry event_type constraint: invalid value rejected', () => {
|
|
134
|
-
const { dir, db } = freshFixture();
|
|
135
|
-
try {
|
|
136
|
-
assert.throws(
|
|
137
|
-
() => telemetry.appendTelemetry(db, { event_type: 'bogus', rootDir: dir }),
|
|
138
|
-
/CHECK|constraint|event_type/,
|
|
139
|
-
);
|
|
140
|
-
} finally { teardown(dir, db); }
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
test('appendTelemetry rejects unsupported runtime labels', () => {
|
|
144
|
-
const { dir, db } = freshFixture();
|
|
145
|
-
const retiredRuntime = ['gem', 'ini'].join('');
|
|
146
|
-
try {
|
|
147
|
-
assert.throws(
|
|
148
|
-
() => telemetry.appendTelemetry(db, {
|
|
149
|
-
event_type: 'tool_call', runtime: retiredRuntime, rootDir: dir,
|
|
150
|
-
}),
|
|
151
|
-
/unsupported runtime/,
|
|
152
|
-
);
|
|
153
|
-
assert.equal(db.prepare('SELECT COUNT(*) AS n FROM telemetry').get().n, 0);
|
|
154
|
-
} finally { teardown(dir, db); }
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
// ─── aggregation ─────────────────────────────────────────────────────────
|
|
158
|
-
|
|
159
|
-
test('aggregateTelemetryByRuntime: groups calls + sums tokens/cost', () => {
|
|
160
|
-
const { dir, db } = freshFixture();
|
|
161
|
-
try {
|
|
162
|
-
seedSession(db, { sid: 's-a1', task_id: 't-a1', runtime: 'claude' });
|
|
163
|
-
seedSession(db, { sid: 's-a2', task_id: 't-a2', runtime: 'codex' });
|
|
164
|
-
telemetry.appendTelemetry(db, { event_type: 'token_usage', tool_name: 'x', session_id: 's-a1', runtime: 'claude', tokens_input: 1000, tokens_output: 200, rootDir: dir, payload: { model: 'claude-sonnet-4-6' } });
|
|
165
|
-
telemetry.appendTelemetry(db, { event_type: 'token_usage', tool_name: 'y', session_id: 's-a1', runtime: 'claude', tokens_input: 500, tokens_output: 100, rootDir: dir, payload: { model: 'claude-sonnet-4-6' } });
|
|
166
|
-
telemetry.appendTelemetry(db, { event_type: 'token_usage', tool_name: 'z', session_id: 's-a2', runtime: 'codex', tokens_input: 800, tokens_output: 300, rootDir: dir, payload: { model: 'gpt-5.4' } });
|
|
167
|
-
|
|
168
|
-
const summary = ops.aggregateTelemetryByRuntime(db);
|
|
169
|
-
const byRuntime = Object.fromEntries(summary.map((r) => [r.runtime, r]));
|
|
170
|
-
assert.equal(byRuntime.claude.calls, 2);
|
|
171
|
-
assert.equal(byRuntime.codex.calls, 1);
|
|
172
|
-
assert.equal(byRuntime.claude.tokens_in, 1500);
|
|
173
|
-
assert.equal(byRuntime.codex.tokens_in, 800);
|
|
174
|
-
assert.ok(byRuntime.claude.cost_usd > 0);
|
|
175
|
-
} finally { teardown(dir, db); }
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
test('aggregateTelemetryByTask: top N tasks by cost', () => {
|
|
179
|
-
const { dir, db } = freshFixture();
|
|
180
|
-
try {
|
|
181
|
-
seedSession(db, { sid: 's-b1', task_id: 't-cheap', runtime: 'claude' });
|
|
182
|
-
seedSession(db, { sid: 's-b2', task_id: 't-expensive', runtime: 'claude' });
|
|
183
|
-
telemetry.appendTelemetry(db, { event_type: 'token_usage', tool_name: 'x', session_id: 's-b1', runtime: 'claude', tokens_input: 100, tokens_output: 50, rootDir: dir, payload: { model: 'claude-sonnet-4-6' } });
|
|
184
|
-
telemetry.appendTelemetry(db, { event_type: 'token_usage', tool_name: 'y', session_id: 's-b2', runtime: 'claude', tokens_input: 10000, tokens_output: 5000, rootDir: dir, payload: { model: 'claude-opus-4-7' } });
|
|
185
|
-
|
|
186
|
-
const top = ops.aggregateTelemetryByTask(db, { limit: 1 });
|
|
187
|
-
assert.equal(top.length, 1);
|
|
188
|
-
assert.equal(top[0].task_id, 't-expensive');
|
|
189
|
-
assert.ok(top[0].cost_usd > 0);
|
|
190
|
-
} finally { teardown(dir, db); }
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
test('aggregateTelemetryBySession: per-session roll-up', () => {
|
|
194
|
-
const { dir, db } = freshFixture();
|
|
195
|
-
try {
|
|
196
|
-
seedSession(db, { sid: 's-c1', task_id: 't-c1', runtime: 'claude' });
|
|
197
|
-
telemetry.appendTelemetry(db, { event_type: 'tool_call', tool_name: 'task.list', session_id: 's-c1', rootDir: dir });
|
|
198
|
-
telemetry.appendTelemetry(db, { event_type: 'tool_call', tool_name: 'task.get', session_id: 's-c1', rootDir: dir });
|
|
199
|
-
telemetry.appendTelemetry(db, { event_type: 'token_usage', tool_name: 'session.close', session_id: 's-c1', runtime: 'claude', tokens_input: 500, tokens_output: 100, rootDir: dir, payload: { model: 'claude-haiku-4-5' } });
|
|
200
|
-
|
|
201
|
-
const summary = ops.aggregateTelemetryBySession(db, 's-c1');
|
|
202
|
-
assert.equal(summary.tool_calls, 3);
|
|
203
|
-
assert.equal(summary.tokens_in, 500);
|
|
204
|
-
assert.equal(summary.tokens_out, 100);
|
|
205
|
-
assert.ok(summary.cost_usd > 0);
|
|
206
|
-
} finally { teardown(dir, db); }
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
test('aggregate filter: since cutoff excludes older rows', () => {
|
|
210
|
-
const { dir, db } = freshFixture();
|
|
211
|
-
try {
|
|
212
|
-
seedSession(db, { sid: 's-d1', task_id: 't-d1', runtime: 'claude' });
|
|
213
|
-
// Insert one row; then a cutoff in the future filters it out.
|
|
214
|
-
telemetry.appendTelemetry(db, { event_type: 'tool_call', tool_name: 'x', session_id: 's-d1', rootDir: dir });
|
|
215
|
-
const futureCutoff = new Date(Date.now() + 60 * 1000).toISOString();
|
|
216
|
-
const summary = ops.aggregateTelemetryByRuntime(db, { since: futureCutoff });
|
|
217
|
-
assert.equal(summary.length, 0);
|
|
218
|
-
} finally { teardown(dir, db); }
|
|
219
|
-
});
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Phase 8A.3 — Prompt templates for task → subtasks expansion.
|
|
4
|
-
//
|
|
5
|
-
// Parent context (title, type, priority, complexity, files_modified) is
|
|
6
|
-
// serialized into the user prompt so the LLM can produce children aligned
|
|
7
|
-
// with the parent's shape. sub_count defaults to 3 when omitted.
|
|
8
|
-
|
|
9
|
-
const DEFAULT_SUB_COUNT = 3;
|
|
10
|
-
|
|
11
|
-
function buildExpandSystemPrompt({ sub_count = DEFAULT_SUB_COUNT } = {}) {
|
|
12
|
-
return `You are an engineering task decomposer. Break a single complex task into ${sub_count} smaller subtasks that together deliver the parent's scope.
|
|
13
|
-
|
|
14
|
-
Output STRICT JSON matching this shape — nothing else, no markdown fences, no prose:
|
|
15
|
-
|
|
16
|
-
{
|
|
17
|
-
"children": [
|
|
18
|
-
{
|
|
19
|
-
"id": "child-1",
|
|
20
|
-
"title": "Imperative short title (3-80 chars)",
|
|
21
|
-
"type": "architecture | feature | bugfix",
|
|
22
|
-
"priority": "P0 | P1 | P2 | P3",
|
|
23
|
-
"complexity": 1-10,
|
|
24
|
-
"deps": ["child-2"],
|
|
25
|
-
"files_modified": ["src/path/to/file.ts"]
|
|
26
|
-
}
|
|
27
|
-
]
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
Rules:
|
|
31
|
-
- Exactly ${sub_count} children.
|
|
32
|
-
- Each id matches /^[a-zA-Z0-9_\\-]+$/ and is unique in the list.
|
|
33
|
-
- "deps" references siblings in the same list; omit or use [] when none.
|
|
34
|
-
- "files_modified" overlaps with the parent's files_modified where possible.
|
|
35
|
-
- Each child's complexity is < parent's complexity; sum should approximate the parent.
|
|
36
|
-
- Return JSON only.`;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function buildExpandUserPrompt(parent) {
|
|
40
|
-
const files = Array.isArray(parent.files_modified) ? parent.files_modified : [];
|
|
41
|
-
const deps = Array.isArray(parent.deps) ? parent.deps : [];
|
|
42
|
-
return [
|
|
43
|
-
'Parent task to decompose:',
|
|
44
|
-
'',
|
|
45
|
-
`- id: ${parent.id}`,
|
|
46
|
-
`- title: ${parent.title}`,
|
|
47
|
-
`- type: ${parent.type}`,
|
|
48
|
-
`- priority: ${parent.priority}`,
|
|
49
|
-
parent.complexity != null ? `- complexity: ${parent.complexity}` : null,
|
|
50
|
-
files.length ? `- files_modified: ${JSON.stringify(files)}` : null,
|
|
51
|
-
deps.length ? `- existing deps: ${JSON.stringify(deps)}` : null,
|
|
52
|
-
parent.trace_to ? `- trace_to: ${parent.trace_to}` : null,
|
|
53
|
-
'',
|
|
54
|
-
'Return the children list as JSON following the system schema.',
|
|
55
|
-
].filter((l) => l !== null).join('\n');
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
module.exports = {
|
|
59
|
-
buildExpandSystemPrompt,
|
|
60
|
-
buildExpandUserPrompt,
|
|
61
|
-
DEFAULT_SUB_COUNT,
|
|
62
|
-
};
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const test = require('node:test');
|
|
4
|
-
const assert = require('node:assert/strict');
|
|
5
|
-
|
|
6
|
-
const {
|
|
7
|
-
buildExpandSystemPrompt,
|
|
8
|
-
buildExpandUserPrompt,
|
|
9
|
-
DEFAULT_SUB_COUNT,
|
|
10
|
-
} = require('./expand-prompt.cjs');
|
|
11
|
-
|
|
12
|
-
test('buildExpandSystemPrompt: mentions sub_count explicitly', () => {
|
|
13
|
-
const s = buildExpandSystemPrompt({ sub_count: 4 });
|
|
14
|
-
assert.match(s, /Exactly 4 children/);
|
|
15
|
-
assert.match(s, /STRICT JSON/);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
test('buildExpandSystemPrompt: falls back to DEFAULT_SUB_COUNT when omitted', () => {
|
|
19
|
-
const s = buildExpandSystemPrompt({});
|
|
20
|
-
assert.match(s, new RegExp(`Exactly ${DEFAULT_SUB_COUNT} children`));
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test('buildExpandUserPrompt: serializes parent fields into prompt body', () => {
|
|
24
|
-
const parent = {
|
|
25
|
-
id: 'parent-1',
|
|
26
|
-
title: 'Build search feature',
|
|
27
|
-
type: 'feature',
|
|
28
|
-
priority: 'P1',
|
|
29
|
-
complexity: 8,
|
|
30
|
-
files_modified: ['src/search/index.ts', 'src/search/query.ts'],
|
|
31
|
-
deps: ['infra-1'],
|
|
32
|
-
trace_to: 'spec §3.2',
|
|
33
|
-
};
|
|
34
|
-
const body = buildExpandUserPrompt(parent);
|
|
35
|
-
assert.match(body, /parent-1/);
|
|
36
|
-
assert.match(body, /Build search feature/);
|
|
37
|
-
assert.match(body, /complexity: 8/);
|
|
38
|
-
assert.match(body, /src\/search\/index\.ts/);
|
|
39
|
-
assert.match(body, /infra-1/);
|
|
40
|
-
assert.match(body, /spec §3\.2/);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test('buildExpandUserPrompt: omits empty optional fields', () => {
|
|
44
|
-
const parent = {
|
|
45
|
-
id: 'p', title: 'Tiny task', type: 'feature', priority: 'P3',
|
|
46
|
-
complexity: null, files_modified: [], deps: [],
|
|
47
|
-
};
|
|
48
|
-
const body = buildExpandUserPrompt(parent);
|
|
49
|
-
assert.doesNotMatch(body, /files_modified/);
|
|
50
|
-
assert.doesNotMatch(body, /existing deps/);
|
|
51
|
-
assert.doesNotMatch(body, /complexity:/);
|
|
52
|
-
});
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const test = require('node:test');
|
|
4
|
-
const assert = require('node:assert/strict');
|
|
5
|
-
|
|
6
|
-
const { buildPlan, listWaveConflicts } = require('./plan-builder.cjs');
|
|
7
|
-
|
|
8
|
-
function mk(id, deps = [], files = [], complexity = 3) {
|
|
9
|
-
return { id, deps, files_modified: files, complexity };
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
test('buildPlan fixture 1: 5 tasks, no file overlap → 2 waves, no conflicts', () => {
|
|
13
|
-
const tasks = [
|
|
14
|
-
mk('a', [], ['src/a.ts'], 2),
|
|
15
|
-
mk('b', ['a'], ['src/b.ts'], 3),
|
|
16
|
-
mk('c', ['a'], ['src/c.ts'], 4),
|
|
17
|
-
mk('d', ['b', 'c'], ['src/d.ts'], 5),
|
|
18
|
-
mk('e', [], ['src/e.ts'], 2),
|
|
19
|
-
];
|
|
20
|
-
const plan = buildPlan(tasks);
|
|
21
|
-
assert.equal(plan.waves.length, 3);
|
|
22
|
-
assert.equal(plan.conflict_surface.length, 0);
|
|
23
|
-
// wave 1 should contain a+e, both parallel-safe
|
|
24
|
-
assert.deepEqual(new Set(plan.waves[0].tasks), new Set(['a', 'e']));
|
|
25
|
-
assert.equal(plan.waves[0].parallel, true);
|
|
26
|
-
assert.deepEqual(plan.cycles, []);
|
|
27
|
-
assert.ok(plan.estimated_cost_usd > 0);
|
|
28
|
-
assert.ok(plan.estimated_duration_min > 0);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test('buildPlan fixture 2: same wave, shared file → conflict_surface + parallel=false', () => {
|
|
32
|
-
const tasks = [
|
|
33
|
-
mk('a', []),
|
|
34
|
-
mk('b', ['a'], ['src/utils.ts']),
|
|
35
|
-
mk('c', ['a'], ['src/utils.ts']),
|
|
36
|
-
];
|
|
37
|
-
const plan = buildPlan(tasks);
|
|
38
|
-
assert.equal(plan.waves.length, 2);
|
|
39
|
-
const w2 = plan.waves[1];
|
|
40
|
-
assert.equal(w2.parallel, false);
|
|
41
|
-
assert.match(w2.reason, /shared files: src\/utils\.ts/);
|
|
42
|
-
|
|
43
|
-
assert.equal(plan.conflict_surface.length, 1);
|
|
44
|
-
const c = plan.conflict_surface[0];
|
|
45
|
-
assert.deepEqual(c.files, ['src/utils.ts']);
|
|
46
|
-
assert.deepEqual(new Set(c.tasks), new Set(['b', 'c']));
|
|
47
|
-
assert.equal(c.recommend, 'sequentialize');
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test('buildPlan fixture 3: cross-wave same file → NOT a conflict', () => {
|
|
51
|
-
const tasks = [
|
|
52
|
-
mk('a', [], ['src/shared.ts']),
|
|
53
|
-
mk('b', ['a'], ['src/shared.ts']),
|
|
54
|
-
];
|
|
55
|
-
const plan = buildPlan(tasks);
|
|
56
|
-
assert.equal(plan.conflict_surface.length, 0);
|
|
57
|
-
// wave 1 has only `a`, not parallel; wave 2 has only `b`, not parallel
|
|
58
|
-
for (const w of plan.waves) {
|
|
59
|
-
assert.equal(w.parallel, false);
|
|
60
|
-
assert.ok(!w.reason);
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test('buildPlan fixture 4: empty tasks → empty plan, non-negative numbers', () => {
|
|
65
|
-
const plan = buildPlan([]);
|
|
66
|
-
assert.deepEqual(plan.waves, []);
|
|
67
|
-
assert.deepEqual(plan.conflict_surface, []);
|
|
68
|
-
assert.deepEqual(plan.ownership_forecast, {});
|
|
69
|
-
assert.equal(plan.estimated_cost_usd, 0);
|
|
70
|
-
assert.equal(plan.estimated_duration_min, 0);
|
|
71
|
-
assert.deepEqual(plan.cycles, []);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test('buildPlan fixture 5: cost + duration math (parallel vs serial waves)', () => {
|
|
75
|
-
// wave 1 (parallel): a complexity=2, b complexity=4 → max = 4 * 5 = 20 min
|
|
76
|
-
// wave 2 (1 task, not parallel): c complexity=6 → 6 * 5 = 30 min
|
|
77
|
-
// total duration = 50 min
|
|
78
|
-
const tasks = [
|
|
79
|
-
mk('a', [], ['src/a.ts'], 2),
|
|
80
|
-
mk('b', [], ['src/b.ts'], 4),
|
|
81
|
-
mk('c', ['a', 'b'], ['src/c.ts'], 6),
|
|
82
|
-
];
|
|
83
|
-
const plan = buildPlan(tasks);
|
|
84
|
-
assert.equal(plan.estimated_duration_min, 50);
|
|
85
|
-
// cost: 3 tasks × sum(complexity)=12 × (5000*3e-6 + 2000*15e-6) per complexity unit
|
|
86
|
-
// = 12 × (0.015 + 0.030) = 12 × 0.045 = 0.54 usd
|
|
87
|
-
assert.ok(Math.abs(plan.estimated_cost_usd - 0.54) < 0.001);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test('buildPlan: serial wave (with conflict) sums complexities instead of max', () => {
|
|
91
|
-
// both tasks in same wave share a file → wave is serial
|
|
92
|
-
// wave 1: a(complexity=3), b(complexity=3) → (3+3) * 5 = 30 min
|
|
93
|
-
const tasks = [
|
|
94
|
-
mk('a', [], ['src/utils.ts'], 3),
|
|
95
|
-
mk('b', [], ['src/utils.ts'], 3),
|
|
96
|
-
];
|
|
97
|
-
const plan = buildPlan(tasks);
|
|
98
|
-
assert.equal(plan.waves[0].parallel, false);
|
|
99
|
-
assert.equal(plan.estimated_duration_min, 30);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test('buildPlan: ownership_forecast captures per-task files_modified verbatim', () => {
|
|
103
|
-
const tasks = [
|
|
104
|
-
mk('a', [], ['src/a.ts', 'src/a.util.ts']),
|
|
105
|
-
mk('b', [], ['src/b.ts']),
|
|
106
|
-
];
|
|
107
|
-
const plan = buildPlan(tasks);
|
|
108
|
-
assert.deepEqual(plan.ownership_forecast, {
|
|
109
|
-
a: ['src/a.ts', 'src/a.util.ts'],
|
|
110
|
-
b: ['src/b.ts'],
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
test('buildPlan: cycle in task graph surfaced in plan.cycles', () => {
|
|
115
|
-
const tasks = [
|
|
116
|
-
mk('x', ['y']),
|
|
117
|
-
mk('y', ['x']),
|
|
118
|
-
];
|
|
119
|
-
const plan = buildPlan(tasks);
|
|
120
|
-
assert.deepEqual(plan.waves, []);
|
|
121
|
-
assert.equal(plan.cycles.length, 1);
|
|
122
|
-
assert.deepEqual(new Set(plan.cycles[0]), new Set(['x', 'y']));
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test('buildPlan: missing complexity defaults to DEFAULT_COMPLEXITY for cost calc', () => {
|
|
126
|
-
const tasks = [{ id: 'a', deps: [], files_modified: [] }];
|
|
127
|
-
const plan = buildPlan(tasks);
|
|
128
|
-
// complexity 3 → 15k in, 6k out → cost > 0
|
|
129
|
-
assert.ok(plan.estimated_cost_usd > 0);
|
|
130
|
-
assert.equal(plan.estimated_duration_min, 15); // 3 * 5 (single-task wave, not parallel)
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test('buildPlan: non-array input → TypeError', () => {
|
|
134
|
-
assert.throws(() => buildPlan(null), TypeError);
|
|
135
|
-
assert.throws(() => buildPlan({}), TypeError);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test('listWaveConflicts: no overlap → empty', () => {
|
|
139
|
-
const byId = new Map([
|
|
140
|
-
['a', { id: 'a', files_modified: ['x.ts'] }],
|
|
141
|
-
['b', { id: 'b', files_modified: ['y.ts'] }],
|
|
142
|
-
]);
|
|
143
|
-
assert.deepEqual(listWaveConflicts(['a', 'b'], byId), []);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test('listWaveConflicts: pair overlap reported once', () => {
|
|
147
|
-
const byId = new Map([
|
|
148
|
-
['a', { id: 'a', files_modified: ['shared.ts', 'x.ts'] }],
|
|
149
|
-
['b', { id: 'b', files_modified: ['shared.ts'] }],
|
|
150
|
-
]);
|
|
151
|
-
const out = listWaveConflicts(['a', 'b'], byId);
|
|
152
|
-
assert.equal(out.length, 1);
|
|
153
|
-
assert.deepEqual(out[0].files, ['shared.ts']);
|
|
154
|
-
assert.deepEqual(out[0].tasks, ['a', 'b']);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test('listWaveConflicts: three-way overlap yields C(3,2)=3 pairs', () => {
|
|
158
|
-
const byId = new Map([
|
|
159
|
-
['a', { id: 'a', files_modified: ['x.ts'] }],
|
|
160
|
-
['b', { id: 'b', files_modified: ['x.ts'] }],
|
|
161
|
-
['c', { id: 'c', files_modified: ['x.ts'] }],
|
|
162
|
-
]);
|
|
163
|
-
const out = listWaveConflicts(['a', 'b', 'c'], byId);
|
|
164
|
-
assert.equal(out.length, 3);
|
|
165
|
-
});
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Phase 8A.1 — Prompt templates for PRD → task-list parsing.
|
|
4
|
-
//
|
|
5
|
-
// Kept in a dedicated module so downstream evaluation (fixture regression,
|
|
6
|
-
// prompt A/B testing) can diff the template against commits without touching
|
|
7
|
-
// parser logic.
|
|
8
|
-
|
|
9
|
-
const SYSTEM_PROMPT = `You are an engineering planner. Decompose a product requirement document (PRD) into a topologically ordered task list that a coding agent can execute sequentially or in parallel.
|
|
10
|
-
|
|
11
|
-
Output STRICT JSON matching this shape — nothing else, no markdown fences, no prose:
|
|
12
|
-
|
|
13
|
-
{
|
|
14
|
-
"tasks": [
|
|
15
|
-
{
|
|
16
|
-
"id": "task-1",
|
|
17
|
-
"title": "Imperative short title (3-80 chars)",
|
|
18
|
-
"type": "architecture | feature | bugfix",
|
|
19
|
-
"priority": "P0 | P1 | P2 | P3",
|
|
20
|
-
"complexity": 1-10,
|
|
21
|
-
"deps": ["task-2"],
|
|
22
|
-
"files_modified": ["src/path/to/file.ts"]
|
|
23
|
-
}
|
|
24
|
-
]
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
Rules:
|
|
28
|
-
- Every task id matches /^task-[0-9]+$/ and is unique in the list.
|
|
29
|
-
- "deps" references other ids in the same list; omit or use [] when none.
|
|
30
|
-
- "files_modified" is your best guess of affected paths; use [] when unsure.
|
|
31
|
-
- "complexity" 1 = trivial, 10 = multi-day. Tasks at 7+ will be auto-expanded later.
|
|
32
|
-
- "priority" — P0 blocks other work, P1 critical path, P2 standard, P3 nice-to-have.
|
|
33
|
-
- "type" — architecture for design/scaffolding, feature for new capability, bugfix for correcting existing behaviour.
|
|
34
|
-
- Return JSON only. No surrounding text, no trailing explanation.`;
|
|
35
|
-
|
|
36
|
-
function buildSystemPrompt() {
|
|
37
|
-
return SYSTEM_PROMPT;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function buildUserPrompt(prdText) {
|
|
41
|
-
return `PRD:\n\n${prdText}\n\nReturn the task list as JSON following the system schema.`;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
module.exports = { SYSTEM_PROMPT, buildSystemPrompt, buildUserPrompt };
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const test = require('node:test');
|
|
4
|
-
const assert = require('node:assert/strict');
|
|
5
|
-
|
|
6
|
-
const {
|
|
7
|
-
SYSTEM_PROMPT,
|
|
8
|
-
buildSystemPrompt,
|
|
9
|
-
buildUserPrompt,
|
|
10
|
-
} = require('./prd-prompt.cjs');
|
|
11
|
-
|
|
12
|
-
test('buildSystemPrompt returns the module-level template', () => {
|
|
13
|
-
assert.equal(buildSystemPrompt(), SYSTEM_PROMPT);
|
|
14
|
-
assert.match(SYSTEM_PROMPT, /engineering planner/i);
|
|
15
|
-
assert.match(SYSTEM_PROMPT, /STRICT JSON/);
|
|
16
|
-
assert.match(SYSTEM_PROMPT, /\/\^task-\[0-9\]\+\$\//);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test('buildUserPrompt embeds PRD text verbatim', () => {
|
|
20
|
-
const prd = 'As a user I want to search products by keyword';
|
|
21
|
-
const out = buildUserPrompt(prd);
|
|
22
|
-
assert.ok(out.includes(prd), 'user prompt must contain the PRD text');
|
|
23
|
-
assert.match(out, /JSON/);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test('buildUserPrompt tolerates long PRD text without truncation', () => {
|
|
27
|
-
const long = 'line\n'.repeat(500);
|
|
28
|
-
const out = buildUserPrompt(long);
|
|
29
|
-
assert.ok(out.includes(long));
|
|
30
|
-
});
|