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.
Files changed (40) hide show
  1. package/CHANGELOG.md +38 -1
  2. package/README.md +1 -1
  3. package/adapters/_shared/codex-assets.cjs +1 -1
  4. package/adapters/codex.js +85 -13
  5. package/adapters/opencode.js +32 -1
  6. package/adapters/tests/claude.test.cjs +6 -0
  7. package/adapters/tests/codex-hook.test.cjs +25 -0
  8. package/adapters/tests/codex.test.cjs +7 -0
  9. package/adapters/tests/opencode.test.cjs +37 -0
  10. package/commands/ultra-dev.md +2 -1
  11. package/commands/ultra-plan.md +5 -1
  12. package/commands/ultra-status.md +2 -1
  13. package/commands/ultra-test.md +2 -1
  14. package/docs/COMMIT-HASH-BACKFILL.md +3 -3
  15. package/docs/RUNTIME-COMPAT-MATRIX.md +1 -1
  16. package/hooks/active_task_context.py +28 -0
  17. package/mcp-server/lib/projector.cjs +7 -2
  18. package/mcp-server/lib/state-authority.cjs +73 -0
  19. package/mcp-server/lib/state-db.cjs +19 -1
  20. package/mcp-server/lib/state-ops.cjs +3 -2
  21. package/mcp-server/server.cjs +2 -0
  22. package/mcp-server/tests/migrate.test.cjs +56 -1
  23. package/mcp-server/tests/projector.test.cjs +33 -1
  24. package/mcp-server/tests/server-contract.test.cjs +34 -1
  25. package/mcp-server/tests/state-db.test.cjs +31 -0
  26. package/mcp-server/tests/state-ops.test.cjs +15 -2
  27. package/package.json +1 -1
  28. package/skills/ultra-dev/SKILL.md +22 -17
  29. package/skills/ultra-plan/SKILL.md +28 -13
  30. package/skills/ultra-status/SKILL.md +11 -6
  31. package/skills/ultra-test/SKILL.md +9 -3
  32. package/spec/cli-protocol.md +12 -13
  33. package/spec/mcp-tools.yaml +7 -0
  34. package/spec/schemas/state-db.sql +3 -0
  35. package/spec/schemas/tasks.v4.5.schema.json +1 -0
  36. package/ultra-tools/cli.cjs +5 -5
  37. package/ultra-tools/commands/migrate.cjs +136 -48
  38. package/ultra-tools/commands/task.cjs +4 -6
  39. package/.ultra-template/tasks/contexts/TEMPLATE.md +0 -57
  40. 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.1';
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,
@@ -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.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.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
- - `.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