ultra-builder-pro-cli 0.6.0 → 0.8.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 (75) hide show
  1. package/.ultra-template/changes/active/.gitkeep +1 -0
  2. package/.ultra-template/changes/archive/.gitkeep +1 -0
  3. package/CHANGELOG.md +90 -11
  4. package/README.md +39 -19
  5. package/adapters/_shared/handbook.cjs +8 -7
  6. package/adapters/_shared/provenance.cjs +286 -0
  7. package/adapters/_shared/runtime-assets.cjs +2 -0
  8. package/adapters/_shared/tests/handbook.test.cjs +4 -1
  9. package/adapters/_shared/tests/provenance.test.cjs +133 -0
  10. package/adapters/_shared/tests/runtime-assets.test.cjs +2 -0
  11. package/adapters/claude.js +33 -0
  12. package/adapters/codex.js +87 -0
  13. package/adapters/opencode.js +147 -20
  14. package/adapters/tests/codex.test.cjs +42 -3
  15. package/adapters/tests/opencode.test.cjs +16 -9
  16. package/agents/debugger.md +12 -0
  17. package/bin/install.js +63 -27
  18. package/bin/orchestrator.js +1 -26
  19. package/commands/ultra-change.md +48 -0
  20. package/commands/ultra-deliver.md +14 -7
  21. package/commands/ultra-dev.md +6 -3
  22. package/commands/ultra-doctor.md +36 -0
  23. package/commands/ultra-init.md +4 -3
  24. package/commands/ultra-status.md +7 -5
  25. package/commands/ultra-test.md +2 -1
  26. package/docs/AGENT-CONTEXT.md +31 -23
  27. package/docs/ARCHITECTURE.md +80 -17
  28. package/docs/PLAN.zh-CN.md +37 -29
  29. package/docs/ROADMAP.md +14 -14
  30. package/docs/RUNTIME-COMPAT-MATRIX.md +19 -12
  31. package/hooks/README.md +14 -9
  32. package/hooks/active_task_context.py +22 -15
  33. package/hooks/health_check.py +136 -38
  34. package/hooks/workflow_checkpoint.py +15 -1
  35. package/hooks/workflow_context.py +120 -30
  36. package/hooks/workflow_resume.py +96 -7
  37. package/mcp-server/lib/change-workflow.cjs +625 -0
  38. package/mcp-server/lib/change-workflow.test.cjs +346 -0
  39. package/mcp-server/lib/doctor.cjs +112 -0
  40. package/mcp-server/lib/doctor.test.cjs +86 -0
  41. package/mcp-server/lib/init-project.cjs +32 -6
  42. package/mcp-server/lib/runtime-state.cjs +222 -0
  43. package/mcp-server/lib/runtime-state.test.cjs +92 -0
  44. package/mcp-server/lib/state-db.cjs +31 -8
  45. package/mcp-server/lib/state-ops.cjs +17 -3
  46. package/mcp-server/server.cjs +102 -10
  47. package/mcp-server/tests/db-ops.test.cjs +5 -1
  48. package/mcp-server/tests/init-project.test.cjs +45 -0
  49. package/mcp-server/tests/server-contract.test.cjs +123 -1
  50. package/mcp-server/tests/state-db.test.cjs +44 -7
  51. package/mcp-server/tests/state-ops.test.cjs +19 -0
  52. package/orchestrator/daemon.cjs +39 -0
  53. package/orchestrator/tests/daemon.test.cjs +50 -0
  54. package/package.json +3 -3
  55. package/settings.json +1 -2
  56. package/skills/ultra-change/SKILL.md +149 -0
  57. package/skills/ultra-deliver/SKILL.md +83 -15
  58. package/skills/ultra-dev/SKILL.md +32 -12
  59. package/skills/ultra-doctor/SKILL.md +97 -0
  60. package/skills/ultra-init/SKILL.md +8 -5
  61. package/skills/ultra-review/SKILL.md +16 -0
  62. package/skills/ultra-status/SKILL.md +30 -7
  63. package/skills/ultra-test/SKILL.md +14 -2
  64. package/spec/cli-protocol.md +12 -1
  65. package/spec/mcp-tools.yaml +337 -1
  66. package/spec/schemas/mcp-tools.schema.json +1 -1
  67. package/spec/schemas/state-db.sql +117 -0
  68. package/ultra-tools/cli.cjs +3 -0
  69. package/ultra-tools/commands/system.cjs +57 -0
  70. package/ultra-tools/system-command.test.cjs +53 -0
  71. package/adapters/_shared/rtk-detect.cjs +0 -86
  72. package/adapters/_shared/tests/rtk-detect.test.cjs +0 -116
  73. package/docs/LEGACY-HERMES.md +0 -1099
  74. package/orchestrator/code-graph-watcher.cjs +0 -121
  75. package/orchestrator/tests/code-graph-watcher.test.cjs +0 -183
@@ -0,0 +1,133 @@
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
+ const { execFileSync } = require('node:child_process');
9
+
10
+ const provenance = require('../provenance.cjs');
11
+
12
+ function fixture() {
13
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-provenance-'));
14
+ fs.mkdirSync(path.join(root, 'runtime'), { recursive: true });
15
+ fs.mkdirSync(path.join(root, 'hooks'), { recursive: true });
16
+ fs.writeFileSync(path.join(root, 'runtime', 'launch.cjs'), 'module.exports = true;\n');
17
+ fs.writeFileSync(path.join(root, 'hooks', 'hooks.json'), '{}\n');
18
+ const file = path.join(root, 'provenance.json');
19
+ provenance.writeProvenance({
20
+ file,
21
+ adapter: 'claude',
22
+ packageInfo: { name: 'ultra-builder-pro-cli', version: '1.2.3' },
23
+ repository: 'https://example.test/ultra-builder-pro',
24
+ sourceCommit: '0123456789abcdef0123456789abcdef01234567',
25
+ roots: { plugin: root },
26
+ assets: [
27
+ { root: 'plugin', path: 'hooks/hooks.json' },
28
+ { root: 'plugin', path: 'runtime/launch.cjs' },
29
+ ],
30
+ contracts: {
31
+ hooks_manifest: { root: 'plugin', path: 'hooks/hooks.json' },
32
+ mcp_launcher: { root: 'plugin', path: 'runtime/launch.cjs' },
33
+ },
34
+ });
35
+ return { root, file };
36
+ }
37
+
38
+ test('writeProvenance + inspectProvenance produce a normalized healthy report', () => {
39
+ const fx = fixture();
40
+ try {
41
+ const manifest = JSON.parse(fs.readFileSync(fx.file, 'utf8'));
42
+ assert.equal(manifest.schema_version, 1);
43
+ assert.equal(manifest.adapter, 'claude');
44
+ assert.equal(manifest.package.version, '1.2.3');
45
+ assert.equal(manifest.source.repository, 'https://example.test/ultra-builder-pro');
46
+ assert.match(manifest.content.digest, /^[0-9a-f]{64}$/);
47
+ assert.deepEqual(manifest.assets.map((asset) => asset.path), [
48
+ 'hooks/hooks.json', 'runtime/launch.cjs',
49
+ ]);
50
+
51
+ const report = provenance.inspectProvenance({
52
+ file: fx.file,
53
+ expectedAdapter: 'claude',
54
+ expectedPackageVersion: '1.2.3',
55
+ });
56
+ assert.equal(report.status, 'healthy');
57
+ assert.deepEqual(report.issues, []);
58
+ assert.equal(report.checks.assets.checked, 2);
59
+ assert.equal(report.checks.contracts.checked, 2);
60
+ } finally {
61
+ fs.rmSync(fx.root, { recursive: true, force: true });
62
+ }
63
+ });
64
+
65
+ test('inspectProvenance detects content drift and missing contract targets', () => {
66
+ const fx = fixture();
67
+ try {
68
+ fs.writeFileSync(path.join(fx.root, 'runtime', 'launch.cjs'), 'tampered\n');
69
+ fs.unlinkSync(path.join(fx.root, 'hooks', 'hooks.json'));
70
+ const report = provenance.inspectProvenance({
71
+ file: fx.file,
72
+ expectedAdapter: 'claude',
73
+ expectedPackageVersion: '1.2.3',
74
+ });
75
+ assert.equal(report.status, 'degraded');
76
+ assert.ok(report.issues.some((issue) => issue.code === 'ASSET_HASH_MISMATCH'));
77
+ assert.ok(report.issues.some(
78
+ (issue) => issue.code === 'CONTRACT_TARGET_MISSING' && issue.contract === 'hooks_manifest',
79
+ ));
80
+ } finally {
81
+ fs.rmSync(fx.root, { recursive: true, force: true });
82
+ }
83
+ });
84
+
85
+ test('inspectProvenance fails closed for missing, corrupt, or mismatched manifests', () => {
86
+ const fx = fixture();
87
+ try {
88
+ const mismatch = provenance.inspectProvenance({
89
+ file: fx.file,
90
+ expectedAdapter: 'codex',
91
+ expectedPackageVersion: '9.9.9',
92
+ });
93
+ assert.equal(mismatch.status, 'degraded');
94
+ assert.ok(mismatch.issues.some((issue) => issue.code === 'ADAPTER_MISMATCH'));
95
+ assert.ok(mismatch.issues.some((issue) => issue.code === 'PACKAGE_VERSION_MISMATCH'));
96
+
97
+ fs.writeFileSync(fx.file, 'not-json');
98
+ const corrupt = provenance.inspectProvenance({ file: fx.file });
99
+ assert.equal(corrupt.status, 'degraded');
100
+ assert.equal(corrupt.issues[0].code, 'PROVENANCE_INVALID');
101
+
102
+ fs.unlinkSync(fx.file);
103
+ const missing = provenance.inspectProvenance({ file: fx.file });
104
+ assert.equal(missing.status, 'missing');
105
+ assert.equal(missing.issues[0].code, 'PROVENANCE_MISSING');
106
+ } finally {
107
+ fs.rmSync(fx.root, { recursive: true, force: true });
108
+ }
109
+ });
110
+
111
+ test('packageSource never attributes an enclosing consumer repository commit to the package', () => {
112
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-provenance-source-'));
113
+ try {
114
+ execFileSync('git', ['init', '-q'], { cwd: root });
115
+ execFileSync('git', ['config', 'user.email', 'test@ubp.dev'], { cwd: root });
116
+ execFileSync('git', ['config', 'user.name', 'ubp-test'], { cwd: root });
117
+ fs.writeFileSync(path.join(root, 'README.md'), '# consumer\n');
118
+ execFileSync('git', ['add', 'README.md'], { cwd: root });
119
+ execFileSync('git', ['commit', '-q', '-m', 'seed'], { cwd: root });
120
+ const packageRoot = path.join(root, 'node_modules', 'ultra-builder-pro-cli');
121
+ fs.mkdirSync(packageRoot, { recursive: true });
122
+ fs.writeFileSync(path.join(packageRoot, 'package.json'), JSON.stringify({
123
+ name: 'ultra-builder-pro-cli',
124
+ version: '1.2.3',
125
+ repository: { url: 'https://example.test/ultra-builder-pro' },
126
+ }));
127
+
128
+ const source = provenance.packageSource(packageRoot);
129
+ assert.equal(source.sourceCommit, null);
130
+ } finally {
131
+ fs.rmSync(root, { recursive: true, force: true });
132
+ }
133
+ });
@@ -27,6 +27,8 @@ const CORE = [
27
27
  'ultra-deliver',
28
28
  'ultra-status',
29
29
  'ultra-think',
30
+ 'ultra-change',
31
+ 'ultra-doctor',
30
32
  ];
31
33
 
32
34
  const INTERNAL = [
@@ -15,6 +15,7 @@ const {
15
15
  writeAtomic,
16
16
  } = require('./_shared/file-ops.cjs');
17
17
  const { buildMcpRuntime } = require('./_shared/codex-assets.cjs');
18
+ const provenance = require('./_shared/provenance.cjs');
18
19
  const {
19
20
  CORE_PUBLIC_SKILLS,
20
21
  WORKFLOW_HOOK_FILES,
@@ -26,6 +27,7 @@ const MCP_SERVER_NAME = PLUGIN_NAME;
26
27
  const SOURCE_TAG = 'ubp';
27
28
  const SENTINEL_KEY = '_ubp_manifest';
28
29
  const COMMAND_NAMES = CORE_PUBLIC_SKILLS.filter((name) => name !== 'ultra-review');
30
+ const PROVENANCE_FILE = 'provenance.json';
29
31
 
30
32
  function resolveTarget(ctx = {}) {
31
33
  if (ctx.configDir) return ctx.configDir;
@@ -155,9 +157,39 @@ function install(ctx = {}) {
155
157
  license: pkg.license || 'MIT',
156
158
  }, null, 2) + '\n');
157
159
  markManaged(target, { adapter: 'claude', plugin: PLUGIN_NAME });
160
+ const source = provenance.packageSource(repoRoot);
161
+ const provenanceFile = path.join(target, PROVENANCE_FILE);
162
+ report.provenance = provenance.writeProvenance({
163
+ file: provenanceFile,
164
+ adapter: 'claude',
165
+ ...source,
166
+ roots: { plugin: target },
167
+ assets: provenance.assetRefsForTree('plugin', target, {
168
+ exclude: ['.ubp-managed', PROVENANCE_FILE],
169
+ }),
170
+ contracts: {
171
+ plugin_manifest: { root: 'plugin', path: '.claude-plugin/plugin.json' },
172
+ mcp_registration: { root: 'plugin', path: '.mcp.json' },
173
+ mcp_launcher: { root: 'plugin', path: 'runtime/launch.cjs' },
174
+ hooks_manifest: { root: 'plugin', path: 'hooks/hooks.json' },
175
+ checkpoint_hook: { root: 'plugin', path: 'hooks/workflow_checkpoint.py' },
176
+ resume_hook: { root: 'plugin', path: 'hooks/workflow_resume.py' },
177
+ },
178
+ });
179
+ report.provenance.file = provenanceFile;
158
180
  return report;
159
181
  }
160
182
 
183
+ function doctor(ctx = {}) {
184
+ const repoRoot = resolveRepoRoot(ctx);
185
+ const source = provenance.packageSource(repoRoot);
186
+ return provenance.inspectProvenance({
187
+ file: path.join(resolvePluginRoot(ctx), PROVENANCE_FILE),
188
+ expectedAdapter: 'claude',
189
+ expectedPackageVersion: source.packageInfo.version,
190
+ });
191
+ }
192
+
161
193
  function uninstall(ctx = {}) {
162
194
  const target = resolvePluginRoot(ctx);
163
195
  const report = { target, removed: {}, config: { updated: false } };
@@ -179,4 +211,5 @@ module.exports = {
179
211
  resolvePluginRoot,
180
212
  install,
181
213
  uninstall,
214
+ doctor,
182
215
  };
package/adapters/codex.js CHANGED
@@ -33,6 +33,7 @@ const {
33
33
  buildPlugin,
34
34
  installAgents,
35
35
  } = require('./_shared/codex-assets.cjs');
36
+ const provenance = require('./_shared/provenance.cjs');
36
37
 
37
38
  const MCP_SERVER_NAME = PLUGIN_NAME;
38
39
  const SOURCE_TAG = 'ubp';
@@ -40,6 +41,7 @@ const MARKER_BEGIN = '# >>> ultra-builder-pro managed block — do not edit by h
40
41
  const MARKER_END = '# <<< ultra-builder-pro managed block';
41
42
  const RUNTIME_MANIFEST_DIR = 'ultra-builder-pro';
42
43
  const RUNTIME_MANIFEST_FILE = 'install-manifest.json';
44
+ const PROVENANCE_FILE = 'provenance.json';
43
45
  const HOOK_ADAPTER_RELATIVE = path.join('hooks', 'adapters', 'codex.py');
44
46
 
45
47
  function resolveTarget(ctx = {}) {
@@ -355,6 +357,30 @@ function install(ctx = {}) {
355
357
  agents: agents.installed,
356
358
  hookCacheVersions,
357
359
  });
360
+ const source = provenance.packageSource(repoRoot);
361
+ const provenanceFile = path.join(configDir, RUNTIME_MANIFEST_DIR, PROVENANCE_FILE);
362
+ const provenanceManifest = provenance.writeProvenance({
363
+ file: provenanceFile,
364
+ adapter: 'codex',
365
+ ...source,
366
+ roots: { plugin: pluginRoot, config: configDir },
367
+ assets: [
368
+ ...provenance.assetRefsForTree('plugin', pluginRoot, {
369
+ exclude: ['.ubp-managed'],
370
+ }),
371
+ ...agents.installed.map((file) => ({ root: 'config', path: path.join('agents', file) })),
372
+ { root: 'config', path: path.join(RUNTIME_MANIFEST_DIR, RUNTIME_MANIFEST_FILE) },
373
+ ],
374
+ contracts: {
375
+ plugin_manifest: { root: 'plugin', path: '.codex-plugin/plugin.json' },
376
+ mcp_registration: { root: 'plugin', path: '.mcp.json' },
377
+ mcp_launcher: { root: 'plugin', path: 'runtime/launch.cjs' },
378
+ hooks_manifest: { root: 'plugin', path: 'hooks/hooks.json' },
379
+ hook_adapter: { root: 'plugin', path: HOOK_ADAPTER_RELATIVE },
380
+ runtime_manifest: { root: 'config', path: path.join(RUNTIME_MANIFEST_DIR, RUNTIME_MANIFEST_FILE) },
381
+ },
382
+ });
383
+ provenanceManifest.file = provenanceFile;
358
384
 
359
385
  return {
360
386
  target: configDir,
@@ -365,10 +391,70 @@ function install(ctx = {}) {
365
391
  hookCompatibility,
366
392
  legacy,
367
393
  manifestFile,
394
+ provenance: provenanceManifest,
368
395
  config: { updated: legacy.configUpdated },
369
396
  };
370
397
  }
371
398
 
399
+ function doctor(ctx = {}) {
400
+ const configDir = resolveTarget(ctx);
401
+ const pluginRoot = resolvePluginRoot(ctx);
402
+ const repoRoot = resolveRepoRoot(ctx);
403
+ const marketplaceFile = resolveMarketplaceFile(ctx);
404
+ const source = provenance.packageSource(repoRoot);
405
+ const report = provenance.inspectProvenance({
406
+ file: path.join(configDir, RUNTIME_MANIFEST_DIR, PROVENANCE_FILE),
407
+ expectedAdapter: 'codex',
408
+ expectedPackageVersion: source.packageInfo.version,
409
+ });
410
+
411
+ let runtimeManifest = null;
412
+ try {
413
+ runtimeManifest = readRuntimeManifest(configDir);
414
+ } catch (error) {
415
+ report.issues.push({ code: 'RUNTIME_MANIFEST_INVALID', message: error.message });
416
+ }
417
+ const runtimeManifestOk = runtimeManifest?.source === SOURCE_TAG
418
+ && runtimeManifest.adapter === 'codex'
419
+ && runtimeManifest.plugin?.root === pluginRoot
420
+ && typeof runtimeManifest.plugin?.version === 'string';
421
+ if (!runtimeManifestOk && !report.issues.some((entry) => entry.code === 'RUNTIME_MANIFEST_INVALID')) {
422
+ report.issues.push({ code: 'RUNTIME_MANIFEST_INVALID', path: runtimeManifestFile(configDir) });
423
+ }
424
+ report.checks.runtime_manifest = { status: runtimeManifestOk ? 'pass' : 'fail' };
425
+
426
+ let marketplaceOk = false;
427
+ try {
428
+ const marketplace = loadMarketplace(marketplaceFile);
429
+ const entry = marketplace.plugins.find((plugin) => plugin?.name === PLUGIN_NAME);
430
+ marketplaceOk = entry?.source?.source === 'local'
431
+ && entry.source.path === `./plugins/${PLUGIN_NAME}`;
432
+ } catch (error) {
433
+ report.issues.push({ code: 'MARKETPLACE_REGISTRATION_INVALID', message: error.message });
434
+ }
435
+ if (!marketplaceOk && !report.issues.some((entry) => entry.code === 'MARKETPLACE_REGISTRATION_INVALID')) {
436
+ report.issues.push({ code: 'MARKETPLACE_REGISTRATION_INVALID', path: marketplaceFile });
437
+ }
438
+ report.checks.marketplace = { status: marketplaceOk ? 'pass' : 'fail' };
439
+
440
+ let hookTargetsOk = runtimeManifestOk;
441
+ if (runtimeManifestOk) {
442
+ const cacheRoot = pluginCacheRoot(configDir, runtimeManifest.marketplace?.name || 'personal');
443
+ const currentCacheRequired = shouldRunPluginCli(ctx);
444
+ for (const version of runtimeManifest.hook_cache_versions || []) {
445
+ if (version === runtimeManifest.plugin.version && !currentCacheRequired) continue;
446
+ const adapter = path.join(cacheRoot, version, HOOK_ADAPTER_RELATIVE);
447
+ if (!fs.existsSync(adapter)) {
448
+ hookTargetsOk = false;
449
+ report.issues.push({ code: 'HOOK_TARGET_MISSING', version, path: adapter });
450
+ }
451
+ }
452
+ }
453
+ report.checks.hook_targets = { status: hookTargetsOk ? 'pass' : 'fail' };
454
+ if (report.status !== 'missing') report.status = report.issues.length === 0 ? 'healthy' : 'degraded';
455
+ return report;
456
+ }
457
+
372
458
  function removeManagedAgents(configDir, manifest) {
373
459
  const agentRoot = path.join(configDir, 'agents');
374
460
  const requested = manifest && Array.isArray(manifest.agents)
@@ -440,6 +526,7 @@ module.exports = {
440
526
  resolveMarketplaceFile,
441
527
  install,
442
528
  uninstall,
529
+ doctor,
443
530
  _internal: {
444
531
  stripManagedBlock,
445
532
  hasManagedBlock,
@@ -15,6 +15,7 @@ const {
15
15
  writeAtomic,
16
16
  } = require('./_shared/file-ops.cjs');
17
17
  const { buildMcpRuntime } = require('./_shared/codex-assets.cjs');
18
+ const provenance = require('./_shared/provenance.cjs');
18
19
  const {
19
20
  CORE_PUBLIC_SKILLS,
20
21
  skillsForRuntime,
@@ -34,6 +35,7 @@ const SOURCE_TAG = 'ubp';
34
35
  const BUNDLE_DIR = '.ultra-builder-pro';
35
36
  const MANAGED_TEXT_MARKER = '<!-- ultra-builder-pro:managed -->';
36
37
  const PLUGIN_MARKER = '// Managed by Ultra Builder Pro.';
38
+ const PROVENANCE_FILE = 'provenance.json';
37
39
  const COMMAND_NAMES = CORE_PUBLIC_SKILLS.filter((name) => name !== 'ultra-review');
38
40
 
39
41
  function resolveTarget(ctx = {}) {
@@ -337,20 +339,55 @@ import path from "node:path";
337
339
 
338
340
  const TERMINAL = new Set(["committed", "completed", "done", "cancelled"]);
339
341
  const TASKS_PROJECTION = ".ultra/tasks/tasks.json";
342
+ const PROVIDER_FIELDS = new Set(["provider", "project", "reference", "revision", "indexed_head", "status"]);
343
+
344
+ function providerMetadata(value) {
345
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
346
+ const clean = {};
347
+ for (const [kind, ref] of Object.entries(value)) {
348
+ if (!["memory", "code_graph"].includes(kind) || !ref || typeof ref !== "object" || Array.isArray(ref)) continue;
349
+ clean[kind] = Object.fromEntries(Object.entries(ref).filter(([key]) => PROVIDER_FIELDS.has(key)));
350
+ }
351
+ return clean;
352
+ }
340
353
 
341
- function findActiveWorkflow(directory) {
354
+ function findUltraContext(directory) {
342
355
  let current = path.resolve(directory);
343
356
  while (true) {
344
- const file = path.join(current, ".ultra", "workflow-state.json");
345
- if (fs.existsSync(file)) {
357
+ const ultra = path.join(current, ".ultra");
358
+ const workflowFile = path.join(ultra, "workflow-state.json");
359
+ const statePresent = fs.existsSync(path.join(ultra, "state.db"));
360
+ let workflow = null;
361
+ if (fs.existsSync(workflowFile)) {
346
362
  try {
347
- const state = JSON.parse(fs.readFileSync(file, "utf8"));
348
- return state && !TERMINAL.has(state.status) ? { root: current, state } : null;
363
+ const state = JSON.parse(fs.readFileSync(workflowFile, "utf8"));
364
+ workflow = state && !TERMINAL.has(state.status) ? state : null;
349
365
  } catch (error) {
350
- process.stderr.write(\`[ultra-builder-pro] cannot read \${file}: \${error.message}\\n\`);
351
- return null;
366
+ process.stderr.write(\`[ultra-builder-pro] cannot read \${workflowFile}: \${error.message}\\n\`);
367
+ }
368
+ }
369
+ const changes = [];
370
+ const changeRoot = path.join(ultra, "changes", "active");
371
+ if (fs.existsSync(changeRoot)) {
372
+ for (const entry of fs.readdirSync(changeRoot, { withFileTypes: true })) {
373
+ if (!entry.isDirectory()) continue;
374
+ const manifestPath = path.join(changeRoot, entry.name, "context-manifest.json");
375
+ if (!fs.existsSync(manifestPath)) continue;
376
+ try {
377
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
378
+ if (manifest?.change) changes.push({
379
+ ...manifest.change,
380
+ providers: providerMetadata(manifest.providers),
381
+ manifestPath: path.relative(current, manifestPath),
382
+ });
383
+ } catch (error) {
384
+ process.stderr.write(\`[ultra-builder-pro] cannot read \${manifestPath}: \${error.message}\\n\`);
385
+ }
352
386
  }
353
387
  }
388
+ if (statePresent || workflow || changes.length > 0) {
389
+ return { root: current, workflow, changes, statePresent };
390
+ }
354
391
  const parent = path.dirname(current);
355
392
  if (parent === current) return null;
356
393
  current = parent;
@@ -358,16 +395,42 @@ function findActiveWorkflow(directory) {
358
395
  }
359
396
 
360
397
  function contextText(active) {
361
- const state = active.state;
362
- return [
363
- "[Ultra active workflow]",
364
- \`Project: \${active.root}\`,
365
- \`Command: \${state.command ?? "unknown"}\`,
366
- \`Task: \${state.task_id ?? state.task ?? "unknown"}\`,
367
- \`Step: \${state.step ?? "unknown"}\`,
368
- \`Status: \${state.status ?? "active"}\`,
369
- "Authority: .ultra/state.db; recovery: .ultra/workflow-state.json.",
370
- ].join("\\n");
398
+ const lines = [];
399
+ if (active.workflow) {
400
+ const state = active.workflow;
401
+ lines.push(
402
+ "[Ultra active workflow]",
403
+ \`Project: \${active.root}\`,
404
+ \`Command: \${state.command ?? "unknown"}\`,
405
+ \`Task: \${state.task_id ?? state.task ?? "unknown"}\`,
406
+ \`Step: \${state.step ?? "unknown"}\`,
407
+ \`Status: \${state.status ?? "active"}\`,
408
+ );
409
+ }
410
+ if (active.changes.length > 0) {
411
+ const change = active.changes[0];
412
+ lines.push(
413
+ "[Ultra continuous change]",
414
+ \`Project: \${active.root}\`,
415
+ \`Change: \${change.id} (\${change.kind ?? "unknown"}, \${change.status ?? "active"})\`,
416
+ \`Title: \${change.title ?? "unknown"}\`,
417
+ \`Intent: \${change.intent ?? "unknown"}\`,
418
+ \`Context manifest: \${change.manifestPath}\`,
419
+ \`External providers (metadata references only): \${JSON.stringify(change.providers)}\`,
420
+ );
421
+ if (active.changes.length > 1) lines.push(
422
+ \`Active change count: \${active.changes.length}; resolve scope with change.list before mutation.\`,
423
+ );
424
+ }
425
+ if (!active.workflow && active.changes.length === 0) {
426
+ lines.push(
427
+ "[Ultra baseline]",
428
+ \`Project: \${active.root}\`,
429
+ "No active continuous change. Start daily work with the ultra-change workflow.",
430
+ );
431
+ }
432
+ lines.push("Authority: .ultra/state.db; generated JSON/Markdown are projections or artifacts.");
433
+ return lines.join("\\n");
371
434
  }
372
435
 
373
436
  function targetPaths(tool, args) {
@@ -391,8 +454,8 @@ function isTasksProjection(active, candidate) {
391
454
 
392
455
  export const UltraBuilderProPlugin = async ({ directory, worktree }) => {
393
456
  const root = worktree || directory;
394
- let active = findActiveWorkflow(root);
395
- const refresh = () => { active = findActiveWorkflow(root); };
457
+ let active = findUltraContext(root);
458
+ const refresh = () => { active = findUltraContext(root); };
396
459
  return {
397
460
  event: async ({ event }) => {
398
461
  if (["session.created", "session.compacted", "session.idle"].includes(event?.type)) refresh();
@@ -413,7 +476,7 @@ export const UltraBuilderProPlugin = async ({ directory, worktree }) => {
413
476
  if (targetPaths(tool, output?.args).some((candidate) => isTasksProjection(active, candidate))) {
414
477
  throw new Error(
415
478
  "Ultra Builder Pro refused a direct write to .ultra/tasks/tasks.json. " +
416
- ".ultra/state.db is authoritative; use MCP task tools or run the required v4.4 to v4.5 migration."
479
+ ".ultra/state.db is authoritative; use MCP task tools and run ultra-doctor when state or projection health is degraded."
417
480
  );
418
481
  }
419
482
  },
@@ -454,6 +517,69 @@ function install(ctx = {}) {
454
517
  delete next[LEGACY_SENTINEL_KEY];
455
518
  writeAtomic(configFile, JSON.stringify(next, null, 2) + '\n');
456
519
  report.config.updated = true;
520
+ const source = provenance.packageSource(repoRoot);
521
+ const bundleAssets = provenance.assetRefsForTree('config', bundleRoot, {
522
+ exclude: [PROVENANCE_FILE],
523
+ }).map((asset) => ({ ...asset, path: path.join(BUNDLE_DIR, asset.path) }));
524
+ const skillMarkers = skillsForRuntime('opencode').map((name) => ({
525
+ root: 'config', path: path.join('skills', name, '.ubp-managed'),
526
+ }));
527
+ const provenanceFile = path.join(bundleRoot, PROVENANCE_FILE);
528
+ report.provenance = provenance.writeProvenance({
529
+ file: provenanceFile,
530
+ adapter: 'opencode',
531
+ ...source,
532
+ roots: { config: target },
533
+ assets: [
534
+ ...report.copied.commands.map((file) => ({ root: 'config', path: path.join('commands', file) })),
535
+ ...report.copied.skills.map((file) => ({ root: 'config', path: path.join('skills', file) })),
536
+ ...skillMarkers,
537
+ ...report.copied.agents.map((file) => ({ root: 'config', path: path.join('agents', file) })),
538
+ { root: 'config', path: path.join('plugins', 'ultra-builder-pro.js') },
539
+ ...bundleAssets,
540
+ ],
541
+ contracts: {
542
+ host_plugin: { root: 'config', path: path.join('plugins', 'ultra-builder-pro.js') },
543
+ mcp_config: { root: 'config', path: 'opencode.json' },
544
+ mcp_launcher: { root: 'config', path: path.join(BUNDLE_DIR, 'runtime', 'launch.cjs') },
545
+ },
546
+ });
547
+ report.provenance.file = provenanceFile;
548
+ return report;
549
+ }
550
+
551
+ function doctor(ctx = {}) {
552
+ const target = resolveTarget(ctx);
553
+ const repoRoot = resolveRepoRoot(ctx);
554
+ const source = provenance.packageSource(repoRoot);
555
+ const report = provenance.inspectProvenance({
556
+ file: path.join(target, BUNDLE_DIR, PROVENANCE_FILE),
557
+ expectedAdapter: 'opencode',
558
+ expectedPackageVersion: source.packageInfo.version,
559
+ });
560
+ const configFile = path.join(target, 'opencode.json');
561
+ const expectedLauncher = path.join(target, BUNDLE_DIR, 'runtime', 'launch.cjs');
562
+ let registrationOk = false;
563
+ try {
564
+ const config = readJsonSafe(configFile);
565
+ const entry = config.mcp && config.mcp[MCP_SERVER_NAME];
566
+ registrationOk = entry?.type === 'local'
567
+ && entry.enabled === true
568
+ && Array.isArray(entry.command)
569
+ && entry.command[0] === process.execPath
570
+ && entry.command[1] === expectedLauncher;
571
+ } catch (error) {
572
+ report.issues.push({ code: 'MCP_REGISTRATION_INVALID', message: error.message });
573
+ }
574
+ if (!registrationOk && !report.issues.some((entry) => entry.code === 'MCP_REGISTRATION_INVALID')) {
575
+ report.issues.push({
576
+ code: 'MCP_REGISTRATION_INVALID',
577
+ path: configFile,
578
+ expected_launcher: expectedLauncher,
579
+ });
580
+ }
581
+ report.checks.registration = { status: registrationOk ? 'pass' : 'fail' };
582
+ if (report.status !== 'missing') report.status = report.issues.length === 0 ? 'healthy' : 'degraded';
457
583
  return report;
458
584
  }
459
585
 
@@ -529,4 +655,5 @@ module.exports = {
529
655
  resolveTarget,
530
656
  install,
531
657
  uninstall,
658
+ doctor,
532
659
  };
@@ -21,8 +21,10 @@ const {
21
21
  const REPO_ROOT = path.resolve(__dirname, '..', '..');
22
22
  const COMMANDS = [
23
23
  'learn',
24
+ 'ultra-change',
24
25
  'ultra-deliver',
25
26
  'ultra-dev',
27
+ 'ultra-doctor',
26
28
  'ultra-init',
27
29
  'ultra-plan',
28
30
  'ultra-research',
@@ -89,7 +91,7 @@ test('install builds one Codex-native plugin with complete skill and command cov
89
91
 
90
92
  const expectedSkills = skillsForRuntime('codex').sort();
91
93
  assert.deepEqual(skillNames(layout.pluginRoot), expectedSkills);
92
- assert.equal(expectedSkills.length, 16);
94
+ assert.equal(expectedSkills.length, 18);
93
95
  assert.ok(!fs.existsSync(path.join(layout.pluginRoot, 'skills', 'codex-collab')));
94
96
  assert.ok(!fs.existsSync(path.join(layout.pluginRoot, 'skills', 'learned')));
95
97
 
@@ -229,8 +231,8 @@ test('plugin declares current Codex hooks and a project-local Ultra MCP server',
229
231
  const liveSpec = yaml.load(fs.readFileSync(path.join(layout.pluginRoot, 'spec', 'mcp-tools.yaml'), 'utf8'));
230
232
  const upstreamSpec = yaml.load(fs.readFileSync(path.join(layout.pluginRoot, 'spec', 'upstream-mcp-tools.yaml'), 'utf8'));
231
233
  const capabilityMap = JSON.parse(fs.readFileSync(path.join(layout.pluginRoot, 'spec', 'codex-capability-map.json'), 'utf8'));
232
- assert.equal(liveSpec.tools.length, 21);
233
- assert.equal(upstreamSpec.tools.length, 21);
234
+ assert.equal(liveSpec.tools.length, 29);
235
+ assert.equal(upstreamSpec.tools.length, 29);
234
236
  assert.deepEqual(upstreamSpec.tools.map((tool) => tool.name).sort(), liveSpec.tools.map((tool) => tool.name).sort());
235
237
  assert.deepEqual(capabilityMap.live_mcp_tools.sort(), liveSpec.tools.map((tool) => tool.name).sort());
236
238
  assert.equal(Object.keys(capabilityMap.codex_native_replacements).length, 9);
@@ -360,3 +362,40 @@ test('uninstall removes only UBP-managed plugin and agents', () => {
360
362
  cleanup(layout);
361
363
  }
362
364
  });
365
+
366
+ test('doctor checks the current Codex cache hook target for a CLI-managed global install', () => {
367
+ const layout = mkLayout();
368
+ try {
369
+ install(layout);
370
+ const runtimeManifest = JSON.parse(fs.readFileSync(
371
+ path.join(layout.configDir, 'ultra-builder-pro', 'install-manifest.json'),
372
+ 'utf8',
373
+ ));
374
+ const version = runtimeManifest.plugin.version;
375
+ const cacheAdapter = path.join(
376
+ codex._internal.pluginCacheRoot(layout.configDir, 'personal'),
377
+ version,
378
+ 'hooks',
379
+ 'adapters',
380
+ 'codex.py',
381
+ );
382
+ const doctorCtx = {
383
+ homeDir: layout.homeDir,
384
+ scope: 'global',
385
+ repoRoot: REPO_ROOT,
386
+ runPluginCli: true,
387
+ };
388
+
389
+ const degraded = codex.doctor(doctorCtx);
390
+ assert.equal(degraded.status, 'degraded');
391
+ assert.ok(degraded.issues.some((entry) => (
392
+ entry.code === 'HOOK_TARGET_MISSING' && entry.version === version
393
+ )));
394
+
395
+ fs.mkdirSync(path.dirname(cacheAdapter), { recursive: true });
396
+ fs.copyFileSync(path.join(layout.pluginRoot, 'hooks', 'adapters', 'codex.py'), cacheAdapter);
397
+ assert.equal(codex.doctor(doctorCtx).status, 'healthy');
398
+ } finally {
399
+ cleanup(layout);
400
+ }
401
+ });
@@ -171,13 +171,14 @@ test('install writes a schema-safe opencode.json and keeps ownership outside hos
171
171
  assert.match(plugin, /\.ultra[\\/]tasks[\\/]tasks\.json/);
172
172
  assert.match(plugin, /throw new Error/);
173
173
  assert.match(plugin, /session\.compacted/);
174
- assert.doesNotMatch(plugin, /memory|recall|journal|observation/);
174
+ assert.doesNotMatch(plugin, /memory\.(?:retain|recall|reflect)|journal|observation|prompt[_ -]?capture/);
175
+ assert.match(plugin, /metadata references only/);
175
176
  } finally {
176
177
  fs.rmSync(target, { recursive: true, force: true });
177
178
  }
178
179
  });
179
180
 
180
- test('OpenCode before-tool hook blocks tasks.json writes only during an active workflow', async () => {
181
+ test('OpenCode context covers active changes and projection writes are blocked for Ultra projects', async () => {
181
182
  const target = mkTarget();
182
183
  const project = mkTarget();
183
184
  try {
@@ -186,15 +187,21 @@ test('OpenCode before-tool hook blocks tasks.json writes only during an active w
186
187
  const module = await import(pathToFileURL(pluginFile).href);
187
188
  const plugin = await module.UltraBuilderProPlugin({ directory: project });
188
189
 
189
- await plugin['tool.execute.before'](
190
- { tool: 'edit' },
191
- { args: { filePath: '.ultra/tasks/tasks.json' } },
192
- );
193
-
194
190
  fs.mkdirSync(path.join(project, '.ultra'), { recursive: true });
195
- fs.writeFileSync(path.join(project, '.ultra', 'workflow-state.json'), JSON.stringify({
196
- command: 'ultra-plan', task_id: 'plan', step: 'persist', status: 'active',
191
+ fs.writeFileSync(path.join(project, '.ultra', 'state.db'), 'owned-by-ultra');
192
+ const changeRoot = path.join(project, '.ultra', 'changes', 'active', 'daily-fix');
193
+ fs.mkdirSync(changeRoot, { recursive: true });
194
+ fs.writeFileSync(path.join(changeRoot, 'context-manifest.json'), JSON.stringify({
195
+ change: { id: 'daily-fix', title: 'Daily fix', kind: 'quick', status: 'active', intent: 'Fix drift' },
196
+ providers: { memory: { provider: 'cloud-mem', status: 'available' } },
197
197
  }));
198
+
199
+ const output = { system: [] };
200
+ await plugin['experimental.chat.system.transform']({}, output);
201
+ assert.match(output.system.join('\n'), /Ultra continuous change/);
202
+ assert.match(output.system.join('\n'), /daily-fix/);
203
+ assert.match(output.system.join('\n'), /metadata references only/);
204
+
198
205
  await assert.rejects(
199
206
  plugin['tool.execute.before'](
200
207
  { tool: 'apply_patch' },