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.
Files changed (39) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/README.md +1 -1
  3. package/adapters/_shared/codex-assets.cjs +1 -1
  4. package/adapters/opencode.js +32 -1
  5. package/adapters/tests/claude.test.cjs +6 -0
  6. package/adapters/tests/codex-hook.test.cjs +25 -0
  7. package/adapters/tests/codex.test.cjs +7 -0
  8. package/adapters/tests/opencode.test.cjs +37 -0
  9. package/commands/ultra-dev.md +2 -1
  10. package/commands/ultra-plan.md +5 -1
  11. package/commands/ultra-status.md +2 -1
  12. package/commands/ultra-test.md +2 -1
  13. package/docs/COMMIT-HASH-BACKFILL.md +3 -3
  14. package/docs/RUNTIME-COMPAT-MATRIX.md +1 -1
  15. package/hooks/active_task_context.py +28 -0
  16. package/mcp-server/lib/projector.cjs +7 -2
  17. package/mcp-server/lib/state-authority.cjs +73 -0
  18. package/mcp-server/lib/state-db.cjs +19 -1
  19. package/mcp-server/lib/state-ops.cjs +3 -2
  20. package/mcp-server/server.cjs +2 -0
  21. package/mcp-server/tests/migrate.test.cjs +56 -1
  22. package/mcp-server/tests/projector.test.cjs +33 -1
  23. package/mcp-server/tests/server-contract.test.cjs +34 -1
  24. package/mcp-server/tests/state-db.test.cjs +31 -0
  25. package/mcp-server/tests/state-ops.test.cjs +15 -2
  26. package/package.json +1 -1
  27. package/skills/ultra-dev/SKILL.md +22 -17
  28. package/skills/ultra-plan/SKILL.md +28 -13
  29. package/skills/ultra-status/SKILL.md +11 -6
  30. package/skills/ultra-test/SKILL.md +9 -3
  31. package/spec/cli-protocol.md +12 -13
  32. package/spec/mcp-tools.yaml +7 -0
  33. package/spec/schemas/state-db.sql +3 -0
  34. package/spec/schemas/tasks.v4.5.schema.json +1 -0
  35. package/ultra-tools/cli.cjs +5 -5
  36. package/ultra-tools/commands/migrate.cjs +136 -48
  37. package/ultra-tools/commands/task.cjs +4 -6
  38. package/.ultra-template/tasks/contexts/TEMPLATE.md +0 -57
  39. package/.ultra-template/tasks/tasks.json +0 -33
@@ -3,9 +3,9 @@
3
3
  /**
4
4
  * ultra-tools — runtime-agnostic state engine for Ultra Builder Pro.
5
5
  *
6
- * Shell fallback for Ultra task/session state, status, database maintenance,
7
- * migration, and explicit legacy-memory cleanup. User interaction, skill
8
- * discovery, and subagent delegation stay on each host's native surfaces.
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 create | update | list | get | delete
37
- session spawn | close | get | list | admission | heartbeat | subscribe
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: tasksJson.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
- const taskInserted = ops.tx(db, () => {
128
- let n = 0;
129
- for (const t of plan.tasks) {
130
- const ctx = plan.contextHeaders[t.id];
131
- insertTask.run({
132
- id: t.id,
133
- title: t.title,
134
- type: t.type,
135
- priority: t.priority,
136
- complexity: t.complexity ?? null,
137
- status: t.status,
138
- deps: t.deps ? JSON.stringify(t.deps) : null,
139
- tag: t.tag ?? null,
140
- trace_to: t.trace_to ?? null,
141
- context_file: ctx ? ctx._file : null,
142
- created_at: t.created_at,
143
- updated_at: t.updated_at,
144
- });
145
- n++;
146
- }
147
- return n;
148
- });
149
-
150
- const eventsInserted = ops.tx(db, () => {
151
- let n = 0;
152
- for (const e of plan.events) {
153
- insertEvent.run({
154
- ts: e.ts || new Date().toISOString(),
155
- type: e.type,
156
- task_id: e.task_id ?? null,
157
- session_id: e.session_id ?? null,
158
- runtime: e.runtime ?? null,
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 = applyForward(db, plan);
224
- recordMigration(db, {
225
- from: SUPPORTED_FROM,
226
- to: SUPPORTED_TO,
227
- direction: 'forward',
228
- status: 'success',
229
- notes: `tasks=${counts.taskInserted} events=${counts.eventsInserted} warnings=${plan.warnings.length}`,
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 = { dispatch, USAGE, parseFlags, planForward, parseFrontmatter };
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 `task` family. Phase 3.1 implements `init-project`
4
- // as the fallback for /ultra-init when MCP is unreachable. Additional verbs
5
- // (create / update / list / ...) will be wired in Phase 3.3+ as each command
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}'; see spec/cli-protocol.md for supported task CLI verbs` } });
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
- }