ultra-builder-pro-cli 0.5.0 → 0.5.1
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 +27 -1
- package/README.md +1 -1
- package/adapters/_shared/codex-assets.cjs +1 -1
- 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
package/ultra-tools/cli.cjs
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* ultra-tools — runtime-agnostic state engine for Ultra Builder Pro.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* migration, and explicit legacy-memory cleanup.
|
|
8
|
-
*
|
|
6
|
+
* Maintenance CLI for Ultra initialization, status, database operations,
|
|
7
|
+
* migration, and explicit legacy-memory cleanup. Authoritative task mutations
|
|
8
|
+
* stay on the live MCP server; this CLI is not a task-state fallback.
|
|
9
9
|
*
|
|
10
10
|
* Usage:
|
|
11
11
|
* ultra-tools <subcommand> [...]
|
|
@@ -33,8 +33,8 @@ USAGE:
|
|
|
33
33
|
ultra-tools <subcommand> [args]
|
|
34
34
|
|
|
35
35
|
SUBCOMMANDS:
|
|
36
|
-
task
|
|
37
|
-
session
|
|
36
|
+
task init-project
|
|
37
|
+
session close | get | list | admission | heartbeat | subscribe | reap
|
|
38
38
|
status [--cost] [--since <duration>] [--json]
|
|
39
39
|
db init | checkpoint | vacuum | integrity | backup (Phase 2)
|
|
40
40
|
migrate --from=4.4 --to=4.5 [--dry|--rollback] (Phase 2)
|
|
@@ -5,6 +5,7 @@ const path = require('node:path');
|
|
|
5
5
|
|
|
6
6
|
const { initStateDb, openStateDb, closeStateDb } = require('../../mcp-server/lib/state-db.cjs');
|
|
7
7
|
const ops = require('../../mcp-server/lib/state-ops.cjs');
|
|
8
|
+
const projector = require('../../mcp-server/lib/projector.cjs');
|
|
8
9
|
|
|
9
10
|
const SUPPORTED_FROM = '4.4';
|
|
10
11
|
const SUPPORTED_TO = '4.5';
|
|
@@ -78,12 +79,85 @@ function copyDirSync(src, dst) {
|
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
function isoTimestamp(value, fallback) {
|
|
83
|
+
const candidate = value || fallback;
|
|
84
|
+
const date = new Date(candidate);
|
|
85
|
+
if (Number.isNaN(date.getTime())) {
|
|
86
|
+
throw new Error(`migrate: invalid timestamp ${JSON.stringify(candidate)}`);
|
|
87
|
+
}
|
|
88
|
+
return date.toISOString();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function projectRelativeContextPath(sourceDir, value) {
|
|
92
|
+
if (!value) return null;
|
|
93
|
+
const normalized = String(value).replaceAll('\\', '/');
|
|
94
|
+
if (normalized.startsWith('.ultra/')) return normalized;
|
|
95
|
+
if (normalized.startsWith('contexts/')) return `.ultra/tasks/${normalized}`;
|
|
96
|
+
if (path.isAbsolute(value)) {
|
|
97
|
+
const relative = path.relative(sourceDir, value).replaceAll(path.sep, '/');
|
|
98
|
+
if (relative.startsWith('../') || relative === '..') {
|
|
99
|
+
throw new Error(`migrate: context_file escapes project root: ${value}`);
|
|
100
|
+
}
|
|
101
|
+
return relative;
|
|
102
|
+
}
|
|
103
|
+
return `.ultra/tasks/contexts/${path.basename(normalized)}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function sanitizeLegacyContextTemplate(sourceDir) {
|
|
107
|
+
const file = path.join(sourceDir, '.ultra', 'tasks', 'contexts', 'TEMPLATE.md');
|
|
108
|
+
if (!fs.existsSync(file)) return false;
|
|
109
|
+
const current = fs.readFileSync(file, 'utf8');
|
|
110
|
+
const next = current
|
|
111
|
+
.replace(/^>\s*\*\*Status\*\*:.*(?:\r?\n|$)(?:\r?\n)?/mi, '')
|
|
112
|
+
.replace(
|
|
113
|
+
/Read by mid_workflow_recall\.py and session_context\.py and injected into agent context\./g,
|
|
114
|
+
'Used as task-local acceptance criteria by the active Ultra workflow.',
|
|
115
|
+
);
|
|
116
|
+
if (next === current) return false;
|
|
117
|
+
fs.writeFileSync(file, next);
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function normalizeLegacyTask(task, tasksJson, contextHeaders, sourceDir) {
|
|
122
|
+
if (!task || typeof task !== 'object') throw new Error('migrate: every task must be an object');
|
|
123
|
+
for (const field of ['id', 'title', 'type', 'priority', 'status']) {
|
|
124
|
+
if (task[field] === undefined || task[field] === null || task[field] === '') {
|
|
125
|
+
throw new Error(`migrate: task ${task.id || '(unknown)'} missing ${field}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const deps = task.dependencies ?? task.deps ?? [];
|
|
129
|
+
if (!Array.isArray(deps)) throw new Error(`migrate: task ${task.id} dependencies must be an array`);
|
|
130
|
+
if (task.estimated_days !== undefined
|
|
131
|
+
&& (!Number.isFinite(task.estimated_days) || task.estimated_days <= 0)) {
|
|
132
|
+
throw new Error(`migrate: task ${task.id} estimated_days must be a positive number`);
|
|
133
|
+
}
|
|
134
|
+
const createdAt = isoTimestamp(task.created_at, tasksJson.created);
|
|
135
|
+
const updatedAt = isoTimestamp(task.updated_at, tasksJson.updated || tasksJson.created);
|
|
136
|
+
const ctx = contextHeaders[task.id];
|
|
137
|
+
const contextFile = projectRelativeContextPath(
|
|
138
|
+
sourceDir,
|
|
139
|
+
task.context_file || (ctx && ctx._file),
|
|
140
|
+
);
|
|
141
|
+
return {
|
|
142
|
+
...task,
|
|
143
|
+
deps,
|
|
144
|
+
estimated_days: task.estimated_days ?? null,
|
|
145
|
+
context_file: contextFile,
|
|
146
|
+
created_at: createdAt,
|
|
147
|
+
updated_at: updatedAt,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
81
151
|
function planForward(sourceDir) {
|
|
82
152
|
const tasksPath = path.join(sourceDir, '.ultra', 'tasks', 'tasks.json');
|
|
83
153
|
const tasksJson = readJsonOptional(tasksPath);
|
|
84
154
|
if (!tasksJson || !Array.isArray(tasksJson.tasks)) {
|
|
85
155
|
throw new Error(`migrate: tasks.json missing or malformed at ${tasksPath}`);
|
|
86
156
|
}
|
|
157
|
+
const version = String(tasksJson.version || tasksJson.schema_version || '');
|
|
158
|
+
if (version !== SUPPORTED_FROM) {
|
|
159
|
+
throw new Error(`migrate: expected v${SUPPORTED_FROM} tasks.json, found ${version || '(missing version)'}`);
|
|
160
|
+
}
|
|
87
161
|
|
|
88
162
|
const contextHeaders = {};
|
|
89
163
|
for (const file of findContexts(sourceDir)) {
|
|
@@ -107,9 +181,12 @@ function planForward(sourceDir) {
|
|
|
107
181
|
const eventsPath = path.join(sourceDir, '.ultra', 'activity-log.json');
|
|
108
182
|
const events = readJsonOptional(eventsPath);
|
|
109
183
|
const eventList = Array.isArray(events) ? events : [];
|
|
184
|
+
const tasks = tasksJson.tasks.map((task) => (
|
|
185
|
+
normalizeLegacyTask(task, tasksJson, contextHeaders, sourceDir)
|
|
186
|
+
));
|
|
110
187
|
|
|
111
188
|
return {
|
|
112
|
-
tasks
|
|
189
|
+
tasks,
|
|
113
190
|
events: eventList,
|
|
114
191
|
contextHeaders,
|
|
115
192
|
warnings,
|
|
@@ -118,50 +195,44 @@ function planForward(sourceDir) {
|
|
|
118
195
|
|
|
119
196
|
function applyForward(db, plan) {
|
|
120
197
|
const insertTask = db.prepare(
|
|
121
|
-
"INSERT INTO tasks (id, title, type, priority, complexity, status, deps, tag, trace_to, context_file, created_at, updated_at) VALUES (@id, @title, @type, @priority, @complexity, @status, @deps, @tag, @trace_to, @context_file, @created_at, @updated_at)",
|
|
198
|
+
"INSERT INTO tasks (id, title, type, priority, complexity, estimated_days, status, deps, tag, trace_to, context_file, created_at, updated_at) VALUES (@id, @title, @type, @priority, @complexity, @estimated_days, @status, @deps, @tag, @trace_to, @context_file, @created_at, @updated_at)",
|
|
122
199
|
);
|
|
123
200
|
const insertEvent = db.prepare(
|
|
124
201
|
"INSERT INTO events (ts, type, task_id, session_id, runtime, payload_json) VALUES (@ts, @type, @task_id, @session_id, @runtime, @payload)",
|
|
125
202
|
);
|
|
126
203
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
payload: e.payload === undefined ? null : JSON.stringify(e.payload),
|
|
160
|
-
});
|
|
161
|
-
n++;
|
|
162
|
-
}
|
|
163
|
-
return n;
|
|
164
|
-
});
|
|
204
|
+
let taskInserted = 0;
|
|
205
|
+
for (const t of plan.tasks) {
|
|
206
|
+
insertTask.run({
|
|
207
|
+
id: t.id,
|
|
208
|
+
title: t.title,
|
|
209
|
+
type: t.type,
|
|
210
|
+
priority: t.priority,
|
|
211
|
+
complexity: t.complexity ?? null,
|
|
212
|
+
estimated_days: t.estimated_days,
|
|
213
|
+
status: t.status,
|
|
214
|
+
deps: JSON.stringify(t.deps),
|
|
215
|
+
tag: t.tag ?? null,
|
|
216
|
+
trace_to: t.trace_to ?? null,
|
|
217
|
+
context_file: t.context_file,
|
|
218
|
+
created_at: t.created_at,
|
|
219
|
+
updated_at: t.updated_at,
|
|
220
|
+
});
|
|
221
|
+
taskInserted++;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let eventsInserted = 0;
|
|
225
|
+
for (const e of plan.events) {
|
|
226
|
+
insertEvent.run({
|
|
227
|
+
ts: e.ts || new Date().toISOString(),
|
|
228
|
+
type: e.type,
|
|
229
|
+
task_id: e.task_id ?? null,
|
|
230
|
+
session_id: e.session_id ?? null,
|
|
231
|
+
runtime: e.runtime ?? null,
|
|
232
|
+
payload: e.payload === undefined ? null : JSON.stringify(e.payload),
|
|
233
|
+
});
|
|
234
|
+
eventsInserted++;
|
|
235
|
+
}
|
|
165
236
|
|
|
166
237
|
return { taskInserted, eventsInserted };
|
|
167
238
|
}
|
|
@@ -220,14 +291,23 @@ function cmdForward(flags) {
|
|
|
220
291
|
let db;
|
|
221
292
|
try {
|
|
222
293
|
db = initStateDb(dbPath).db;
|
|
223
|
-
const counts =
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
294
|
+
const counts = ops.tx(db, () => {
|
|
295
|
+
const existing = db.prepare('SELECT COUNT(*) AS count FROM tasks').get().count;
|
|
296
|
+
if (existing > 0) {
|
|
297
|
+
throw new Error(`migrate: refusing to merge into non-empty state.db (tasks=${existing})`);
|
|
298
|
+
}
|
|
299
|
+
const inserted = applyForward(db, plan);
|
|
300
|
+
recordMigration(db, {
|
|
301
|
+
from: SUPPORTED_FROM,
|
|
302
|
+
to: SUPPORTED_TO,
|
|
303
|
+
direction: 'forward',
|
|
304
|
+
status: 'success',
|
|
305
|
+
notes: `tasks=${inserted.taskInserted} events=${inserted.eventsInserted} warnings=${plan.warnings.length}`,
|
|
306
|
+
});
|
|
307
|
+
return inserted;
|
|
230
308
|
});
|
|
309
|
+
projector.projectAll(db, { rootDir: sourceDir });
|
|
310
|
+
const contextTemplateSanitized = sanitizeLegacyContextTemplate(sourceDir);
|
|
231
311
|
emit({
|
|
232
312
|
ok: true,
|
|
233
313
|
data: {
|
|
@@ -240,6 +320,7 @@ function cmdForward(flags) {
|
|
|
240
320
|
tasks_inserted: counts.taskInserted,
|
|
241
321
|
events_inserted: counts.eventsInserted,
|
|
242
322
|
warnings: plan.warnings,
|
|
323
|
+
context_template_sanitized: contextTemplateSanitized,
|
|
243
324
|
},
|
|
244
325
|
});
|
|
245
326
|
return 0;
|
|
@@ -351,4 +432,11 @@ Rollback restores from the latest backup-v4.4-* directory and writes a
|
|
|
351
432
|
matching migration_history rollback row before dropping state.db.
|
|
352
433
|
`;
|
|
353
434
|
|
|
354
|
-
module.exports = {
|
|
435
|
+
module.exports = {
|
|
436
|
+
dispatch,
|
|
437
|
+
USAGE,
|
|
438
|
+
parseFlags,
|
|
439
|
+
planForward,
|
|
440
|
+
parseFrontmatter,
|
|
441
|
+
sanitizeLegacyContextTemplate,
|
|
442
|
+
};
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// CLI dispatcher for the
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// migrates — they share the same stdout envelope contract declared in
|
|
7
|
-
// spec/cli-protocol.md §2.
|
|
3
|
+
// CLI dispatcher for the only task-family maintenance command. Runtime task
|
|
4
|
+
// reads and writes intentionally remain MCP-only so a Host cannot silently
|
|
5
|
+
// bypass the state-authority and projection-conflict gates.
|
|
8
6
|
|
|
9
7
|
const { initProject, InitProjectError } = require('../../mcp-server/lib/init-project.cjs');
|
|
10
8
|
|
|
@@ -87,7 +85,7 @@ function dispatch(args) {
|
|
|
87
85
|
switch (verb) {
|
|
88
86
|
case 'init-project': return dispatchInitProject(rest);
|
|
89
87
|
default:
|
|
90
|
-
emit({ ok: false, error: { code: 'UNKNOWN_VERB', message: `unknown task verb '${verb}';
|
|
88
|
+
emit({ ok: false, error: { code: 'UNKNOWN_VERB', message: `unknown task verb '${verb}'; only init-project is supported by the maintenance CLI` } });
|
|
91
89
|
return 1;
|
|
92
90
|
}
|
|
93
91
|
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
# Task {id}: {title}
|
|
2
|
-
|
|
3
|
-
> **Status**: pending | **Priority**: {priority} | **Complexity**: {complexity}
|
|
4
|
-
|
|
5
|
-
## Context
|
|
6
|
-
|
|
7
|
-
**What**: [Clear description of what to build/change]
|
|
8
|
-
|
|
9
|
-
**Why**: [Business value linking to Persona + Scenario from specs]
|
|
10
|
-
|
|
11
|
-
**Constraints**:
|
|
12
|
-
- [Technical or business constraint 1]
|
|
13
|
-
- [Technical or business constraint 2]
|
|
14
|
-
|
|
15
|
-
## Implementation
|
|
16
|
-
|
|
17
|
-
**Target Files**:
|
|
18
|
-
- `path/to/file.ts` (create)
|
|
19
|
-
- `path/to/existing.ts` (modify: description)
|
|
20
|
-
|
|
21
|
-
**Pattern**: [Reference to existing code patterns to follow]
|
|
22
|
-
|
|
23
|
-
**Tech Notes**: [Framework/library specific guidance]
|
|
24
|
-
|
|
25
|
-
## Acceptance
|
|
26
|
-
|
|
27
|
-
**Tests**:
|
|
28
|
-
- [ ] `test command here`
|
|
29
|
-
- [ ] Pass: [scenario 1 description]
|
|
30
|
-
- [ ] Pass: [scenario 2 description]
|
|
31
|
-
|
|
32
|
-
**Verification**:
|
|
33
|
-
```bash
|
|
34
|
-
# Manual verification command
|
|
35
|
-
curl -X POST localhost:3000/api/example
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
## Trace
|
|
39
|
-
|
|
40
|
-
**Source**: `.ultra/specs/product.md#section-id`
|
|
41
|
-
|
|
42
|
-
## Change Log
|
|
43
|
-
|
|
44
|
-
| Date | Change | Specs Updated | Reason |
|
|
45
|
-
|------|--------|---------------|--------|
|
|
46
|
-
| {created_date} | Initial creation | - | Generated by /ultra-plan |
|
|
47
|
-
|
|
48
|
-
## Completion
|
|
49
|
-
|
|
50
|
-
> _Fill when task completed_
|
|
51
|
-
|
|
52
|
-
- **Completed**: {date}
|
|
53
|
-
- **Commit**: {hash}
|
|
54
|
-
- **Summary**: {brief description of what was delivered}
|
|
55
|
-
|
|
56
|
-
---
|
|
57
|
-
*Generated by /ultra-plan*
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": "4.4",
|
|
3
|
-
"created": "",
|
|
4
|
-
"updated": "",
|
|
5
|
-
"project": {
|
|
6
|
-
"name": "",
|
|
7
|
-
"type": "",
|
|
8
|
-
"stack": ""
|
|
9
|
-
},
|
|
10
|
-
"tasks": [],
|
|
11
|
-
"metadata": {
|
|
12
|
-
"totalTasks": 0,
|
|
13
|
-
"completedTasks": 0,
|
|
14
|
-
"inProgressTasks": 0,
|
|
15
|
-
"pendingTasks": 0,
|
|
16
|
-
"totalEstimatedDays": 0
|
|
17
|
-
},
|
|
18
|
-
"_schema": {
|
|
19
|
-
"_comment": "Lightweight task registry - full context in contexts/task-{id}.md",
|
|
20
|
-
"task_example": {
|
|
21
|
-
"id": "1",
|
|
22
|
-
"title": "Action verb + specific target",
|
|
23
|
-
"type": "feature|architecture|bugfix",
|
|
24
|
-
"priority": "P0|P1|P2|P3",
|
|
25
|
-
"complexity": "1-10",
|
|
26
|
-
"status": "pending|in_progress|completed|blocked",
|
|
27
|
-
"dependencies": ["task_id"],
|
|
28
|
-
"estimated_days": 2,
|
|
29
|
-
"context_file": "contexts/task-1.md",
|
|
30
|
-
"trace_to": ".ultra/specs/product.md#section-id"
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|