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
@@ -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.schema_version, '4.4');
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.0",
3
+ "version": "0.5.1",
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
- - `.ultra/tasks/tasks.json` exists (from `/ultra-plan`)
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 → CLI fallback matrix
305
+ ## MCP failure matrix
301
306
 
302
- | Purpose | MCP tool (phase) | CLI / runtime fallback |
303
- |---------|------------------|------------------------|
304
- | Select pending task | `task.list` (2) / `task.get` (2) | `ultra-tools task list --status pending` |
305
- | Status → in_progress | `task.update` (2) | `ultra-tools task update <id> --status in_progress` |
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) | `ultra-tools task update <id> --status completed` |
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** | `.ultra/tasks/tasks.json`, `.ultra/tasks/contexts/task-<id>.md` body, state.db |
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), `dependencies` (task IDs), `estimated_days`,
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 → CLI fallback matrix
316
+ ## MCP failure matrix
303
317
 
304
- | Purpose | MCP tool | CLI fallback |
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` | `ultra-tools task create --title … --type … --priority …` |
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
- | `.ultra/tasks/tasks.json` projection present (or state.db reachable) | suggest `/ultra-init` |
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 → CLI fallback matrix
140
+ ## MCP and CLI matrix
136
141
 
137
142
  | Purpose | MCP tool | CLI fallback |
138
143
  |---------|----------|--------------|
139
- | List tasks | `task.list` | `ultra-tools task list` |
140
- | Single task detail | `task.get` | `ultra-tools task get <id>` |
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 → CLI fallback matrix
182
+ ## MCP failure matrix
177
183
 
178
184
  | Purpose | MCP tool | CLI fallback |
179
185
  |---------|----------|--------------|
180
- | Confirm ≥1 completed task | `task.list { status: "completed" }` | `ultra-tools 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
@@ -1,9 +1,9 @@
1
1
  # `ultra-tools` CLI protocol
2
2
 
3
- `ultra-tools` is the shell fallback for the live MCP state contract
4
- (PLAN §4, D12). Every tool in `spec/mcp-tools.yaml` has a matching
5
- `ultra-tools <subcommand>`; host-native review, impact discovery, skill
6
- resolution, and human interaction are intentionally outside this protocol.
3
+ `ultra-tools` is the maintenance CLI around the live MCP runtime (PLAN §4,
4
+ D12). The mapping registry below reserves stable command names for MCP tools;
5
+ it does not mean every mapping is executable. Task lifecycle workflows must use
6
+ the live MCP server and fail closed when it is unavailable.
7
7
 
8
8
  Trace: PLAN §6 Phase 1.4, decisions D12 / D29 / D33 / D37.
9
9
 
@@ -74,9 +74,10 @@ informational. Never rely on stderr for status.
74
74
 
75
75
  ## 5. Tool ↔ CLI mapping
76
76
 
77
- The mapping below is generated from `spec/mcp-tools.yaml` by
77
+ The compatibility mapping below is generated from `spec/mcp-tools.yaml` by
78
78
  `spec/scripts/check-cli-mapping.cjs` and asserted at gate time. This table
79
- is the authoritative reference; do not duplicate it in command md files.
79
+ is a naming contract, not an implementation inventory; do not advertise a row
80
+ as a fallback unless `ultra-tools <family> --help` lists it.
80
81
 
81
82
  | MCP tool | CLI subcommand | Phase | Writer |
82
83
  |---------------------------|-----------------------------|------:|---------|
@@ -102,13 +103,11 @@ is the authoritative reference; do not duplicate it in command md files.
102
103
  | `plan.export` | `plan export` | 8a | mcp |
103
104
  | `plan.get` | `plan get` | 8a | any |
104
105
 
105
- `Writer = mcp` rows go through the MCP server's single-writer connection;
106
- the CLI shells out to the running MCP server over stdio rather than
107
- touching `.ultra/state.db` directly. `Writer = any` rows are safe to
108
- execute from any process — read-only queries and `events` appends fall
109
- into this bucket because `events.id AUTOINCREMENT` makes concurrent
110
- INSERTs collision-free under WAL + `busy_timeout`. The full policy lives
111
- in `docs/STATE-DB-ACCESS-POLICY.md` (Phase 2.2 / R25).
106
+ Current executable maintenance surfaces are `task init-project`,
107
+ `session close|get|list|admission|heartbeat|subscribe|reap`, `status`, `db`,
108
+ `migrate`, and `legacy-memory`. In particular, `task create|update|list|get`
109
+ are not CLI fallbacks. The full access policy lives in
110
+ `docs/STATE-DB-ACCESS-POLICY.md` (Phase 2.2 / R25).
112
111
 
113
112
  ## 6. Versioning
114
113
 
@@ -14,6 +14,8 @@ info:
14
14
  Host-native review, impact, skill discovery, and human-interaction capabilities
15
15
  are documented by each adapter's capability map rather than advertised as MCP.
16
16
  Generators consume this YAML to derive types, validators, and CLI parsers (R19).
17
+ cli_subcommand values reserve stable names; runtime availability is defined by
18
+ ultra-tools <family> --help. Task lifecycle skills use MCP and fail closed.
17
19
  Adding a tool requires: (a) update this file, (b) add valid+invalid
18
20
  fixture to spec/fixtures/{valid,invalid}/, (c) `npm run test:spec` green.
19
21
 
@@ -40,6 +42,7 @@ tools:
40
42
  type: { type: string, enum: [architecture, feature, bugfix] }
41
43
  priority: { type: string, enum: [P0, P1, P2, P3] }
42
44
  complexity: { type: integer, minimum: 1, maximum: 10 }
45
+ estimated_days: { type: number, exclusiveMinimum: 0, description: 'estimated implementation effort in days' }
43
46
  deps: { type: array, items: { type: string } }
44
47
  files_modified: { type: array, items: { type: string }, description: 'Phase 8A conflict detection' }
45
48
  parent_id: { type: string, description: 'Phase 8A.3 parent reference for task.expand children' }
@@ -56,6 +59,7 @@ tools:
56
59
  errors:
57
60
  - { code: VALIDATION_ERROR, description: 'input failed schema validation' }
58
61
  - { code: DUPLICATE_TASK_ID, description: 'task id already exists' }
62
+ - { code: LEGACY_STATE_MIGRATION_REQUIRED, description: 'non-empty v4.4 projection must be migrated before state-backed tools can run' }
59
63
  - { code: STATE_DB_LOCKED, description: 'database is locked', retriable: true }
60
64
  cli_subcommand: task create
61
65
  emits_events: [task_created]
@@ -79,6 +83,7 @@ tools:
79
83
  status: { type: string, enum: [pending, in_progress, completed, blocked, expanded] }
80
84
  priority: { type: string, enum: [P0, P1, P2, P3] }
81
85
  complexity: { type: integer, minimum: 1, maximum: 10 }
86
+ estimated_days: { type: number, exclusiveMinimum: 0 }
82
87
  deps: { type: array, items: { type: string } }
83
88
  files_modified: { type: array, items: { type: string } }
84
89
  session_id: { type: string }
@@ -96,6 +101,7 @@ tools:
96
101
  - { code: TASK_NOT_FOUND, description: 'no task with given id' }
97
102
  - { code: ILLEGAL_STATUS_TRANSITION, description: 'transition rejected by state machine' }
98
103
  - { code: VALIDATION_ERROR, description: 'patch failed schema validation' }
104
+ - { code: LEGACY_STATE_MIGRATION_REQUIRED, description: 'non-empty v4.4 projection must be migrated before state-backed tools can run' }
99
105
  cli_subcommand: task update
100
106
  emits_events: [task_started, task_completed, task_blocked, task_stale_marked]
101
107
  trace_to: '§7.3, §7.2'
@@ -123,6 +129,7 @@ tools:
123
129
  count: { type: integer, minimum: 0 }
124
130
  errors:
125
131
  - { code: VALIDATION_ERROR, description: 'filter failed schema validation' }
132
+ - { code: LEGACY_STATE_MIGRATION_REQUIRED, description: 'non-empty v4.4 projection must be migrated before state-backed tools can run' }
126
133
  cli_subcommand: task list
127
134
  emits_events: []
128
135
  trace_to: '§7.3'
@@ -21,6 +21,7 @@ CREATE TABLE IF NOT EXISTS tasks (
21
21
  type TEXT NOT NULL CHECK (type IN ('architecture', 'feature', 'bugfix')),
22
22
  priority TEXT NOT NULL CHECK (priority IN ('P0', 'P1', 'P2', 'P3')),
23
23
  complexity INTEGER CHECK (complexity BETWEEN 1 AND 10),
24
+ estimated_days REAL CHECK (estimated_days IS NULL OR estimated_days > 0),
24
25
  status TEXT NOT NULL DEFAULT 'pending'
25
26
  CHECK (status IN ('pending', 'in_progress', 'completed', 'blocked', 'expanded')),
26
27
  deps TEXT, -- JSON array of task ids
@@ -152,3 +153,5 @@ INSERT OR IGNORE INTO schema_version (version, description)
152
153
  VALUES ('5.2', 'Phase 5.2 — circuit_breaker table for per-task trip tracking');
153
154
  INSERT OR IGNORE INTO schema_version (version, description)
154
155
  VALUES ('8A.1', 'Phase 8A.1 — tasks.parent_id for task.expand parent→children');
156
+ INSERT OR IGNORE INTO schema_version (version, description)
157
+ VALUES ('8A.2', 'Phase 8A.2 — tasks.estimated_days preserved across MCP and v4.4 migration');
@@ -26,6 +26,7 @@
26
26
  "type": { "type": "string", "enum": ["architecture", "feature", "bugfix"] },
27
27
  "priority": { "type": "string", "enum": ["P0", "P1", "P2", "P3"] },
28
28
  "complexity": { "type": "integer", "minimum": 1, "maximum": 10 },
29
+ "estimated_days": { "type": "number", "exclusiveMinimum": 0 },
29
30
  "status": {
30
31
  "type": "string",
31
32
  "enum": ["pending", "in_progress", "completed", "blocked", "expanded"],