ultra-builder-pro-cli 0.20.0 → 0.21.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 (102) hide show
  1. package/.ultra-template/docs/research/README.md +26 -5
  2. package/.ultra-template/specs/discovery.md +1 -1
  3. package/.ultra-template/tasks/tasks.json +1 -1
  4. package/CHANGELOG.md +66 -0
  5. package/README.md +54 -24
  6. package/adapters/_shared/codex-assets.cjs +69 -11
  7. package/adapters/_shared/interaction-contract.cjs +36 -3
  8. package/adapters/_shared/runtime-assets.cjs +41 -1
  9. package/adapters/claude.js +7 -5
  10. package/adapters/codex.js +1 -1
  11. package/adapters/kimi.js +2 -8
  12. package/adapters/opencode.js +30 -28
  13. package/bin/orchestrator.js +64 -24
  14. package/docs/AGENT-CONTEXT.md +74 -43
  15. package/docs/ARCHITECTURE.md +63 -31
  16. package/docs/ARTIFACT-AUTHORITY.md +234 -0
  17. package/docs/DECISIONS.md +10 -4
  18. package/docs/LEGACY-CLI-CROSSWALK.md +6 -4
  19. package/docs/PLUGIN-ISOLATION-CONTRACT.md +4 -3
  20. package/docs/ROADMAP.md +19 -5
  21. package/docs/RUNTIME-COMPAT-MATRIX.md +24 -13
  22. package/docs/STATE-DB-ACCESS-POLICY.md +11 -9
  23. package/docs/WORKFLOW-LIFECYCLE.md +70 -39
  24. package/hooks/README.md +6 -5
  25. package/hooks/active_task_context.py +97 -12
  26. package/hooks/context_spine.py +16 -4
  27. package/hooks/health_check.py +5 -4
  28. package/hooks/pre_stop_check.py +5 -2
  29. package/hooks/runtime_paths.py +396 -0
  30. package/hooks/subagent_tracker.py +5 -2
  31. package/hooks/workflow_checkpoint.py +16 -3
  32. package/hooks/workflow_context.py +11 -3
  33. package/hooks/workflow_resume.py +7 -4
  34. package/mcp-server/breadcrumb.cjs +20 -2
  35. package/mcp-server/lib/archive-journal.cjs +1026 -111
  36. package/mcp-server/lib/archive-mutation-worker.py +365 -0
  37. package/mcp-server/lib/artifact-registry.cjs +1126 -0
  38. package/mcp-server/lib/baseline-workflow.cjs +9 -4
  39. package/mcp-server/lib/change-packet.cjs +818 -0
  40. package/mcp-server/lib/change-workflow.cjs +932 -176
  41. package/mcp-server/lib/context-paths.cjs +111 -0
  42. package/mcp-server/lib/context-spine.cjs +511 -45
  43. package/mcp-server/lib/decision-dialogue.cjs +345 -3
  44. package/mcp-server/lib/delivery-transaction.cjs +471 -0
  45. package/mcp-server/lib/doctor-backup-worker.cjs +328 -0
  46. package/mcp-server/lib/doctor.cjs +373 -11
  47. package/mcp-server/lib/git-bootstrap.cjs +459 -47
  48. package/mcp-server/lib/init-project.cjs +190 -95
  49. package/mcp-server/lib/plan-publication-worker.cjs +830 -0
  50. package/mcp-server/lib/plan-store.cjs +700 -5
  51. package/mcp-server/lib/project-breadcrumb.cjs +23 -4
  52. package/mcp-server/lib/projector.cjs +174 -40
  53. package/mcp-server/lib/runtime-paths.cjs +1687 -0
  54. package/mcp-server/lib/safe-project-file.cjs +385 -0
  55. package/mcp-server/lib/spec-learning.cjs +28 -12
  56. package/mcp-server/lib/state-db.cjs +263 -9
  57. package/mcp-server/lib/state-ops.cjs +248 -49
  58. package/mcp-server/lib/telemetry.cjs +12 -3
  59. package/mcp-server/lib/workflow-state.cjs +610 -58
  60. package/mcp-server/server.cjs +278 -33
  61. package/orchestrator/daemon.cjs +2 -10
  62. package/orchestrator/parallel-orchestrator.cjs +314 -50
  63. package/orchestrator/planner/plan-builder.cjs +1 -1
  64. package/orchestrator/recovery.cjs +125 -7
  65. package/orchestrator/session-close-journal-worker.cjs +5 -0
  66. package/orchestrator/session-close-journal.cjs +1014 -0
  67. package/orchestrator/session-runner.cjs +679 -69
  68. package/orchestrator/worktree-manager.cjs +186 -35
  69. package/package.json +2 -3
  70. package/skills/cc-collab/SKILL.md +4 -1
  71. package/skills/codex-collab/SKILL.md +5 -2
  72. package/skills/ultra-change/SKILL.md +26 -8
  73. package/skills/ultra-deliver/SKILL.md +23 -11
  74. package/skills/ultra-dev/SKILL.md +33 -6
  75. package/skills/ultra-doctor/SKILL.md +6 -2
  76. package/skills/ultra-init/SKILL.md +4 -2
  77. package/skills/ultra-plan/SKILL.md +49 -16
  78. package/skills/ultra-research/SKILL.md +30 -27
  79. package/skills/ultra-review/SKILL.md +9 -5
  80. package/skills/ultra-status/SKILL.md +9 -4
  81. package/skills/ultra-test/SKILL.md +10 -5
  82. package/skills/ultra-think/SKILL.md +6 -2
  83. package/skills/ultra-think/references/decision-dialogue.md +17 -6
  84. package/skills/ultra-verify/SKILL.md +4 -1
  85. package/spec/cli-protocol.md +18 -7
  86. package/spec/mcp-tools.yaml +502 -44
  87. package/spec/schemas/change-delta.v1.schema.json +153 -0
  88. package/spec/schemas/documentation-reconciliation.v1.schema.json +106 -0
  89. package/spec/schemas/mcp-tools.schema.json +1 -1
  90. package/spec/schemas/state-db.sql +50 -13
  91. package/spec/schemas/tasks.v4.5.schema.json +1 -1
  92. package/ultra-tools/cli.cjs +3 -2
  93. package/ultra-tools/commands/db.cjs +76 -25
  94. package/ultra-tools/commands/migrate.cjs +995 -80
  95. package/ultra-tools/commands/session.cjs +34 -8
  96. package/ultra-tools/commands/status.cjs +32 -6
  97. package/ultra-tools/commands/system.cjs +129 -22
  98. package/commands/learn.md +0 -10
  99. package/orchestrator/skill-miner.cjs +0 -133
  100. package/output-styles/coding-vibes.md +0 -67
  101. package/output-styles/structural-thinking.md +0 -61
  102. package/skills/learn/SKILL.md +0 -58
@@ -7,6 +7,7 @@ const Database = require('better-sqlite3');
7
7
 
8
8
  const { EXPECTED_VERSION, REQUIRED_TABLES } = require('./state-db.cjs');
9
9
  const { readBreadcrumb } = require('./context-spine.cjs');
10
+ const runtimePaths = require('./runtime-paths.cjs');
10
11
 
11
12
  function blockedBreadcrumb(code, {
12
13
  allowedTransitions = [],
@@ -26,6 +27,7 @@ function blockedBreadcrumb(code, {
26
27
  context_manifest_hash: null,
27
28
  git_head: null,
28
29
  baseline: null,
30
+ accepted_intent: [],
29
31
  };
30
32
  }
31
33
 
@@ -43,6 +45,12 @@ function doctorBreadcrumb(code) {
43
45
  });
44
46
  }
45
47
 
48
+ function compactText(value, maximum = 240) {
49
+ const normalized = String(value || '').replace(/\s+/g, ' ').trim();
50
+ if (normalized.length <= maximum) return normalized;
51
+ return `${normalized.slice(0, maximum - 3).trimEnd()}...`;
52
+ }
53
+
46
54
  function latestSchemaVersion(db) {
47
55
  return db.prepare(
48
56
  'SELECT version FROM schema_version ORDER BY applied_at DESC, rowid DESC LIMIT 1',
@@ -59,7 +67,7 @@ function readProjectBreadcrumb(rootDir) {
59
67
  const root = path.resolve(rootDir || process.cwd());
60
68
  const ultraDir = path.join(root, '.ultra');
61
69
  if (!fs.existsSync(ultraDir)) return null;
62
- const dbPath = path.join(ultraDir, 'state.db');
70
+ const dbPath = runtimePaths.locateStateDb(root);
63
71
  if (!fs.existsSync(dbPath)) {
64
72
  return blockedBreadcrumb('STATE_DB_MISSING', {
65
73
  allowedTransitions: ['ultra-init', 'ultra-status'],
@@ -131,8 +139,17 @@ function renderProjectBreadcrumb(rootDir, breadcrumb) {
131
139
  lines.push(
132
140
  `Decision thread: ${breadcrumb.decision.thread_id} (${breadcrumb.decision.status}/${breadcrumb.decision.mode})`,
133
141
  current
134
- ? `Decision: ${current.id} — ${current.question}`
135
- : 'Decision: checkpoint confirmation required',
142
+ ? `Decision: ${current.id} — ${compactText(current.question)}`
143
+ : (breadcrumb.decision.status === 'checkpoint_ready'
144
+ ? 'Decision: checkpoint confirmation required'
145
+ : 'Decision: normalized intent recorded; application and read-back remain'),
146
+ );
147
+ }
148
+ for (const accepted of (breadcrumb.accepted_intent || []).slice(0, 3)) {
149
+ const value = accepted.decision
150
+ || (accepted.consequences ? `Deferred: ${accepted.consequences}` : 'Normalized state recorded');
151
+ lines.push(
152
+ `Accepted intent: ${accepted.decision_id} [${accepted.authority || accepted.status}] ${compactText(value)}`,
136
153
  );
137
154
  }
138
155
  lines.push(`Allowed transitions: ${(breadcrumb.allowed_transitions || []).join(', ') || 'none'}`);
@@ -144,7 +161,9 @@ function renderProjectBreadcrumb(rootDir, breadcrumb) {
144
161
  `Context: ${breadcrumb.context_manifest_path} sha256=${breadcrumb.context_manifest_hash || 'unknown'}`,
145
162
  );
146
163
  }
147
- lines.push('Authority: .ultra/state.db; JSON/Markdown remain projections or evidence artifacts.');
164
+ lines.push(
165
+ 'Lifecycle authority: .ultra/.runtime/state.db; digest-bound files carry semantic and evidence content; projections remain non-authoritative.',
166
+ );
148
167
  return lines.join('\n');
149
168
  }
150
169
 
@@ -1,9 +1,10 @@
1
1
  'use strict';
2
2
 
3
- // Projector: regenerates the read-only file views from .ultra/state.db.
3
+ // Projector: regenerates the read-only file views from .ultra/.runtime/state.db.
4
4
  //
5
- // state.db is the only authority (D32). tasks.json and contexts/task-*.md
6
- // are projections — humans may read them, never write them. The projector
5
+ // state.db owns lifecycle and artifact bindings (D32); registered digest-bound
6
+ // files own semantic/evidence bodies. tasks.json and contexts/task-*.md are
7
+ // complete projections — humans may read them, never write them. The projector
7
8
  // is trigger-based: state-ops calls projectAll() after each successful
8
9
  // write transaction. Projection output passes the v4.5 schemas under
9
10
  // spec/schemas/ — see mcp-server/tests/projector.test.cjs for the
@@ -11,9 +12,14 @@
11
12
 
12
13
  const fs = require('node:fs');
13
14
  const path = require('node:path');
15
+ const crypto = require('node:crypto');
16
+
17
+ const ops = require('./state-ops.cjs');
18
+ const artifacts = require('./artifact-registry.cjs');
19
+ const contextPaths = require('./context-paths.cjs');
14
20
 
15
21
  const SCHEMA_VERSION = '4.5';
16
- const SOURCE_TAG = '.ultra/state.db';
22
+ const SOURCE_TAG = '.ultra/.runtime/state.db';
17
23
 
18
24
  // Frozen SELECTs — values bind through @placeholders.
19
25
  const TASK_PROJECTION_COLUMNS = 'id, title, type, priority, complexity, estimated_days, status, deps, files_modified, session_id, stale, tag, trace_to, outcome, slice_kind, public_seam, verification_command, acceptance_json, context_refs_json, docs_impact_json, ownership_json, context_file, completion_commit, created_at, updated_at';
@@ -84,7 +90,7 @@ function projectTasks(db, { tasksJson } = {}, opts = {}) {
84
90
  return { path: target, count: rows.length };
85
91
  }
86
92
 
87
- function buildContextDoc(taskRow, existingBody) {
93
+ function buildContextDoc(taskRow) {
88
94
  const headerLines = [
89
95
  '---',
90
96
  `task_id: ${taskRow.id}`,
@@ -97,31 +103,20 @@ function buildContextDoc(taskRow, existingBody) {
97
103
  if (taskRow.completion_commit) headerLines.push(`completion_commit: ${taskRow.completion_commit}`);
98
104
  if (taskRow.stale) headerLines.push('stale: true');
99
105
  headerLines.push(`schema_version: ${SCHEMA_VERSION}`);
106
+ headerLines.push(`generated_by: ${contextPaths.GENERATED_BY}`);
100
107
  headerLines.push(`generated_at: ${taskRow.updated_at || new Date().toISOString()}`);
101
108
  headerLines.push('---', '');
102
109
 
103
- let body = existingBody || '';
104
- // v4.4 contexts carried a second generated status banner in the Markdown
105
- // body. Once frontmatter is authoritative, retaining that line creates two
106
- // state surfaces that can disagree. Remove only the legacy generated line;
107
- // all task-authored body content remains intact.
108
- body = body.replace(/^>\s*\*\*Status\*\*:.*(?:\r?\n|$)(?:\r?\n)?/mi, '');
109
- // Strip any previous STALE banner so re-projection after reset is clean.
110
- body = body.replace(/^> ⚠️\s*STALE[^\n]*\n(?:>[^\n]*\n)*\n?/m, '');
111
- body = body.replace(
112
- new RegExp(`${CONTRACT_START}[\\s\\S]*?${CONTRACT_END}\\n*`, 'm'),
113
- '',
114
- );
110
+ const body = [];
115
111
  if (taskRow.stale) {
116
- const banner = [
112
+ body.push([
117
113
  `> ⚠️ STALE since ${taskRow.updated_at || new Date().toISOString()}`,
118
114
  '> Specification evidence may have changed. Recompile the task context before continuing.',
119
115
  '',
120
- ].join('\n');
121
- body = banner + body;
116
+ ].join('\n'));
122
117
  }
123
- body = buildTaskContract(taskRow) + body;
124
- return headerLines.join('\n') + body;
118
+ body.push(buildTaskContract(taskRow));
119
+ return headerLines.join('\n') + body.join('');
125
120
  }
126
121
 
127
122
  function buildTaskContract(task) {
@@ -167,43 +162,180 @@ function escapeYaml(str) {
167
162
  return str;
168
163
  }
169
164
 
170
- function extractBodyFromExistingFile(file) {
171
- if (!fs.existsSync(file)) return '';
172
- const text = fs.readFileSync(file, 'utf8');
173
- if (!text.startsWith('---')) return text;
174
- const end = text.indexOf('\n---', 3);
175
- if (end === -1) return text;
176
- return text.slice(end + 4).replace(/^\n/, '');
165
+ function legacyContextParseError(message) {
166
+ const error = new Error(message);
167
+ error.code = 'LEGACY_CONTEXT_PARSE_ERROR';
168
+ return error;
169
+ }
170
+
171
+ function extractAuthoredContextBytes(contents) {
172
+ const original = Buffer.isBuffer(contents) ? contents : Buffer.from(contents);
173
+ let body = original.toString('utf8');
174
+ const frontmatter = /^(?:\uFEFF)?---(?:\r\n|\n)[\s\S]*?(?:\r\n|\n)---(?:(?:\r\n|\n)|$)/.exec(body);
175
+ if (frontmatter) body = body.slice(frontmatter[0].length);
176
+ const staleBanner = /^> ⚠️ STALE[^\r\n]*(?:(?:\r\n|\n)>[^\r\n]*)*(?:(?:\r\n|\n){1,2}|$)/.exec(body);
177
+ if (staleBanner) body = body.slice(staleBanner[0].length);
178
+
179
+ let cursor = 0;
180
+ while (true) {
181
+ const start = body.indexOf(CONTRACT_START, cursor);
182
+ if (start === -1) break;
183
+ const markerEnd = body.indexOf(CONTRACT_END, start + CONTRACT_START.length);
184
+ if (markerEnd === -1) {
185
+ throw legacyContextParseError(
186
+ 'legacy task context has an unterminated generated execution contract',
187
+ );
188
+ }
189
+ let end = markerEnd + CONTRACT_END.length;
190
+ if (body.startsWith('\r\n', end)) end += 2;
191
+ else if (body.startsWith('\n', end)) end += 1;
192
+ const removeFrom = body.slice(0, start).trim() === '' ? 0 : start;
193
+ body = body.slice(0, removeFrom) + body.slice(end);
194
+ cursor = removeFrom;
195
+ }
196
+ return body.trim() === '' ? null : Buffer.from(body, 'utf8');
197
+ }
198
+
199
+ function promoteAuthoredContext(db, taskId, target, rootDir) {
200
+ if (!fs.existsSync(target.file)) return null;
201
+ const stat = fs.lstatSync(target.file);
202
+ if (!stat.isFile() || stat.isSymbolicLink()) {
203
+ throw new contextPaths.ContextPathError(
204
+ `task context must be a regular file: ${target.relative}`,
205
+ );
206
+ }
207
+ const sourceBytes = fs.readFileSync(target.file);
208
+ const bytes = extractAuthoredContextBytes(sourceBytes);
209
+ if (!bytes) return null;
210
+ const digest = crypto.createHash('sha256').update(bytes).digest('hex');
211
+ const sourceDigest = crypto.createHash('sha256').update(sourceBytes).digest('hex');
212
+ const safeTaskId = String(taskId).replace(/[^a-zA-Z0-9_-]+/g, '-');
213
+ const relative = `.ultra/docs/migration/legacy-context/${safeTaskId}-${digest.slice(0, 12)}.md`;
214
+ const file = path.resolve(rootDir, ...relative.split('/'));
215
+ const existed = fs.existsSync(file);
216
+ if (existed) {
217
+ const existingDigest = crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
218
+ if (existingDigest !== digest) {
219
+ throw new Error(`legacy context promotion digest conflict: ${relative}`);
220
+ }
221
+ } else {
222
+ writeAtomic(file, bytes);
223
+ }
224
+ try {
225
+ const recorded = artifacts.recordArtifact(db, {
226
+ owner_type: 'task',
227
+ owner_id: taskId,
228
+ kind: 'legacy_context_findings',
229
+ path: relative,
230
+ content_digest: digest,
231
+ source_refs: [{
232
+ type: 'external',
233
+ id: `legacy-context:${target.relative}`,
234
+ relation: 'imported_from',
235
+ }],
236
+ consumer_refs: [{ type: 'task', id: taskId, relation: 'consumed_by' }],
237
+ provenance: {
238
+ writer: 'ultra-projector',
239
+ operation: 'legacy-context-promotion',
240
+ },
241
+ metadata: {
242
+ original_path: target.relative,
243
+ original_digest: sourceDigest,
244
+ authored_body_digest: digest,
245
+ },
246
+ }, { rootDir });
247
+ return { path: relative, digest, artifact_id: recorded.artifact.id };
248
+ } catch (error) {
249
+ if (!existed) fs.rmSync(file, { force: true });
250
+ throw error;
251
+ }
177
252
  }
178
253
 
179
254
  function projectContext(db, taskId, { contextsDir } = {}, opts = {}) {
180
255
  const row = db.prepare(READ_TASK_FOR_PROJECTION_SQL).get({ id: taskId });
181
256
  if (!row) return null;
182
257
  const projection = rowToProjection(row);
183
- const dir = contextsDir || defaultPaths(opts.rootDir || '.').contextsDir;
184
- const target = projection.context_file
185
- ? path.resolve(opts.rootDir || '.', projection.context_file)
186
- : path.join(dir, `task-${taskId}.md`);
187
- const body = extractBodyFromExistingFile(target);
188
- const next = buildContextDoc(projection, body);
189
- writeAtomic(target, next);
190
- return { path: target, task_id: taskId };
258
+ const rootDir = path.resolve(opts.rootDir || '.');
259
+ if (contextsDir) {
260
+ const supplied = path.resolve(contextsDir);
261
+ const canonical = defaultPaths(rootDir).contextsDir;
262
+ if (supplied !== canonical) {
263
+ throw new contextPaths.ContextPathError(
264
+ `custom context projection roots are not allowed: ${contextsDir}`,
265
+ );
266
+ }
267
+ }
268
+ const target = contextPaths.resolveContextPath(rootDir, projection.context_file, { taskId });
269
+ const promoted = promoteAuthoredContext(db, taskId, target, rootDir);
270
+ const next = buildContextDoc(projection);
271
+ writeAtomic(target.file, next);
272
+ return { path: target.file, task_id: taskId, promoted };
273
+ }
274
+
275
+ function listContextFiles(directory) {
276
+ if (!fs.existsSync(directory)) return [];
277
+ const output = [];
278
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
279
+ const file = path.join(directory, entry.name);
280
+ if (entry.isSymbolicLink()) continue;
281
+ if (entry.isDirectory()) output.push(...listContextFiles(file));
282
+ else if (entry.isFile()) output.push(file);
283
+ }
284
+ return output;
285
+ }
286
+
287
+ function pruneGeneratedGhosts(db, rootDir, expected) {
288
+ const contextsDir = defaultPaths(rootDir).contextsDir;
289
+ const pruned = [];
290
+ for (const file of listContextFiles(contextsDir)) {
291
+ const relative = path.relative(rootDir, file).split(path.sep).join('/');
292
+ if (expected.has(relative) || path.basename(file) === 'TEMPLATE.md') continue;
293
+ const contents = fs.readFileSync(file);
294
+ if (!contextPaths.isGeneratedContextContents(contents)) continue;
295
+ const staged = `${file}.prune-${process.pid}-${Date.now()}`;
296
+ fs.renameSync(file, staged);
297
+ try {
298
+ ops.appendEvent(db, {
299
+ type: 'projection_pruned',
300
+ payload: { path: relative, reason: 'task_missing' },
301
+ });
302
+ fs.rmSync(staged, { force: true });
303
+ pruned.push(relative);
304
+ } catch (error) {
305
+ fs.renameSync(staged, file);
306
+ throw error;
307
+ }
308
+ }
309
+ return pruned.sort();
191
310
  }
192
311
 
193
312
  function projectAll(db, { rootDir = '.', tasksJson, contextsDir } = {}) {
194
- const paths = defaultPaths(rootDir);
313
+ const resolvedRoot = path.resolve(rootDir);
314
+ const paths = defaultPaths(resolvedRoot);
195
315
  const tasksTarget = tasksJson || paths.tasksJson;
196
316
  const contextsTarget = contextsDir || paths.contextsDir;
317
+ if (path.resolve(contextsTarget) !== paths.contextsDir) {
318
+ throw new contextPaths.ContextPathError(
319
+ `custom context projection roots are not allowed: ${contextsTarget}`,
320
+ );
321
+ }
197
322
 
323
+ const rows = db.prepare("SELECT id, context_file FROM tasks").all();
324
+ const expected = new Set(rows.map((row) => (
325
+ contextPaths.resolveContextPath(resolvedRoot, row.context_file, { taskId: row.id }).relative
326
+ )));
198
327
  const tasksResult = projectTasks(db, { tasksJson: tasksTarget });
199
328
  const contextResults = [];
200
- const rows = db.prepare("SELECT id FROM tasks").all();
201
329
  for (const r of rows) {
202
- contextResults.push(projectContext(db, r.id, { contextsDir: contextsTarget }, { rootDir }));
330
+ contextResults.push(projectContext(
331
+ db, r.id, { contextsDir: contextsTarget }, { rootDir: resolvedRoot },
332
+ ));
203
333
  }
334
+ const pruned = pruneGeneratedGhosts(db, resolvedRoot, expected);
204
335
  return {
205
336
  tasks_json: tasksResult,
206
337
  contexts: contextResults.filter(Boolean),
338
+ pruned,
207
339
  };
208
340
  }
209
341
 
@@ -214,4 +346,6 @@ module.exports = {
214
346
  projectContext,
215
347
  projectAll,
216
348
  rowToProjection,
349
+ extractAuthoredContextBytes,
350
+ promoteAuthoredContext,
217
351
  };