ultra-builder-pro-cli 0.7.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.
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 = {}) {
@@ -515,6 +517,69 @@ function install(ctx = {}) {
515
517
  delete next[LEGACY_SENTINEL_KEY];
516
518
  writeAtomic(configFile, JSON.stringify(next, null, 2) + '\n');
517
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';
518
583
  return report;
519
584
  }
520
585
 
@@ -590,4 +655,5 @@ module.exports = {
590
655
  resolveTarget,
591
656
  install,
592
657
  uninstall,
658
+ doctor,
593
659
  };
@@ -362,3 +362,40 @@ test('uninstall removes only UBP-managed plugin and agents', () => {
362
362
  cleanup(layout);
363
363
  }
364
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
+ });
@@ -115,6 +115,18 @@ If **3 consecutive fix attempts fail**, and each reveals new problems in differe
115
115
  {test output proving fix resolves the issue, no regressions}
116
116
  ```
117
117
 
118
+ When the parent assigns an Ultra `incident` change, also map the verified report
119
+ into the exact durable `diagnosis.md` contract:
120
+
121
+ - `Reproduction`: the smallest repeatable symptom and exact trigger;
122
+ - `Hypotheses`: falsifiable hypotheses and the evidence that accepted/rejected each;
123
+ - `Root cause`: the earliest incorrect state and why it occurred;
124
+ - `Regression test`: the failing-first test and its final passing command;
125
+ - `Recovery`: rollback, retry, repair, or operator action when applicable.
126
+
127
+ Return this content to the primary agent. Do not write `.ultra/state.db` or claim
128
+ incident convergence; the primary agent owns the change artifact and verification.
129
+
118
130
  ## Rules
119
131
 
120
132
  - Never guess. Every claim must have evidence from code or output.
package/bin/install.js CHANGED
@@ -16,6 +16,8 @@
16
16
  * -g, --global install to runtime's global config dir
17
17
  * -l, --local install into current working directory
18
18
  * -u, --uninstall remove installed assets
19
+ * -d, --doctor inspect installation provenance without mutation
20
+ * --json emit machine-readable doctor output
19
21
  * -c, --config-dir <path> override config directory
20
22
  * -h, --help show help
21
23
  */
@@ -77,6 +79,8 @@ function printHelp() {
77
79
 
78
80
  ${paint('yellow', 'Other:')}
79
81
  ${paint('cyan', '-u, --uninstall')} remove installed assets
82
+ ${paint('cyan', '-d, --doctor')} verify installed version, provenance, managed assets, and live entry points
83
+ ${paint('cyan', '--json')} emit machine-readable doctor output (doctor only)
80
84
  ${paint('cyan', '-c, --config-dir')} override runtime's config directory (string path)
81
85
  ${paint('cyan', '-h, --help')} show this help
82
86
  ${paint('cyan', '-v, --version')} show CLI version
@@ -90,6 +94,9 @@ function printHelp() {
90
94
 
91
95
  ${paint('dim', '# Uninstall from OpenCode')}
92
96
  npx ultra-builder-pro-cli --opencode --global --uninstall
97
+
98
+ ${paint('dim', '# Verify all host installations without changing them')}
99
+ npx ultra-builder-pro-cli --all --global --doctor
93
100
  `);
94
101
  }
95
102
 
@@ -100,6 +107,8 @@ function parseArgs(argv) {
100
107
  global: false,
101
108
  local: false,
102
109
  uninstall: false,
110
+ doctor: false,
111
+ json: false,
103
112
  help: false,
104
113
  version: false,
105
114
  configDir: null,
@@ -117,6 +126,8 @@ function parseArgs(argv) {
117
126
  case '-g': case '--global': flags.global = true; break;
118
127
  case '-l': case '--local': flags.local = true; break;
119
128
  case '-u': case '--uninstall': flags.uninstall = true; break;
129
+ case '-d': case '--doctor': flags.doctor = true; break;
130
+ case '--json': flags.json = true; break;
120
131
  case '-h': case '--help': flags.help = true; break;
121
132
  case '-v': case '--version': flags.version = true; break;
122
133
  case '-c': case '--config-dir':
@@ -178,13 +189,15 @@ async function main() {
178
189
  return;
179
190
  }
180
191
 
181
- printBanner();
182
-
183
192
  if (flags.help) {
193
+ printBanner();
184
194
  printHelp();
185
195
  return;
186
196
  }
187
197
 
198
+ if (flags.doctor && flags.uninstall) bail('cannot combine --doctor and --uninstall');
199
+ if (flags.json && !flags.doctor) bail('--json is available only with --doctor');
200
+
188
201
  if (runtimes.length === 0) {
189
202
  bail('no runtime selected; use --claude / --opencode / --codex / --all');
190
203
  }
@@ -193,15 +206,57 @@ async function main() {
193
206
  const repoRoot = path.resolve(__dirname, '..');
194
207
  const configDir = flags.configDir ? expandTilde(flags.configDir) : null;
195
208
 
196
- const mode = flags.uninstall ? 'uninstall' : 'install';
197
- console.log(` ${paint('bold', 'Mode:')} ${mode}`);
198
- console.log(` ${paint('bold', 'Scope:')} ${scope}`);
199
- console.log(` ${paint('bold', 'Runtimes:')} ${runtimes.join(', ')}`);
200
- if (configDir) console.log(` ${paint('bold', 'ConfigDir:')} ${configDir}`);
201
- console.log();
209
+ if (!flags.json) printBanner();
210
+
211
+ const mode = flags.doctor ? 'doctor' : flags.uninstall ? 'uninstall' : 'install';
212
+ if (!flags.json) {
213
+ console.log(` ${paint('bold', 'Mode:')} ${mode}`);
214
+ console.log(` ${paint('bold', 'Scope:')} ${scope}`);
215
+ console.log(` ${paint('bold', 'Runtimes:')} ${runtimes.join(', ')}`);
216
+ if (configDir) console.log(` ${paint('bold', 'ConfigDir:')} ${configDir}`);
217
+ console.log();
218
+ }
202
219
 
203
220
  const ctx = { repoRoot, scope, configDir, homeDir: os.homedir() };
204
221
 
222
+ if (flags.doctor) {
223
+ const reports = [];
224
+ for (const runtime of runtimes) {
225
+ const adapter = loadAdapter(runtime);
226
+ try {
227
+ if (typeof adapter.doctor !== 'function') throw new Error('adapter does not expose doctor()');
228
+ reports.push(await adapter.doctor(ctx));
229
+ } catch (error) {
230
+ reports.push({
231
+ adapter: runtime,
232
+ status: 'degraded',
233
+ manifest_path: null,
234
+ checks: {},
235
+ issues: [{ code: 'DOCTOR_FAILED', message: error.message }],
236
+ });
237
+ }
238
+ }
239
+ const output = {
240
+ status: reports.every((report) => report.status === 'healthy') ? 'healthy' : 'degraded',
241
+ reports,
242
+ };
243
+ if (flags.json) {
244
+ process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
245
+ } else {
246
+ for (const report of reports) {
247
+ const color = report.status === 'healthy' ? 'green' : 'red';
248
+ console.log(` ${paint(color, report.status === 'healthy' ? '✓' : '✗')} ${report.adapter}: ${report.status}`);
249
+ for (const finding of report.issues || []) {
250
+ const location = finding.path ? ` (${finding.path})` : '';
251
+ console.log(` ${finding.code}${location}`);
252
+ }
253
+ }
254
+ console.log();
255
+ }
256
+ if (output.status !== 'healthy') process.exitCode = 2;
257
+ return;
258
+ }
259
+
205
260
  let failed = 0;
206
261
  for (const runtime of runtimes) {
207
262
  const adapter = loadAdapter(runtime);
@@ -28,8 +28,9 @@ mcp_tools_required:
28
28
  1. 先查 active/blocked change,匹配则恢复,避免重复创建。
29
29
  2. 按 `quick / standard / major / incident` 分类并声明 docs impact。
30
30
  3. 调 `change.create`,为 change 创建至少一个带 `change_id` 的 task。
31
- 4. standard/major 写 delta + plan;调 `change.context` 编译最小上下文。
32
- 5. 路由到 `/ultra-dev`、`/ultra-test`、`/ultra-review`、`/ultra-deliver`。
31
+ 4. standard/major 写 delta + plan;incident 完成结构化 `diagnosis.md`。
32
+ 5. 调 `change.context` 编译最小上下文;incident 先走 debugger 诊断切片。
33
+ 6. 路由到 `/ultra-dev`、`/ultra-test`、`/ultra-review`、`/ultra-deliver`。
33
34
 
34
35
  Memory 与代码图谱只允许以外部 provider 元数据引用出现;本命令不保存 provider
35
36
  正文,也不自动刷新外部 provider。
@@ -141,6 +141,14 @@ but all require completed linked tasks, current context, declared documentation
141
141
  impact, and no open incident. Memory and graph payloads never enter Ultra;
142
142
  `context_snapshots.provider_refs_json` stores metadata references only.
143
143
 
144
+ `incident` is the canonical debug lane. Creating one also registers a durable
145
+ `diagnosis.md` artifact with five mandatory sections: reproduction, hypotheses,
146
+ root cause, regression test, and recovery. Convergence validates the artifact
147
+ structure and refreshes its content hash, so a debugger result that exists only
148
+ in chat cannot satisfy delivery. A host-native debugger may produce the bounded
149
+ analysis, but the primary agent owns the artifact, linked task, regression
150
+ evidence, and final convergence decision.
151
+
144
152
  Mutating MCP calls enqueue durable `projection_jobs`. Success is exposed in MCP
145
153
  response metadata; failure becomes a retryable structured incident instead of a
146
154
  swallowed warning. `system.doctor` is read-only by default and performs only
@@ -168,7 +176,33 @@ This admission gate is the smallest piece that prevents two agents from
168
176
  silently double-writing the same task — it is part of the v0.1 minimum
169
177
  execution layer, not deferred to Phase 5.
170
178
 
171
- ## 6. Two timelines — rule layer vs execution layer
179
+ ## 6. Compaction recovery — checkpoint as a validated consumer
180
+
181
+ `workflow_checkpoint.py` validates the current non-terminal workflow and writes
182
+ a schema-versioned checkpoint atomically before compaction. On resume,
183
+ `workflow_resume.py` reads both the live workflow and checkpoint defensively,
184
+ rejects corrupt, non-object, schema-mismatched, or terminal candidates, and
185
+ selects the newest valid state. A newer live state wins; a newer checkpoint can
186
+ atomically restore a missing or older live state before context is re-injected.
187
+ The checkpoint is therefore a recovery artifact and consumer, never a second
188
+ durable authority alongside `.ultra/state.db`.
189
+
190
+ ## 7. Installation provenance — read-only cross-host diagnosis
191
+
192
+ Every Claude Code, OpenCode, and Codex adapter writes a normalized
193
+ `provenance.json` for the assets it owns. The manifest records adapter/package
194
+ identity, source metadata, per-file SHA-256 hashes, an aggregate digest, and the
195
+ host-specific plugin, MCP, hook, launcher, and runtime contracts expected at
196
+ that install scope. It never attributes a package to an enclosing consumer
197
+ repository commit.
198
+
199
+ `ubp --doctor [--json]` is read-only: it recomputes hashes and validates those
200
+ entry points, returning a non-zero degraded result for missing/corrupt
201
+ provenance, content drift, or broken host wiring. This is separate from
202
+ `system.doctor`, which diagnoses project state, projection, incidents, sessions,
203
+ and backup-first workflow recovery.
204
+
205
+ ## 8. Two timelines — rule layer vs execution layer
172
206
 
173
207
  The roadmap separates two concerns that compete for the same surface
174
208
  area (D13):
@@ -196,7 +230,7 @@ value from "skills + tasks survive across sessions" long before they get
196
230
  value from "ten agents in parallel." Each milestone is independently
197
231
  shippable; downstream slip never blocks an earlier release.
198
232
 
199
- ## 7. Where to look next
233
+ ## 9. Where to look next
200
234
 
201
235
  | Question | File |
202
236
  |----------------------------------------------------|-----------------------------------------------------|
@@ -206,7 +240,7 @@ shippable; downstream slip never blocks an earlier release.
206
240
  | What does a tasks.json look like after projection? | [`spec/fixtures/valid/tasks.v4.5.json`](../spec/fixtures/valid/tasks.v4.5.json) |
207
241
  | Phase-by-phase work breakdown + decision log | [`docs/PLAN.zh-CN.md`](./PLAN.zh-CN.md) |
208
242
 
209
- ## 8. Verifying the architecture
243
+ ## 10. Verifying the architecture
210
244
 
211
245
  Every contract on this page is enforced by `npm run test:spec`:
212
246
 
@@ -1,6 +1,6 @@
1
1
  # ultra-builder-pro-cli — 历史执行计划与决策日志
2
2
 
3
- > **2026-07-17 当前生效边界(D50-D52,覆盖下文旧 Phase 6/7 的内置图谱、memory 与 runtime 设计)**:
3
+ > **2026-07-17 当前生效边界(D50-D53,覆盖下文旧 Phase 6/7 的内置图谱、memory 与 runtime 设计)**:
4
4
  > Ultra Builder Pro 不再拥有 `memory.*`、recall、prompt/transcript/observation/summary
5
5
  > 收集 hook 或私有 agent memory。跨会话记忆全部交给独立安装的 cloud-mem/claude-mem。
6
6
  > 插件只保留 12 个 Ultra 公共 workflow(新增 daily `ultra-change` 与诊断
@@ -10,8 +10,9 @@
10
10
  > 采用各自 native plugin 呈现,其他旧 runtime adapter 已退役。下文所有与此冲突的
11
11
  > memory/hindsight 章节仅作为历史实施记录,不再是当前需求或运行时契约。
12
12
 
13
- **当前状态**:v0.7.0 发布候选;三宿主 native plugin、29 个 live MCP tool、
14
- schema 9.0(15 表)、持续 change/context/convergence/doctor 已落地;npm 发布由
13
+ **当前状态**:v0.8.1;三宿主 native plugin、29 个 live MCP tool、
14
+ schema 9.0(15 表)、持续 change/context/convergence/doctor、incident debug lane、
15
+ checkpoint 恢复消费与跨宿主安装 provenance/doctor 已落地;npm 发布由
15
16
  `v*.*.*` tag 触发。以下 Phase 0-9 正文保留为 2026-04 的实施历史,不再作为
16
17
  当前 runtime 或发布状态的事实来源。
17
18
  **历史版本**:0.3.1-plan · 2026-04-17 Codex 第二轮评审收尾
@@ -27,7 +28,7 @@ schema 9.0(15 表)、持续 change/context/convergence/doctor 已落地;np
27
28
  - **Phase 4.6 拆 a/b**(R6 + D35):v0.1 只跑 smoke flow,full conformance 推 v0.2
28
29
  - **Codex 第二轮结论**:"小改后开工"(tl;dr);本版已完成小改
29
30
 
30
- **总决策数**:D1-D52(当前边界以 D50-D52 为准)
31
+ **总决策数**:D1-D53(当前边界以 D50-D53 为准)
31
32
  **总风险数**:R1-R32(v0.3.1 新增 R25-R32 共 8 条)
32
33
 
33
34
  ---
@@ -1639,6 +1640,7 @@ Week 18 buffer(ship 中任何 Phase 的 25% 滑动吃掉)
1639
1640
  | **D50** | **2026-07-15** | **重划插件与记忆边界**:Claude Code、Codex、OpenCode 改为各自 native plugin;只打包 10 个 Ultra 公共 workflow、4 个 agent-only 规则 skill 与宿主专属 collab companion;删除 6 个外部 skill、recall、Impeccable 残留、Ultra memory MCP/store/wrapper/capture hooks;持久记忆由独立 cloud-mem/claude-mem 负责;用户 handbook 通过可预览、可备份的 managed block 显式同步 | 用户确认最终方案;运行时 allowlist、conformance、hook 与 handbook 回归测试共同锁定边界 |
1640
1641
  | **D51** | **2026-07-17** | **runtime 收敛为 Claude Code、OpenCode、Codex**:删除第四 runtime 的 adapter、安装参数、collab skill、prompt、schema、pricing、调度与测试面;`ultra-verify` 改为当前宿主主责加单一只读外部顾问 | 用户确认不再需要第四 runtime;0.6.0 退役契约阻止其重新进入活跃源码或发布包 |
1641
1642
  | **D52** | **2026-07-17** | **初始交付后进入持续变更闭环**:新增 `ultra-change` / `ultra-doctor`、change/context/artifact/trace/incident/projection/consumer 状态、delta-first spec、确定性 convergence 与 baseline reconciliation;删除内置 code-graph watcher 和通用命令代理,Memory/图谱只保留外部 provider 元数据引用 | 用户确认采用最佳组合落地;解决日常小改导致 spec/context 漂移、投影失败静默和异常无恢复证据的问题 |
1643
+ | **D53** | **2026-07-17** | **Harness 缺口一次性闭环**:`incident` 成为带五段 `diagnosis.md` 结构门禁的 canonical debug lane;compact checkpoint 由 resume 消费、校验、择新并原子恢复;三宿主安装写 normalized provenance,`ubp --doctor` 只读校验 asset hash 与 plugin/hook/MCP/runtime contract | 将 GSD 的 debug/fresh context、GStack 的 checkpoint/readiness 与 ECC 的 doctor/provenance 思路收敛为 Ultra 自有契约;Memory 与图谱继续由外部 provider 所有,且不恢复任何已退役 surface |
1642
1644
 
1643
1645
  ---
1644
1646
 
@@ -41,7 +41,7 @@ primary verification.
41
41
  |---|---|---|---|
42
42
  | Session context/health | FULL, native `SessionStart` | DEGRADED, native context; health via `system.doctor` | FULL, native `SessionStart` |
43
43
  | Active edit boundary | FULL, `PreToolUse Edit|Write` | FULL, `tool.execute.before` rejects projection writes | FULL, `PreToolUse Edit|Write|apply_patch` |
44
- | Compaction recovery | FULL, `PreCompact` + resume matcher | FULL, native compacting context | FULL, `PreCompact` + `PostCompact` |
44
+ | Compaction recovery | FULL, validated checkpoint + resume/restore matcher | FULL, native compacting context | FULL, validated checkpoint + `PostCompact` restore |
45
45
  | Incomplete-stop gate | FULL, native blocking `Stop` | DEGRADED, no equivalent blocking stop hook | FULL, native blocking `Stop` |
46
46
  | Subagent lifecycle evidence | FULL | DEGRADED, no equivalent packaged event | FULL |
47
47
 
@@ -83,6 +83,9 @@ loading, and user interaction remain host-native surfaces.
83
83
  | Removes stale managed assets | FULL | FULL | FULL |
84
84
  | Preserves unrelated config | FULL | FULL | FULL |
85
85
  | Uninstall ownership guard | managed plugin root | sentinels + owned MCP entry | managed root/manifest/agent headers |
86
+ | Normalized install provenance | FULL | FULL | FULL |
87
+ | Read-only `ubp --doctor` | FULL | FULL | FULL |
88
+ | Asset-content and host-contract drift detection | FULL | FULL | FULL |
86
89
 
87
90
  ## 7. Verification sources
88
91
 
@@ -95,5 +98,9 @@ loading, and user interaction remain host-native surfaces.
95
98
  from re-entering active product surfaces.
96
99
  - `adapters/_shared/tests/handbook.test.cjs` verifies host rendering, backup,
97
100
  migration, and idempotency.
101
+ - `adapters/_shared/tests/provenance.test.cjs` verifies normalized manifests,
102
+ content hashes, source attribution, corruption handling, and contract drift.
103
+ - `tests/install.test.cjs` verifies healthy three-host doctor output and
104
+ host-specific hook/MCP degradation after controlled tampering.
98
105
 
99
106
  Any capability claim elsewhere must match current adapter code and these tests.
package/hooks/README.md CHANGED
@@ -9,8 +9,8 @@ bounded to the relevant Ultra projection or a non-terminal workflow.
9
9
  | Session start | `health_check.py` | Read-only integrity, incident, projection, session, and change-artifact checks |
10
10
  | Session start | `workflow_context.py` | Inject baseline/active workflow/change context and filtered provider metadata references |
11
11
  | Before edit | `active_task_context.py` | Protect `tasks.json` projection and restate an active task boundary |
12
- | Before compact | `workflow_checkpoint.py` | Atomically save a minimal workflow checkpoint |
13
- | After compact/resume | `workflow_resume.py` | Re-inject the current workflow boundary |
12
+ | Before compact | `workflow_checkpoint.py` | Validate and atomically save a minimal workflow checkpoint |
13
+ | After compact/resume | `workflow_resume.py` | Select the newest valid live/checkpoint state, restore it atomically when needed, and re-inject the boundary |
14
14
  | Stop | `pre_stop_check.py` | Block once when the Ultra workflow is incomplete |
15
15
  | Subagent lifecycle | `subagent_tracker.py` | Append lifecycle evidence under `.ultra/runtime/` |
16
16
 
@@ -18,6 +18,10 @@ The plugin deliberately does not capture prompts, tool observations, transcripts
18
18
  cross-session memory or code-graph content. Generic command blocking and post-edit policy are
19
19
  user/repository governance. Persistent Memory/graph content belongs to separately installed providers.
20
20
 
21
+ The checkpoint is a recovery artifact, not a second authority. A valid newer
22
+ live workflow wins; a newer valid checkpoint may restore a missing or older
23
+ live file; corrupt, non-object, or terminal checkpoints are ignored.
24
+
21
25
  Runtime wiring:
22
26
 
23
27
  - Claude Code: native `hooks/hooks.json` with `${CLAUDE_PLUGIN_ROOT}` paths.
@@ -11,6 +11,17 @@ from pathlib import Path
11
11
  TERMINAL = {"committed", "completed", "done", "cancelled"}
12
12
 
13
13
 
14
+ def valid_workflow(state: object) -> bool:
15
+ return (
16
+ isinstance(state, dict)
17
+ and isinstance(state.get("command"), str)
18
+ and bool(state["command"].strip())
19
+ and state.get("step") is not None
20
+ and isinstance(state.get("status"), str)
21
+ and bool(state["status"].strip())
22
+ )
23
+
24
+
14
25
  def main() -> None:
15
26
  try:
16
27
  data = json.loads(sys.stdin.read() or "{}")
@@ -28,7 +39,10 @@ def main() -> None:
28
39
  except (OSError, json.JSONDecodeError) as exc:
29
40
  print(f"[workflow_checkpoint] cannot read {state_file}: {exc}", file=sys.stderr)
30
41
  break
31
- if not isinstance(state, dict) or state.get("status") in TERMINAL:
42
+ if not valid_workflow(state):
43
+ print(f"[workflow_checkpoint] invalid workflow state in {state_file}", file=sys.stderr)
44
+ break
45
+ if state.get("status") in TERMINAL:
32
46
  break
33
47
  runtime_dir = root / ".ultra" / "runtime"
34
48
  runtime_dir.mkdir(parents=True, exist_ok=True)