ultra-builder-pro-cli 0.5.0 → 0.5.2
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/CHANGELOG.md +38 -1
- package/README.md +1 -1
- package/adapters/_shared/codex-assets.cjs +1 -1
- package/adapters/codex.js +85 -13
- package/adapters/opencode.js +32 -1
- package/adapters/tests/claude.test.cjs +6 -0
- package/adapters/tests/codex-hook.test.cjs +25 -0
- package/adapters/tests/codex.test.cjs +7 -0
- package/adapters/tests/opencode.test.cjs +37 -0
- package/commands/ultra-dev.md +2 -1
- package/commands/ultra-plan.md +5 -1
- package/commands/ultra-status.md +2 -1
- package/commands/ultra-test.md +2 -1
- package/docs/COMMIT-HASH-BACKFILL.md +3 -3
- package/docs/RUNTIME-COMPAT-MATRIX.md +1 -1
- package/hooks/active_task_context.py +28 -0
- package/mcp-server/lib/projector.cjs +7 -2
- package/mcp-server/lib/state-authority.cjs +73 -0
- package/mcp-server/lib/state-db.cjs +19 -1
- package/mcp-server/lib/state-ops.cjs +3 -2
- package/mcp-server/server.cjs +2 -0
- package/mcp-server/tests/migrate.test.cjs +56 -1
- package/mcp-server/tests/projector.test.cjs +33 -1
- package/mcp-server/tests/server-contract.test.cjs +34 -1
- package/mcp-server/tests/state-db.test.cjs +31 -0
- package/mcp-server/tests/state-ops.test.cjs +15 -2
- package/package.json +1 -1
- package/skills/ultra-dev/SKILL.md +22 -17
- package/skills/ultra-plan/SKILL.md +28 -13
- package/skills/ultra-status/SKILL.md +11 -6
- package/skills/ultra-test/SKILL.md +9 -3
- package/spec/cli-protocol.md +12 -13
- package/spec/mcp-tools.yaml +7 -0
- package/spec/schemas/state-db.sql +3 -0
- package/spec/schemas/tasks.v4.5.schema.json +1 -0
- package/ultra-tools/cli.cjs +5 -5
- package/ultra-tools/commands/migrate.cjs +136 -48
- package/ultra-tools/commands/task.cjs +4 -6
- package/.ultra-template/tasks/contexts/TEMPLATE.md +0 -57
- package/.ultra-template/tasks/tasks.json +0 -33
|
@@ -9,7 +9,7 @@ const REPO_ROOT = process.env.UBP_RUNTIME_ROOT
|
|
|
9
9
|
? path.resolve(process.env.UBP_RUNTIME_ROOT)
|
|
10
10
|
: path.resolve(__dirname, '..', '..');
|
|
11
11
|
const SCHEMA_FILE = path.join(REPO_ROOT, 'spec', 'schemas', 'state-db.sql');
|
|
12
|
-
const EXPECTED_VERSION = '8A.
|
|
12
|
+
const EXPECTED_VERSION = '8A.2';
|
|
13
13
|
|
|
14
14
|
const REQUIRED_TABLES = Object.freeze([
|
|
15
15
|
'tasks',
|
|
@@ -60,6 +60,22 @@ function applySchema(db) {
|
|
|
60
60
|
runScript(db, readSchemaSql());
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
function columnNames(db, table) {
|
|
64
|
+
return new Set(db.prepare(`PRAGMA table_info(${table})`).all().map((row) => row.name));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function applyCompatibleUpgrades(db) {
|
|
68
|
+
const taskColumns = columnNames(db, 'tasks');
|
|
69
|
+
if (!taskColumns.has('estimated_days')) {
|
|
70
|
+
db.transaction(() => {
|
|
71
|
+
db.exec('ALTER TABLE tasks ADD COLUMN estimated_days REAL CHECK (estimated_days IS NULL OR estimated_days > 0)');
|
|
72
|
+
db.prepare(
|
|
73
|
+
'INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)',
|
|
74
|
+
).run(EXPECTED_VERSION, 'Phase 8A.2 — tasks.estimated_days preserved across MCP and v4.4 migration');
|
|
75
|
+
})();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
63
79
|
function ensureSchemaVersion(db) {
|
|
64
80
|
// schema_version is an audit trail — multiple rows across phase upgrades.
|
|
65
81
|
// Guard by checking whether the expected version row exists rather than
|
|
@@ -82,6 +98,7 @@ function initStateDb(dbPath) {
|
|
|
82
98
|
if (missing.length > 0) {
|
|
83
99
|
applySchema(db);
|
|
84
100
|
}
|
|
101
|
+
applyCompatibleUpgrades(db);
|
|
85
102
|
const version = ensureSchemaVersion(db);
|
|
86
103
|
return {
|
|
87
104
|
db,
|
|
@@ -104,6 +121,7 @@ module.exports = {
|
|
|
104
121
|
SCHEMA_FILE,
|
|
105
122
|
openStateDb,
|
|
106
123
|
applySchema,
|
|
124
|
+
applyCompatibleUpgrades,
|
|
107
125
|
applyPragmas,
|
|
108
126
|
ensureSchemaVersion,
|
|
109
127
|
initStateDb,
|
|
@@ -23,14 +23,14 @@ const STATUS_TRANSITIONS = Object.freeze({
|
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
const TASK_FIELDS = Object.freeze([
|
|
26
|
-
'id', 'title', 'type', 'priority', 'complexity', 'status',
|
|
26
|
+
'id', 'title', 'type', 'priority', 'complexity', 'estimated_days', 'status',
|
|
27
27
|
'deps', 'files_modified', 'session_id', 'stale', 'complexity_hint',
|
|
28
28
|
'tag', 'trace_to', 'context_file', 'completion_commit', 'parent_id',
|
|
29
29
|
'created_at', 'updated_at',
|
|
30
30
|
]);
|
|
31
31
|
|
|
32
32
|
const PATCHABLE_FIELDS = Object.freeze([
|
|
33
|
-
'priority', 'complexity', 'deps', 'files_modified',
|
|
33
|
+
'priority', 'complexity', 'estimated_days', 'deps', 'files_modified',
|
|
34
34
|
'session_id', 'stale', 'complexity_hint', 'tag', 'trace_to',
|
|
35
35
|
'context_file', 'completion_commit',
|
|
36
36
|
]);
|
|
@@ -140,6 +140,7 @@ function createTask(db, input) {
|
|
|
140
140
|
type: input.type,
|
|
141
141
|
priority: input.priority,
|
|
142
142
|
complexity: input.complexity ?? null,
|
|
143
|
+
estimated_days: input.estimated_days ?? null,
|
|
143
144
|
status: 'pending',
|
|
144
145
|
deps: input.deps ? JSON.stringify(input.deps) : null,
|
|
145
146
|
files_modified: input.files_modified ? JSON.stringify(input.files_modified) : null,
|
package/mcp-server/server.cjs
CHANGED
|
@@ -18,6 +18,7 @@ const { ListToolsRequestSchema, CallToolRequestSchema } = require('@modelcontext
|
|
|
18
18
|
const { version: PACKAGE_VERSION } = require('../package.json');
|
|
19
19
|
|
|
20
20
|
const { initStateDb, closeStateDb } = require('./lib/state-db.cjs');
|
|
21
|
+
const { assertStateAuthority } = require('./lib/state-authority.cjs');
|
|
21
22
|
const ops = require('./lib/state-ops.cjs');
|
|
22
23
|
const projector = require('./lib/projector.cjs');
|
|
23
24
|
const telemetry = require('./lib/telemetry.cjs');
|
|
@@ -413,6 +414,7 @@ function startServer({ dbPath, rootDir, projectOnWrite = true }) {
|
|
|
413
414
|
let toolDb = null;
|
|
414
415
|
try {
|
|
415
416
|
toolDb = STATELESS_TOOLS.has(name) ? null : getDb();
|
|
417
|
+
if (toolDb) assertStateAuthority(toolDb, rootDir);
|
|
416
418
|
result = await dispatchTool(name, args, toolDb, { rootDir });
|
|
417
419
|
} catch (err) {
|
|
418
420
|
const code = err.code || (err instanceof ops.StateOpsError ? err.code : 'STATE_DB_ERROR');
|
|
@@ -73,6 +73,17 @@ test('migrate forward inserts tasks + events, records migration_history, creates
|
|
|
73
73
|
const taskIds = db.prepare('SELECT id FROM tasks ORDER BY id').all().map((r) => r.id);
|
|
74
74
|
assert.deepEqual(taskIds, ['task-1', 'task-2', 'task-3']);
|
|
75
75
|
|
|
76
|
+
const migrated = db.prepare(
|
|
77
|
+
'SELECT id, deps, estimated_days, context_file, created_at, updated_at FROM tasks ORDER BY id',
|
|
78
|
+
).all();
|
|
79
|
+
assert.deepEqual(JSON.parse(migrated[1].deps), ['task-1']);
|
|
80
|
+
assert.equal(migrated[1].estimated_days, 1.5);
|
|
81
|
+
assert.equal(migrated[1].context_file, '.ultra/tasks/contexts/task-2.md');
|
|
82
|
+
for (const task of migrated) {
|
|
83
|
+
assert.match(task.created_at, /^2026-04-15T00:00:00\.000Z$/);
|
|
84
|
+
assert.match(task.updated_at, /^2026-04-16T00:00:00\.000Z$/);
|
|
85
|
+
}
|
|
86
|
+
|
|
76
87
|
// Status comes from tasks.json (task-3 = pending), not the context md (blocked)
|
|
77
88
|
const t3 = db.prepare("SELECT status FROM tasks WHERE id = 'task-3'").get();
|
|
78
89
|
assert.equal(t3.status, 'pending');
|
|
@@ -85,6 +96,27 @@ test('migrate forward inserts tasks + events, records migration_history, creates
|
|
|
85
96
|
assert.equal(mig[0].direction, 'forward');
|
|
86
97
|
assert.equal(mig[0].status, 'success');
|
|
87
98
|
closeStateDb(db);
|
|
99
|
+
|
|
100
|
+
const projected = JSON.parse(fs.readFileSync(
|
|
101
|
+
path.join(dir, '.ultra', 'tasks', 'tasks.json'),
|
|
102
|
+
'utf8',
|
|
103
|
+
));
|
|
104
|
+
assert.equal(projected.schema_version, '4.5');
|
|
105
|
+
assert.equal(projected.source, '.ultra/state.db');
|
|
106
|
+
assert.equal(projected.tasks.length, 3);
|
|
107
|
+
assert.equal(projected.tasks[1].estimated_days, 1.5);
|
|
108
|
+
assert.deepEqual(projected.tasks[1].deps, ['task-1']);
|
|
109
|
+
assert.match(
|
|
110
|
+
fs.readFileSync(path.join(dir, '.ultra', 'tasks', 'contexts', 'task-2.md'), 'utf8'),
|
|
111
|
+
/Wire JWT validation into the request pipeline/,
|
|
112
|
+
);
|
|
113
|
+
const contextTemplate = fs.readFileSync(
|
|
114
|
+
path.join(dir, '.ultra', 'tasks', 'contexts', 'TEMPLATE.md'),
|
|
115
|
+
'utf8',
|
|
116
|
+
);
|
|
117
|
+
assert.doesNotMatch(contextTemplate, /> \*\*Status\*\*:/);
|
|
118
|
+
assert.doesNotMatch(contextTemplate, /mid_workflow_recall|session_context/);
|
|
119
|
+
assert.match(contextTemplate, /Preserve this acceptance item/);
|
|
88
120
|
} finally {
|
|
89
121
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
90
122
|
}
|
|
@@ -112,7 +144,7 @@ test('migrate --rollback restores .ultra contents and writes a rollback row', ()
|
|
|
112
144
|
|
|
113
145
|
// .ultra/tasks/tasks.json restored to original v4.4 content
|
|
114
146
|
const tasksJson = JSON.parse(fs.readFileSync(path.join(dir, '.ultra', 'tasks', 'tasks.json'), 'utf8'));
|
|
115
|
-
assert.equal(tasksJson.
|
|
147
|
+
assert.equal(tasksJson.version, '4.4');
|
|
116
148
|
assert.equal(tasksJson.tasks.length, 3);
|
|
117
149
|
closeStateDb(undefined);
|
|
118
150
|
} finally {
|
|
@@ -120,6 +152,29 @@ test('migrate --rollback restores .ultra contents and writes a rollback row', ()
|
|
|
120
152
|
}
|
|
121
153
|
});
|
|
122
154
|
|
|
155
|
+
test('migrate refuses to merge a v4.4 projection into a non-empty state.db', () => {
|
|
156
|
+
const dir = tmpProject();
|
|
157
|
+
try {
|
|
158
|
+
const dbPath = path.join(dir, '.ultra', 'state.db');
|
|
159
|
+
const init = require('../lib/state-db.cjs').initStateDb(dbPath);
|
|
160
|
+
require('../lib/state-ops.cjs').createTask(init.db, {
|
|
161
|
+
id: 'existing', title: 'existing task', type: 'feature', priority: 'P1',
|
|
162
|
+
});
|
|
163
|
+
closeStateDb(init.db);
|
|
164
|
+
|
|
165
|
+
const r = runCli(['migrate', '--from=4.4', '--to=4.5', '--source-dir', dir]);
|
|
166
|
+
assert.equal(r.code, 2);
|
|
167
|
+
assert.equal(r.envelope.error.code, 'MIGRATE_FAILED');
|
|
168
|
+
assert.match(r.envelope.error.message, /non-empty state\.db/);
|
|
169
|
+
|
|
170
|
+
const db = openStateDb(dbPath);
|
|
171
|
+
assert.deepEqual(db.prepare('SELECT id FROM tasks ORDER BY id').all(), [{ id: 'existing' }]);
|
|
172
|
+
closeStateDb(db);
|
|
173
|
+
} finally {
|
|
174
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
123
178
|
test('migrate rejects unsupported --from / --to versions', () => {
|
|
124
179
|
const r1 = runCli(['migrate', '--from=3.0', '--to=4.5', '--dry', '--source-dir', FIXTURE]);
|
|
125
180
|
assert.equal(r1.code, 1);
|
|
@@ -34,7 +34,7 @@ function readJson(p) {
|
|
|
34
34
|
test('projectTasks emits a v4.5-conformant tasks.json', () => {
|
|
35
35
|
const { dir, db } = tmpProject();
|
|
36
36
|
try {
|
|
37
|
-
ops.createTask(db, { id: 'p-1', title: 'first', type: 'feature', priority: 'P1', tag: 'main' });
|
|
37
|
+
ops.createTask(db, { id: 'p-1', title: 'first', type: 'feature', priority: 'P1', tag: 'main', estimated_days: 2.5 });
|
|
38
38
|
ops.createTask(db, { id: 'p-2', title: 'second', type: 'bugfix', priority: 'P2' });
|
|
39
39
|
ops.updateTaskStatus(db, 'p-1', 'in_progress');
|
|
40
40
|
|
|
@@ -49,6 +49,7 @@ test('projectTasks emits a v4.5-conformant tasks.json', () => {
|
|
|
49
49
|
|
|
50
50
|
const p1 = projection.tasks.find((t) => t.id === 'p-1');
|
|
51
51
|
assert.equal(p1.status, 'in_progress');
|
|
52
|
+
assert.equal(p1.estimated_days, 2.5);
|
|
52
53
|
closeStateDb(db);
|
|
53
54
|
} finally {
|
|
54
55
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
@@ -78,6 +79,37 @@ test('projectContext rebuilds header but preserves the body', () => {
|
|
|
78
79
|
}
|
|
79
80
|
});
|
|
80
81
|
|
|
82
|
+
test('projectContext removes the legacy body status banner while preserving task content', () => {
|
|
83
|
+
const { dir, db } = tmpProject();
|
|
84
|
+
try {
|
|
85
|
+
ops.createTask(db, {
|
|
86
|
+
id: 'legacy-body', title: 'legacy context body', type: 'feature', priority: 'P1',
|
|
87
|
+
context_file: '.ultra/tasks/contexts/task-legacy-body.md',
|
|
88
|
+
});
|
|
89
|
+
const ctxFile = path.join(dir, '.ultra', 'tasks', 'contexts', 'task-legacy-body.md');
|
|
90
|
+
fs.mkdirSync(path.dirname(ctxFile), { recursive: true });
|
|
91
|
+
fs.writeFileSync(ctxFile, [
|
|
92
|
+
'# Task legacy-body',
|
|
93
|
+
'',
|
|
94
|
+
'> **Status**: pending | **Priority**: P1 | **Complexity**: 4',
|
|
95
|
+
'',
|
|
96
|
+
'## Context',
|
|
97
|
+
'Keep this body.',
|
|
98
|
+
'',
|
|
99
|
+
].join('\n'));
|
|
100
|
+
|
|
101
|
+
projector.projectContext(db, 'legacy-body', {}, { rootDir: dir });
|
|
102
|
+
const text = fs.readFileSync(ctxFile, 'utf8');
|
|
103
|
+
assert.match(text, /status: pending/);
|
|
104
|
+
assert.doesNotMatch(text, /> \*\*Status\*\*:/);
|
|
105
|
+
assert.match(text, /# Task legacy-body/);
|
|
106
|
+
assert.match(text, /Keep this body\./);
|
|
107
|
+
closeStateDb(db);
|
|
108
|
+
} finally {
|
|
109
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
81
113
|
test('manual edits to tasks.json are overwritten on the next projectAll', () => {
|
|
82
114
|
const { dir, db } = tmpProject();
|
|
83
115
|
try {
|
|
@@ -131,7 +131,7 @@ test('task.create + task.get round trip via MCP', async () => {
|
|
|
131
131
|
await withClient(proj, async (client) => {
|
|
132
132
|
const created = await client.callTool({
|
|
133
133
|
name: 'task.create',
|
|
134
|
-
arguments: { id: 'mcp-1', title: 'first', type: 'feature', priority: 'P1' },
|
|
134
|
+
arguments: { id: 'mcp-1', title: 'first', type: 'feature', priority: 'P1', estimated_days: 2.5 },
|
|
135
135
|
});
|
|
136
136
|
const createdData = readToolPayload(created);
|
|
137
137
|
assert.equal(createdData.id, 'mcp-1');
|
|
@@ -141,6 +141,39 @@ test('task.create + task.get round trip via MCP', async () => {
|
|
|
141
141
|
const gotData = readToolPayload(got);
|
|
142
142
|
assert.equal(gotData.task.id, 'mcp-1');
|
|
143
143
|
assert.equal(gotData.task.title, 'first');
|
|
144
|
+
assert.equal(gotData.task.estimated_days, 2.5);
|
|
145
|
+
});
|
|
146
|
+
} finally {
|
|
147
|
+
fs.rmSync(proj.dir, { recursive: true, force: true });
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('state-backed MCP tools fail closed when a non-empty v4.4 tasks.json meets an empty state.db', async () => {
|
|
152
|
+
const proj = tmpProject();
|
|
153
|
+
try {
|
|
154
|
+
const tasksDir = path.join(proj.dir, '.ultra', 'tasks');
|
|
155
|
+
fs.mkdirSync(tasksDir, { recursive: true });
|
|
156
|
+
fs.writeFileSync(path.join(tasksDir, 'tasks.json'), JSON.stringify({
|
|
157
|
+
version: '4.4',
|
|
158
|
+
tasks: [{
|
|
159
|
+
id: 'legacy-1', title: 'legacy task', type: 'feature', priority: 'P1',
|
|
160
|
+
status: 'pending', dependencies: [], estimated_days: 1,
|
|
161
|
+
context_file: 'contexts/task-legacy-1.md',
|
|
162
|
+
}],
|
|
163
|
+
}));
|
|
164
|
+
|
|
165
|
+
await withClient(proj, async (client) => {
|
|
166
|
+
const listed = await client.callTool({ name: 'task.list', arguments: {} });
|
|
167
|
+
const error = expectError(listed);
|
|
168
|
+
assert.equal(error.code, 'LEGACY_STATE_MIGRATION_REQUIRED');
|
|
169
|
+
assert.match(error.message, /ultra-tools migrate --from=4\.4 --to=4\.5/);
|
|
170
|
+
assert.equal(error.details.legacy_task_count, 1);
|
|
171
|
+
|
|
172
|
+
const created = await client.callTool({
|
|
173
|
+
name: 'task.create',
|
|
174
|
+
arguments: { title: 'must not overwrite legacy', type: 'feature', priority: 'P1' },
|
|
175
|
+
});
|
|
176
|
+
assert.equal(expectError(created).code, 'LEGACY_STATE_MIGRATION_REQUIRED');
|
|
144
177
|
});
|
|
145
178
|
} finally {
|
|
146
179
|
fs.rmSync(proj.dir, { recursive: true, force: true });
|
|
@@ -0,0 +1,31 @@
|
|
|
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 stateDb = require('../lib/state-db.cjs');
|
|
10
|
+
|
|
11
|
+
test('initStateDb upgrades a v8A.1 database in place with estimated_days', () => {
|
|
12
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-db-upgrade-'));
|
|
13
|
+
const file = path.join(dir, 'state.db');
|
|
14
|
+
try {
|
|
15
|
+
const db = stateDb.openStateDb(file);
|
|
16
|
+
const oldSql = fs.readFileSync(stateDb.SCHEMA_FILE, 'utf8')
|
|
17
|
+
.replace(/^\s*estimated_days.*\n/m, '')
|
|
18
|
+
.replace(/^INSERT OR IGNORE INTO schema_version \(version, description\)\nVALUES \('8A\.2'.*\n/m, '');
|
|
19
|
+
db.exec(oldSql);
|
|
20
|
+
stateDb.closeStateDb(db);
|
|
21
|
+
|
|
22
|
+
const upgraded = stateDb.initStateDb(file);
|
|
23
|
+
const columns = upgraded.db.prepare('PRAGMA table_info(tasks)').all().map((row) => row.name);
|
|
24
|
+
assert.ok(columns.includes('estimated_days'));
|
|
25
|
+
assert.equal(upgraded.schema_version, '8A.2');
|
|
26
|
+
assert.ok(upgraded.db.prepare("SELECT 1 FROM schema_version WHERE version = '8A.2'").get());
|
|
27
|
+
stateDb.closeStateDb(upgraded.db);
|
|
28
|
+
} finally {
|
|
29
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
30
|
+
}
|
|
31
|
+
});
|
|
@@ -20,14 +20,15 @@ function freshDb() {
|
|
|
20
20
|
return { ...t, db: init.db };
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
test('createTask inserts a row, defaults status=pending, emits task_created', () => {
|
|
23
|
+
test('createTask inserts a row, preserves estimated_days, defaults status=pending, emits task_created', () => {
|
|
24
24
|
const { dir, db } = freshDb();
|
|
25
25
|
try {
|
|
26
26
|
const out = ops.createTask(db, {
|
|
27
|
-
id: 'task-001', title: 'first task', type: 'feature', priority: 'P1',
|
|
27
|
+
id: 'task-001', title: 'first task', type: 'feature', priority: 'P1', estimated_days: 2.5,
|
|
28
28
|
});
|
|
29
29
|
assert.equal(out.id, 'task-001');
|
|
30
30
|
assert.equal(out.status, 'pending');
|
|
31
|
+
assert.equal(out.estimated_days, 2.5);
|
|
31
32
|
const events = db.prepare('SELECT type, task_id FROM events').all();
|
|
32
33
|
assert.equal(events.length, 1);
|
|
33
34
|
assert.equal(events[0].type, 'task_created');
|
|
@@ -38,6 +39,18 @@ test('createTask inserts a row, defaults status=pending, emits task_created', ()
|
|
|
38
39
|
}
|
|
39
40
|
});
|
|
40
41
|
|
|
42
|
+
test('patchTask updates estimated_days', () => {
|
|
43
|
+
const { dir, db } = freshDb();
|
|
44
|
+
try {
|
|
45
|
+
ops.createTask(db, { id: 'estimate', title: 'estimate task', type: 'feature', priority: 'P2' });
|
|
46
|
+
const out = ops.patchTask(db, 'estimate', { estimated_days: 1.5 });
|
|
47
|
+
assert.equal(out.estimated_days, 1.5);
|
|
48
|
+
closeStateDb(db);
|
|
49
|
+
} finally {
|
|
50
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
41
54
|
test('createTask rejects duplicates with DUPLICATE_TASK_ID', () => {
|
|
42
55
|
const { dir, db } = freshDb();
|
|
43
56
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra-builder-pro-cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Multi-runtime installer for the Ultra Builder Pro agent engineering system — deploys commands, agents, skills, hooks, and MCP server to Claude Code, OpenCode, Codex CLI, and Gemini CLI.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ultra-builder-pro-cli": "bin/install.js",
|
|
@@ -6,7 +6,6 @@ mcp_tools_required:
|
|
|
6
6
|
- task.update
|
|
7
7
|
- task.get
|
|
8
8
|
- task.list
|
|
9
|
-
cli_fallback: "task update"
|
|
10
9
|
---
|
|
11
10
|
|
|
12
11
|
# ultra-dev — Phase 3.4
|
|
@@ -17,6 +16,19 @@ MCP `task.update`; the projector regenerates `tasks.json` and context-md
|
|
|
17
16
|
frontmatter. The skill only writes context-md **bodies** (Change Log,
|
|
18
17
|
Completion, Dual-Write notes).
|
|
19
18
|
|
|
19
|
+
## Authority failure boundary
|
|
20
|
+
|
|
21
|
+
All task selection and transitions require the live MCP state tools. If they
|
|
22
|
+
are unavailable or return an authority error, stop. For
|
|
23
|
+
`LEGACY_STATE_MIGRATION_REQUIRED`, instruct:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
ultra-tools migrate --from=4.4 --to=4.5 --source-dir <project-root>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Never fall back to `.ultra/tasks/tasks.json`, direct SQLite writes, or a task
|
|
30
|
+
CLI. The JSON file is only a projector-owned view.
|
|
31
|
+
|
|
20
32
|
## Design decisions vs pre-Phase-3
|
|
21
33
|
|
|
22
34
|
- **Single-write status**: Step 1.5 / Step 5 stop touching `contexts/task-{id}.md`
|
|
@@ -30,9 +42,7 @@ Completion, Dual-Write notes).
|
|
|
30
42
|
|
|
31
43
|
## Prerequisites
|
|
32
44
|
|
|
33
|
-
-
|
|
34
|
-
- At least one task with `status=pending`
|
|
35
|
-
- `state.db` initialized (first `task.*` call will init if missing)
|
|
45
|
+
- `task.list` succeeds against state.db and returns at least one `pending` task
|
|
36
46
|
|
|
37
47
|
## Arguments
|
|
38
48
|
|
|
@@ -82,8 +92,6 @@ Acceptance sections. Use the Acceptance list as the initial todo set.
|
|
|
82
92
|
{ "id": "<id>", "patch": { "status": "in_progress" } }
|
|
83
93
|
```
|
|
84
94
|
|
|
85
|
-
**CLI fallback**: `ultra-tools task update <id> --status in_progress`.
|
|
86
|
-
|
|
87
95
|
Projector runs after the write → `tasks.json` + `contexts/task-{id}.md`
|
|
88
96
|
frontmatter re-generated with `status: in_progress`. **Do not hand-edit either.**
|
|
89
97
|
|
|
@@ -191,14 +199,11 @@ Checkpoint `step="4.5", status="review_done", review_session=<id>, review_iterat
|
|
|
191
199
|
{
|
|
192
200
|
"id": "<id>",
|
|
193
201
|
"patch": {
|
|
194
|
-
"status": "completed"
|
|
195
|
-
"completion_commit": "_pending_" // overwritten in Step 6 after commit
|
|
202
|
+
"status": "completed"
|
|
196
203
|
}
|
|
197
204
|
}
|
|
198
205
|
```
|
|
199
206
|
|
|
200
|
-
**CLI fallback**: `ultra-tools task update <id> --status completed`.
|
|
201
|
-
|
|
202
207
|
Projector regenerates frontmatter. **Then** write the context-md **body**
|
|
203
208
|
Completion section (skill responsibility, not projector):
|
|
204
209
|
|
|
@@ -297,15 +302,15 @@ Log in context-md body Change Log:
|
|
|
297
302
|
history. Specs may grow (EXPANSION) or be corrected (CORRECTION). Specs must
|
|
298
303
|
never silently shrink (REDUCTION without gate).
|
|
299
304
|
|
|
300
|
-
## MCP
|
|
305
|
+
## MCP failure matrix
|
|
301
306
|
|
|
302
|
-
| Purpose | MCP tool (phase) |
|
|
303
|
-
|
|
304
|
-
| Select pending task | `task.list` (2) / `task.get` (2) |
|
|
305
|
-
| Status → in_progress | `task.update` (2) |
|
|
307
|
+
| Purpose | MCP tool (phase) | Failure behavior |
|
|
308
|
+
|---------|------------------|------------------|
|
|
309
|
+
| Select pending task | `task.list` (2) / `task.get` (2) | stop and surface the MCP error |
|
|
310
|
+
| Status → in_progress | `task.update` (2) | stop; do not edit a projection |
|
|
306
311
|
| Pre-review checkpoint | none | write `.ultra/workflow-state.json`; lifecycle hooks restore it |
|
|
307
312
|
| Run review | none | `/ultra-review all` + Host-native review agents |
|
|
308
|
-
| Status → completed | `task.update` (2) |
|
|
313
|
+
| Status → completed | `task.update` (2) | stop; do not claim completion |
|
|
309
314
|
| Ask user | none | current Host's native user-interaction surface |
|
|
310
315
|
|
|
311
316
|
## What this skill DOES NOT do
|
|
@@ -319,6 +324,6 @@ never silently shrink (REDUCTION without gate).
|
|
|
319
324
|
|
|
320
325
|
| | |
|
|
321
326
|
|---|---|
|
|
322
|
-
| **Input** |
|
|
327
|
+
| **Input** | state.db via MCP, plus the `context_file` body returned for the selected task |
|
|
323
328
|
| **Writes** | state.db (via `task.update`), context-md body (Completion + Change Log), workflow-state.json checkpoints |
|
|
324
329
|
| **Next** | `/ultra-dev <next-id>` or `/ultra-test` when all Walking Skeleton + critical-path tasks complete |
|
|
@@ -8,7 +8,6 @@ mcp_tools_required:
|
|
|
8
8
|
- task.dependency_topo
|
|
9
9
|
- plan.export
|
|
10
10
|
- plan.get
|
|
11
|
-
cli_fallback: "task create"
|
|
12
11
|
---
|
|
13
12
|
|
|
14
13
|
# ultra-plan — Phase 3.3
|
|
@@ -18,6 +17,20 @@ the authority** (D32): every new task is created via MCP `task.create`; the
|
|
|
18
17
|
projector regenerates `.ultra/tasks/tasks.json` and context-md frontmatter
|
|
19
18
|
automatically. Context-md bodies are written by this skill (not projector).
|
|
20
19
|
|
|
20
|
+
## Authority failure boundary
|
|
21
|
+
|
|
22
|
+
Before planning, call a state-backed MCP tool. If the MCP server is missing,
|
|
23
|
+
unreachable, or returns any authority error, stop before creating tasks. In
|
|
24
|
+
particular, `LEGACY_STATE_MIGRATION_REQUIRED` means the project still has a
|
|
25
|
+
non-empty v4.4 projection; tell the user to run:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
ultra-tools migrate --from=4.4 --to=4.5 --source-dir <project-root>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Never read or write `.ultra/tasks/tasks.json` as a fallback. It is a generated
|
|
32
|
+
projection, not an alternate task store. There is no task-mutation CLI fallback.
|
|
33
|
+
|
|
21
34
|
## Prerequisites
|
|
22
35
|
|
|
23
36
|
- `/ultra-research` completed — specs must be 100% (no `[NEEDS CLARIFICATION]`)
|
|
@@ -26,6 +39,13 @@ automatically. Context-md bodies are written by this skill (not projector).
|
|
|
26
39
|
|
|
27
40
|
## Workflow
|
|
28
41
|
|
|
42
|
+
### Workflow activation
|
|
43
|
+
|
|
44
|
+
At entry, write `.ultra/workflow-state.json` with
|
|
45
|
+
`{"command":"ultra-plan","task_id":"plan","step":"0","status":"active","ts":"<ISO8601>"}`.
|
|
46
|
+
This activates the workflow-only hooks. Mark it terminal only after the MCP
|
|
47
|
+
task writes and verification complete.
|
|
48
|
+
|
|
29
49
|
### Input Mode Selection (BEFORE Step 0)
|
|
30
50
|
|
|
31
51
|
Two entry points lead into the planner:
|
|
@@ -140,7 +160,7 @@ On fail → instruct user to run `/ultra-research` targeting the gap.
|
|
|
140
160
|
|
|
141
161
|
Fields per task: `id`, `title` (action verb + target), `type`
|
|
142
162
|
(architecture/feature/bugfix), `priority` (P0-P3), `complexity` (1-10),
|
|
143
|
-
`status` (pending initially), `
|
|
163
|
+
`status` (pending initially), `deps` (task IDs), `estimated_days`,
|
|
144
164
|
`context_file`, `trace_to` (spec section anchor).
|
|
145
165
|
|
|
146
166
|
#### Integration tasks — MANDATORY inserts
|
|
@@ -187,19 +207,13 @@ For **each** task in the planned list (topological order):
|
|
|
187
207
|
"type": "architecture",
|
|
188
208
|
"priority": "P0",
|
|
189
209
|
"complexity": 4,
|
|
210
|
+
"estimated_days": 2,
|
|
190
211
|
"deps": [],
|
|
191
212
|
"tag": "<git branch>", // optional
|
|
192
213
|
"trace_to": ".ultra/specs/product.md#US-01"
|
|
193
214
|
}
|
|
194
215
|
```
|
|
195
216
|
|
|
196
|
-
**CLI fallback** (per task):
|
|
197
|
-
```bash
|
|
198
|
-
ultra-tools task create \
|
|
199
|
-
--title "Walking skeleton: …" --type architecture --priority P0 \
|
|
200
|
-
--complexity 4 --trace-to ".ultra/specs/product.md#US-01"
|
|
201
|
-
```
|
|
202
|
-
|
|
203
217
|
After each `task.create`, the server auto-runs the projector — `tasks.json`
|
|
204
218
|
and `contexts/task-{id}.md` (frontmatter only) appear.
|
|
205
219
|
|
|
@@ -299,16 +313,17 @@ Summary output:
|
|
|
299
313
|
| **Output (projected)** | `.ultra/tasks/tasks.json`, `.ultra/tasks/contexts/task-*.md` |
|
|
300
314
|
| **Next** | `/ultra-dev` |
|
|
301
315
|
|
|
302
|
-
## MCP
|
|
316
|
+
## MCP failure matrix
|
|
303
317
|
|
|
304
|
-
| Purpose | MCP tool |
|
|
305
|
-
|
|
318
|
+
| Purpose | MCP tool | Failure behavior |
|
|
319
|
+
|---------|----------|------------------|
|
|
306
320
|
| Scope mode prompt | none | current Host's native user-interaction surface |
|
|
307
|
-
| Create task | `task.create` |
|
|
321
|
+
| Create task | `task.create` | stop; surface the MCP error; never write a projection |
|
|
308
322
|
|
|
309
323
|
## What this skill DOES NOT do
|
|
310
324
|
|
|
311
325
|
- Does NOT write state.db directly — all task writes go through `task.create`
|
|
312
326
|
- Does NOT regenerate `tasks.json` — projector owns that
|
|
327
|
+
- Does NOT read `tasks.json` when MCP state is empty or unavailable
|
|
313
328
|
- Does NOT start implementation — that's `/ultra-dev`
|
|
314
329
|
- Does NOT re-run research — if specs are incomplete, redirects to `/ultra-research`
|
|
@@ -16,13 +16,20 @@ Single-call dashboard. Pulls task state from state.db (authoritative), layers
|
|
|
16
16
|
in `.ultra/test-report.json` + `.ultra/delivery-report.json` (file artifacts),
|
|
17
17
|
reports progress + risks + workflow routing.
|
|
18
18
|
|
|
19
|
+
## Authority failure boundary
|
|
20
|
+
|
|
21
|
+
`task.list` and `task.get` are the only task-state inputs. If either is
|
|
22
|
+
unavailable or returns an authority error, surface it and stop. Never fall back to `.ultra/tasks/tasks.json`.
|
|
23
|
+
On `LEGACY_STATE_MIGRATION_REQUIRED`, instruct the
|
|
24
|
+
user to run `ultra-tools migrate --from=4.4 --to=4.5 --source-dir <project-root>`.
|
|
25
|
+
|
|
19
26
|
## Workflow
|
|
20
27
|
|
|
21
28
|
### Phase 0 — Environment validation
|
|
22
29
|
|
|
23
30
|
| Check | If missing |
|
|
24
31
|
|-------|-----------|
|
|
25
|
-
|
|
|
32
|
+
| state.db reachable through `task.list` | suggest `/ultra-init` only when `.ultra/` is absent; otherwise surface the MCP error |
|
|
26
33
|
| At least one task visible via `task.list` | suggest `/ultra-plan` |
|
|
27
34
|
| `task.list` returns valid rows | surface error |
|
|
28
35
|
|
|
@@ -42,8 +49,6 @@ reports progress + risks + workflow routing.
|
|
|
42
49
|
// → { events: [...], next_since_id: M }
|
|
43
50
|
```
|
|
44
51
|
|
|
45
|
-
**CLI fallback**: `ultra-tools task list` + `ultra-tools session list`.
|
|
46
|
-
|
|
47
52
|
Also read:
|
|
48
53
|
- `.ultra/test-report.json` — if present
|
|
49
54
|
- `.ultra/delivery-report.json` — if present
|
|
@@ -132,12 +137,12 @@ When `task-id` provided:
|
|
|
132
137
|
Display: title, status, priority, complexity, deps, files_modified (if set),
|
|
133
138
|
session_id (if active), `context_file`, `trace_to`, stale flag, update history.
|
|
134
139
|
|
|
135
|
-
## MCP
|
|
140
|
+
## MCP and CLI matrix
|
|
136
141
|
|
|
137
142
|
| Purpose | MCP tool | CLI fallback |
|
|
138
143
|
|---------|----------|--------------|
|
|
139
|
-
| List tasks | `task.list` |
|
|
140
|
-
| Single task detail | `task.get` |
|
|
144
|
+
| List tasks | `task.list` | none; fail closed |
|
|
145
|
+
| Single task detail | `task.get` | none; fail closed |
|
|
141
146
|
| Cost panel (Phase 6.3) | — | `ultra-tools status --cost --json --since 7d` |
|
|
142
147
|
|
|
143
148
|
## Cost panel (Phase 6.3)
|
|
@@ -4,7 +4,6 @@ description: "Pre-delivery quality audit — Anti-Pattern + Coverage Gap + Wirin
|
|
|
4
4
|
runtime: all
|
|
5
5
|
mcp_tools_required:
|
|
6
6
|
- task.list
|
|
7
|
-
cli_fallback: "task list"
|
|
8
7
|
---
|
|
9
8
|
|
|
10
9
|
# ultra-test — Phase 3.5
|
|
@@ -14,6 +13,13 @@ each writes a JSON gate result; the skill aggregates into `.ultra/test-report.js
|
|
|
14
13
|
This is **not** for running unit tests (that is `/ultra-dev`). This is the
|
|
15
14
|
pre-ship gate.
|
|
16
15
|
|
|
16
|
+
## Authority failure boundary
|
|
17
|
+
|
|
18
|
+
The completed-task precheck requires MCP `task.list`. If it is unavailable or
|
|
19
|
+
returns an authority error, stop the audit. Never fall back to
|
|
20
|
+
`.ultra/tasks/tasks.json`. For `LEGACY_STATE_MIGRATION_REQUIRED`, instruct
|
|
21
|
+
`ultra-tools migrate --from=4.4 --to=4.5 --source-dir <project-root>`.
|
|
22
|
+
|
|
17
23
|
## Prerequisites
|
|
18
24
|
|
|
19
25
|
- `task.list` returns ≥1 task with `status=completed`
|
|
@@ -173,11 +179,11 @@ when `passed=true`.
|
|
|
173
179
|
| Performance | all Core Web Vitals (if frontend) |
|
|
174
180
|
| Security | 0 critical/high |
|
|
175
181
|
|
|
176
|
-
## MCP
|
|
182
|
+
## MCP failure matrix
|
|
177
183
|
|
|
178
184
|
| Purpose | MCP tool | CLI fallback |
|
|
179
185
|
|---------|----------|--------------|
|
|
180
|
-
| Confirm ≥1 completed task | `task.list { status: "completed" }` |
|
|
186
|
+
| Confirm ≥1 completed task | `task.list { status: "completed" }` | none; fail closed |
|
|
181
187
|
| Confirm risky auto-fix | none | current Host's native user-interaction surface |
|
|
182
188
|
|
|
183
189
|
## What this skill DOES NOT do
|