ultra-builder-pro-cli 0.19.1 → 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 (103) 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 +90 -1
  5. package/README.md +62 -29
  6. package/adapters/_shared/codex-assets.cjs +69 -11
  7. package/adapters/_shared/interaction-contract.cjs +60 -5
  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/commands/ultra-research.md +1 -1
  15. package/docs/AGENT-CONTEXT.md +79 -45
  16. package/docs/ARCHITECTURE.md +75 -38
  17. package/docs/ARTIFACT-AUTHORITY.md +234 -0
  18. package/docs/DECISIONS.md +20 -10
  19. package/docs/LEGACY-CLI-CROSSWALK.md +15 -11
  20. package/docs/PLUGIN-ISOLATION-CONTRACT.md +4 -3
  21. package/docs/ROADMAP.md +21 -6
  22. package/docs/RUNTIME-COMPAT-MATRIX.md +30 -13
  23. package/docs/STATE-DB-ACCESS-POLICY.md +11 -9
  24. package/docs/WORKFLOW-LIFECYCLE.md +105 -54
  25. package/hooks/README.md +6 -5
  26. package/hooks/active_task_context.py +97 -12
  27. package/hooks/context_spine.py +16 -4
  28. package/hooks/health_check.py +5 -4
  29. package/hooks/pre_stop_check.py +5 -2
  30. package/hooks/runtime_paths.py +396 -0
  31. package/hooks/subagent_tracker.py +5 -2
  32. package/hooks/workflow_checkpoint.py +16 -3
  33. package/hooks/workflow_context.py +11 -3
  34. package/hooks/workflow_resume.py +7 -4
  35. package/mcp-server/breadcrumb.cjs +20 -2
  36. package/mcp-server/lib/archive-journal.cjs +1026 -111
  37. package/mcp-server/lib/archive-mutation-worker.py +365 -0
  38. package/mcp-server/lib/artifact-registry.cjs +1126 -0
  39. package/mcp-server/lib/baseline-workflow.cjs +9 -4
  40. package/mcp-server/lib/change-packet.cjs +818 -0
  41. package/mcp-server/lib/change-workflow.cjs +932 -176
  42. package/mcp-server/lib/context-paths.cjs +111 -0
  43. package/mcp-server/lib/context-spine.cjs +511 -45
  44. package/mcp-server/lib/decision-dialogue.cjs +427 -12
  45. package/mcp-server/lib/delivery-transaction.cjs +471 -0
  46. package/mcp-server/lib/doctor-backup-worker.cjs +328 -0
  47. package/mcp-server/lib/doctor.cjs +375 -12
  48. package/mcp-server/lib/git-bootstrap.cjs +459 -47
  49. package/mcp-server/lib/init-project.cjs +190 -95
  50. package/mcp-server/lib/plan-publication-worker.cjs +830 -0
  51. package/mcp-server/lib/plan-store.cjs +700 -5
  52. package/mcp-server/lib/project-breadcrumb.cjs +23 -4
  53. package/mcp-server/lib/projector.cjs +174 -40
  54. package/mcp-server/lib/runtime-paths.cjs +1687 -0
  55. package/mcp-server/lib/safe-project-file.cjs +385 -0
  56. package/mcp-server/lib/spec-learning.cjs +28 -12
  57. package/mcp-server/lib/state-db.cjs +397 -13
  58. package/mcp-server/lib/state-ops.cjs +248 -49
  59. package/mcp-server/lib/telemetry.cjs +12 -3
  60. package/mcp-server/lib/workflow-state.cjs +614 -62
  61. package/mcp-server/server.cjs +282 -33
  62. package/orchestrator/daemon.cjs +2 -10
  63. package/orchestrator/parallel-orchestrator.cjs +314 -50
  64. package/orchestrator/planner/plan-builder.cjs +1 -1
  65. package/orchestrator/recovery.cjs +125 -7
  66. package/orchestrator/session-close-journal-worker.cjs +5 -0
  67. package/orchestrator/session-close-journal.cjs +1014 -0
  68. package/orchestrator/session-runner.cjs +679 -69
  69. package/orchestrator/worktree-manager.cjs +186 -35
  70. package/package.json +2 -3
  71. package/skills/cc-collab/SKILL.md +4 -1
  72. package/skills/codex-collab/SKILL.md +5 -2
  73. package/skills/ultra-change/SKILL.md +38 -13
  74. package/skills/ultra-deliver/SKILL.md +23 -11
  75. package/skills/ultra-dev/SKILL.md +33 -6
  76. package/skills/ultra-doctor/SKILL.md +6 -2
  77. package/skills/ultra-init/SKILL.md +4 -2
  78. package/skills/ultra-plan/SKILL.md +68 -18
  79. package/skills/ultra-research/SKILL.md +45 -32
  80. package/skills/ultra-review/SKILL.md +9 -5
  81. package/skills/ultra-status/SKILL.md +9 -4
  82. package/skills/ultra-test/SKILL.md +15 -7
  83. package/skills/ultra-think/SKILL.md +6 -0
  84. package/skills/ultra-think/references/decision-dialogue.md +61 -14
  85. package/skills/ultra-verify/SKILL.md +4 -1
  86. package/spec/cli-protocol.md +19 -7
  87. package/spec/mcp-tools.yaml +531 -46
  88. package/spec/schemas/change-delta.v1.schema.json +153 -0
  89. package/spec/schemas/documentation-reconciliation.v1.schema.json +106 -0
  90. package/spec/schemas/mcp-tools.schema.json +1 -1
  91. package/spec/schemas/state-db.sql +57 -17
  92. package/spec/schemas/tasks.v4.5.schema.json +1 -1
  93. package/ultra-tools/cli.cjs +3 -2
  94. package/ultra-tools/commands/db.cjs +76 -25
  95. package/ultra-tools/commands/migrate.cjs +995 -80
  96. package/ultra-tools/commands/session.cjs +34 -8
  97. package/ultra-tools/commands/status.cjs +34 -7
  98. package/ultra-tools/commands/system.cjs +129 -22
  99. package/commands/learn.md +0 -10
  100. package/orchestrator/skill-miner.cjs +0 -133
  101. package/output-styles/coding-vibes.md +0 -67
  102. package/output-styles/structural-thinking.md +0 -61
  103. package/skills/learn/SKILL.md +0 -58
@@ -8,7 +8,6 @@
8
8
  */
9
9
 
10
10
  const CORE_PUBLIC_SKILLS = Object.freeze([
11
- 'learn',
12
11
  'ultra-init',
13
12
  'ultra-research',
14
13
  'ultra-plan',
@@ -22,6 +21,34 @@ const CORE_PUBLIC_SKILLS = Object.freeze([
22
21
  'ultra-doctor',
23
22
  ]);
24
23
 
24
+ const PUBLIC_CAPABILITY_MODES = Object.freeze({
25
+ 'ultra-init': 'setup',
26
+ 'ultra-research': 'workflow',
27
+ 'ultra-plan': 'workflow',
28
+ 'ultra-dev': 'workflow',
29
+ 'ultra-test': 'workflow',
30
+ 'ultra-review': 'workflow',
31
+ 'ultra-deliver': 'workflow',
32
+ 'ultra-status': 'read_only',
33
+ 'ultra-think': 'reasoning',
34
+ 'ultra-change': 'workflow',
35
+ 'ultra-doctor': 'diagnostic',
36
+ });
37
+
38
+ const PUBLIC_CAPABILITY_GRAPH = Object.freeze(Object.fromEntries(
39
+ CORE_PUBLIC_SKILLS.map((name) => [
40
+ name,
41
+ Object.freeze({
42
+ mode: PUBLIC_CAPABILITY_MODES[name],
43
+ activation: 'explicit_only',
44
+ next_capability_source: 'mcp_allowed_transitions',
45
+ recommendation_owner: 'host_model',
46
+ selection_owner: 'user',
47
+ automatic_invocation: false,
48
+ }),
49
+ ]),
50
+ ));
51
+
25
52
  const INTERNAL_AGENT_SKILLS = Object.freeze([
26
53
  'code-review-expert',
27
54
  'security-rules',
@@ -56,12 +83,22 @@ const WORKFLOW_HOOK_FILES = Object.freeze([
56
83
  'context_spine.py',
57
84
  'health_check.py',
58
85
  'pre_stop_check.py',
86
+ 'runtime_paths.py',
59
87
  'subagent_tracker.py',
60
88
  'workflow_checkpoint.py',
61
89
  'workflow_context.py',
62
90
  'workflow_resume.py',
63
91
  ]);
64
92
 
93
+ const RUNTIME_WORKER_FILES = Object.freeze([
94
+ 'session-close-journal-worker.cjs',
95
+ 'doctor-backup-worker.cjs',
96
+ ]);
97
+
98
+ const RUNTIME_SUPPORT_FILES = Object.freeze([
99
+ 'archive-mutation-worker.py',
100
+ ]);
101
+
65
102
  function skillsForRuntime(runtime) {
66
103
  const collab = COLLAB_SKILLS_BY_RUNTIME[runtime];
67
104
  if (!collab) throw new Error(`unsupported Ultra runtime: ${runtime}`);
@@ -84,11 +121,14 @@ function skillPolicy(name) {
84
121
 
85
122
  module.exports = {
86
123
  CORE_PUBLIC_SKILLS,
124
+ PUBLIC_CAPABILITY_GRAPH,
87
125
  INTERNAL_AGENT_SKILLS,
88
126
  SUPPORTED_RUNTIMES,
89
127
  COLLAB_SKILLS_BY_RUNTIME,
90
128
  MCP_DEPENDENT_SKILLS,
129
+ RUNTIME_SUPPORT_FILES,
91
130
  WORKFLOW_HOOK_FILES,
131
+ RUNTIME_WORKER_FILES,
92
132
  isSupportedRuntime,
93
133
  skillPolicy,
94
134
  skillsForRuntime,
@@ -16,6 +16,7 @@ const {
16
16
  } = require('./_shared/file-ops.cjs');
17
17
  const { buildMcpRuntime } = require('./_shared/codex-assets.cjs');
18
18
  const { parse: parseFrontmatter, serialize: serializeFrontmatter } = require('./_shared/frontmatter.cjs');
19
+ const { adaptInteractionGuidance } = require('./_shared/interaction-contract.cjs');
19
20
  const provenance = require('./_shared/provenance.cjs');
20
21
  const {
21
22
  CORE_PUBLIC_SKILLS,
@@ -55,13 +56,14 @@ function copySkills(repoRoot, target, names) {
55
56
  if (!fs.existsSync(source)) throw new Error(`missing allowlisted Claude skill: ${name}`);
56
57
  const files = copyTree(source, path.join(target, 'skills', name), {
57
58
  transform(buf, rel) {
58
- if (rel !== 'SKILL.md') return buf;
59
+ if (rel !== 'SKILL.md') {
60
+ return rel.endsWith('.md')
61
+ ? Buffer.from(adaptInteractionGuidance(buf.toString('utf8'), 'claude'))
62
+ : buf;
63
+ }
59
64
  const { fm, body } = parseFrontmatter(buf.toString('utf8'));
60
65
  if (!fm) throw new Error(`missing frontmatter in Claude skill: ${name}`);
61
- let adaptedBody = body;
62
- if (name === 'learn') {
63
- adaptedBody = adaptedBody.replaceAll("current host's user skill directory", '`~/.claude/skills`');
64
- }
66
+ let adaptedBody = adaptInteractionGuidance(body, 'claude');
65
67
  if (name === 'ultra-review') {
66
68
  adaptedBody = adaptedBody.replace(
67
69
  /the current host's native bounded-worker\s+mechanism/g,
package/adapters/codex.js CHANGED
@@ -8,7 +8,7 @@
8
8
  * skills/** -> ~/plugins/ultra-builder-pro/skills/**
9
9
  * agents/*.md -> ~/.codex/agents/*.toml
10
10
  * hooks/*.py -> plugin hooks with a Codex wire adapter
11
- * mcp-server -> plugin .mcp.json (project cwd owns .ultra/state.db)
11
+ * mcp-server -> plugin .mcp.json (project cwd owns .ultra/.runtime/state.db)
12
12
  * plugin marketplace -> ~/.agents/plugins/marketplace.json
13
13
  *
14
14
  * Deprecated ~/.codex/prompts and ~/.codex/skills projection is intentionally
package/adapters/kimi.js CHANGED
@@ -16,6 +16,7 @@ const {
16
16
  } = require('./_shared/file-ops.cjs');
17
17
  const { buildMcpRuntime } = require('./_shared/codex-assets.cjs');
18
18
  const { parse: parseFm, serialize: serializeFm } = require('./_shared/frontmatter.cjs');
19
+ const { adaptInteractionGuidance } = require('./_shared/interaction-contract.cjs');
19
20
  const provenance = require('./_shared/provenance.cjs');
20
21
  const {
21
22
  CORE_PUBLIC_SKILLS,
@@ -66,7 +67,7 @@ function loadRegistry(file) {
66
67
  }
67
68
 
68
69
  function kimiTextTransform(input, assetName = '') {
69
- let text = String(input);
70
+ let text = adaptInteractionGuidance(input, 'kimi');
70
71
  text = text.replaceAll('$CLAUDE_PLUGIN_ROOT', '$KIMI_PLUGIN_ROOT');
71
72
  text = text.replaceAll('~/.claude/skills', '~/.kimi-code/skills');
72
73
  text = text.replaceAll('~/.claude/hooks', '~/.kimi-code/hooks');
@@ -99,17 +100,10 @@ function kimiTextTransform(input, assetName = '') {
99
100
  'Kimi `AgentSwarm` for parallel reviewers or one foreground Kimi `Agent` for a single reviewer, using the worker prompt files under `$KIMI_PLUGIN_ROOT/agents/`',
100
101
  );
101
102
  }
102
- if (assetName === 'learn') {
103
- text = text.replaceAll("current host's user skill directory", '`~/.kimi-code/skills`');
104
- }
105
103
  text = text.replace(
106
104
  /(^|[\s`("'])\/ultra-(?!builder-pro:)([a-z][a-z0-9-]*)/gm,
107
105
  '$1/ultra-builder-pro:ultra-$2',
108
106
  );
109
- text = text.replace(
110
- /(^|[\s`("'])\/learn(?=$|[\s`),.;:])/gm,
111
- '$1/ultra-builder-pro:learn',
112
- );
113
107
  text = text.replace(
114
108
  /(^|[\s`("'])\/clear(?=$|[\s`),.;:])/gm,
115
109
  '$1/new',
@@ -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 { adaptInteractionGuidance } = require('./_shared/interaction-contract.cjs');
18
19
  const provenance = require('./_shared/provenance.cjs');
19
20
  const {
20
21
  CORE_PUBLIC_SKILLS,
@@ -67,7 +68,7 @@ function lowercaseFrontmatterTransform(buf, relPath) {
67
68
  }
68
69
 
69
70
  function openCodeTextTransform(input, assetName = '') {
70
- let text = String(input);
71
+ let text = adaptInteractionGuidance(input, 'opencode');
71
72
  text = text.replaceAll('$CLAUDE_PLUGIN_ROOT/skills', '~/.config/opencode/skills');
72
73
  text = text.replaceAll('~/.claude/skills', '~/.config/opencode/skills');
73
74
  text = text.replaceAll('~/.claude/hooks', '~/.config/opencode/.ultra-builder-pro/hooks');
@@ -96,9 +97,6 @@ function openCodeTextTransform(input, assetName = '') {
96
97
  'the OpenCode `task` tool using the installed bounded review agents',
97
98
  );
98
99
  }
99
- if (assetName === 'learn') {
100
- text = text.replaceAll("current host's user skill directory", '`~/.config/opencode/skills`');
101
- }
102
100
  return text;
103
101
  }
104
102
 
@@ -335,7 +333,6 @@ function preflightAssets(repoRoot, target) {
335
333
 
336
334
  function pluginSource() {
337
335
  return `// Managed by Ultra Builder Pro. Rebuild through the adapter; do not edit in place.
338
- import fs from "node:fs";
339
336
  import path from "node:path";
340
337
  import { execFileSync } from "node:child_process";
341
338
  import { fileURLToPath } from "node:url";
@@ -347,36 +344,29 @@ const BREADCRUMB_CLI = path.resolve(
347
344
  PLUGIN_DIR, "..", ${JSON.stringify(BUNDLE_DIR)}, "runtime", "breadcrumb.cjs",
348
345
  );
349
346
 
350
- function findUltraRoot(directory) {
351
- let current = path.resolve(directory);
352
- while (true) {
353
- const stateDb = path.join(current, ".ultra", "state.db");
354
- if (fs.existsSync(stateDb) && fs.statSync(stateDb).isFile()) return current;
355
- const parent = path.dirname(current);
356
- if (parent === current) return null;
357
- current = parent;
358
- }
359
- }
360
-
361
347
  function readUltraContext(directory) {
362
- const root = findUltraRoot(directory);
363
- if (!root) return null;
364
348
  try {
365
- const raw = execFileSync(NODE_BINARY, [BREADCRUMB_CLI, root], {
366
- cwd: root,
349
+ const raw = execFileSync(NODE_BINARY, [BREADCRUMB_CLI, "--discover", directory], {
350
+ cwd: directory,
367
351
  encoding: "utf8",
368
352
  stdio: ["ignore", "pipe", "pipe"],
369
353
  timeout: 5000,
370
354
  });
371
355
  const value = JSON.parse(raw);
356
+ const root = value?.root;
357
+ if (typeof root !== "string" || !root) return null;
372
358
  // A canonical breadcrumb is injectable only while DB authority proves an active workflow.
373
359
  if (!value?.breadcrumb || !value.breadcrumb.workflow
374
360
  || typeof value.text !== "string" || !value.text) {
375
361
  return { root, breadcrumb: null, text: null };
376
362
  }
377
363
  return { root, breadcrumb: value.breadcrumb, text: value.text };
378
- } catch {
379
- return { root, breadcrumb: null, text: null };
364
+ } catch (error) {
365
+ const detail = String(error?.stderr || error?.message || error);
366
+ if (/RUNTIME_(?:STATE_CONFLICT|PATH_UNSAFE|ORPHAN_SIDECAR|ROOT_INVALID|AUTHORITY_MISMATCH)|both legacy .*runtime.*state\\.db/i.test(detail)) {
367
+ throw new Error(detail.trim());
368
+ }
369
+ return null;
380
370
  }
381
371
  }
382
372
 
@@ -392,11 +382,22 @@ function targetPaths(tool, args) {
392
382
  return [...new Set(paths)];
393
383
  }
394
384
 
395
- function isTasksProjection(active, candidate) {
385
+ function projectionRootForTarget(baseRoot, candidate) {
396
386
  const target = path.isAbsolute(candidate)
397
387
  ? path.resolve(candidate)
398
- : path.resolve(active.root, candidate);
399
- return target === path.resolve(active.root, TASKS_PROJECTION);
388
+ : path.resolve(baseRoot, candidate);
389
+ if (path.basename(target) !== "tasks.json"
390
+ || path.basename(path.dirname(target)) !== "tasks"
391
+ || path.basename(path.dirname(path.dirname(target))) !== ".ultra") {
392
+ return null;
393
+ }
394
+ return path.dirname(path.dirname(path.dirname(target)));
395
+ }
396
+
397
+ function protectsTasksProjection(baseRoot, candidate) {
398
+ const targetRoot = projectionRootForTarget(baseRoot, candidate);
399
+ if (!targetRoot) return false;
400
+ return Boolean(readUltraContext(targetRoot));
400
401
  }
401
402
 
402
403
  export const UltraBuilderProPlugin = async ({ directory, worktree }) => {
@@ -417,13 +418,14 @@ export const UltraBuilderProPlugin = async ({ directory, worktree }) => {
417
418
  },
418
419
  "tool.execute.before": async (input, output) => {
419
420
  refresh();
420
- if (!active) return;
421
421
  const tool = String(input?.tool ?? "").toLowerCase();
422
422
  if (!["write", "edit", "apply_patch"].includes(tool)) return;
423
- if (targetPaths(tool, output?.args).some((candidate) => isTasksProjection(active, candidate))) {
423
+ if (targetPaths(tool, output?.args).some(
424
+ (candidate) => protectsTasksProjection(root, candidate),
425
+ )) {
424
426
  throw new Error(
425
427
  "Ultra Builder Pro refused a direct write to .ultra/tasks/tasks.json. " +
426
- ".ultra/state.db is authoritative; use MCP task tools and run ultra-doctor when state or projection health is degraded."
428
+ ".ultra/.runtime/state.db is authoritative; use MCP task tools and run ultra-doctor when state or projection health is degraded."
427
429
  );
428
430
  }
429
431
  },
@@ -6,7 +6,7 @@
6
6
  * Subcommands:
7
7
  * execute-plan resume the current plan through the first unfinished dependency wave.
8
8
  * run foreground daemon (debug / test).
9
- * start detached background daemon, writes .ultra/orchestrator.pid.
9
+ * start detached background daemon, writes .ultra/.runtime/orchestrator.pid.
10
10
  * stop reads pidfile, SIGTERM the process, deletes pidfile.
11
11
  * status prints pidfile + running session summary.
12
12
  *
@@ -23,10 +23,12 @@ const fs = require('node:fs');
23
23
  const path = require('node:path');
24
24
  const { spawn } = require('node:child_process');
25
25
  const { isSupportedRuntime } = require('../adapters/_shared/runtime-assets.cjs');
26
+ const runtimePaths = require('../mcp-server/lib/runtime-paths.cjs');
26
27
 
27
28
  const REPO_ROOT = process.env.UBP_REPO_ROOT || process.cwd();
28
- const PIDFILE = path.join(REPO_ROOT, '.ultra', 'orchestrator.pid');
29
- const LOGFILE = path.join(REPO_ROOT, '.ultra', 'orchestrator.log');
29
+ const RUNTIME_PATHS = runtimePaths.pathsFor(REPO_ROOT);
30
+ const PIDFILE = RUNTIME_PATHS.orchestratorPidPath;
31
+ const LOGFILE = RUNTIME_PATHS.orchestratorLogPath;
30
32
  const SETTINGS_FILES = [
31
33
  path.join(REPO_ROOT, 'settings.json'),
32
34
  path.join(REPO_ROOT, '.claude', 'settings.json'),
@@ -111,6 +113,7 @@ function resolveDispatchCommand(settings, env = process.env) {
111
113
  function parseExecutePlanArgs(argv = []) {
112
114
  const options = {
113
115
  planPath: null,
116
+ changeId: null,
114
117
  autoMerge: false,
115
118
  mergeBaseBranch: 'main',
116
119
  };
@@ -120,7 +123,7 @@ function parseExecutePlanArgs(argv = []) {
120
123
  options.autoMerge = true;
121
124
  continue;
122
125
  }
123
- if (arg === '--plan' || arg === '--base-branch') {
126
+ if (arg === '--plan' || arg === '--change' || arg === '--base-branch') {
124
127
  const value = argv[index + 1];
125
128
  if (!value || value.startsWith('--')) {
126
129
  throw commandConfigError(
@@ -129,6 +132,7 @@ function parseExecutePlanArgs(argv = []) {
129
132
  );
130
133
  }
131
134
  if (arg === '--plan') options.planPath = value;
135
+ else if (arg === '--change') options.changeId = value;
132
136
  else options.mergeBaseBranch = value;
133
137
  index += 1;
134
138
  continue;
@@ -138,21 +142,43 @@ function parseExecutePlanArgs(argv = []) {
138
142
  `unknown execute-plan option: ${arg}`,
139
143
  );
140
144
  }
145
+ if (options.planPath && options.changeId) {
146
+ throw commandConfigError(
147
+ 'ORCHESTRATOR_ARGUMENT_INVALID',
148
+ '--plan is explicit legacy compatibility and cannot be combined with --change',
149
+ );
150
+ }
141
151
  return options;
142
152
  }
143
153
 
154
+ function validateOrchestratorRuntime(rootDir = REPO_ROOT, {
155
+ forMutation = false,
156
+ } = {}) {
157
+ return runtimePaths.validateProjectLayout(rootDir, {
158
+ env: {},
159
+ forMutation,
160
+ validateRuntimeTree: true,
161
+ });
162
+ }
163
+
164
+ function openNoFollow(file, flags) {
165
+ const noFollow = fs.constants.O_NOFOLLOW || 0;
166
+ return fs.openSync(file, flags | noFollow, 0o600);
167
+ }
168
+
144
169
  async function cmdExecutePlan(argv = []) {
145
170
  const settings = readSettings();
146
171
  const dispatch = resolveDispatchCommand(settings);
147
172
  const options = parseExecutePlanArgs(argv);
148
173
  const { initStateDb, closeStateDb } = require('../mcp-server/lib/state-db.cjs');
149
174
  const parallelOrchestrator = require('../orchestrator/parallel-orchestrator.cjs');
150
- const { db } = initStateDb(path.join(REPO_ROOT, '.ultra', 'state.db'));
175
+ const { db } = initStateDb(runtimePaths.ensureRuntimeState(REPO_ROOT).stateDbPath);
151
176
  try {
152
177
  const result = await parallelOrchestrator.runPlan({
153
178
  db,
154
179
  repoRoot: REPO_ROOT,
155
180
  planPath: options.planPath ? path.resolve(REPO_ROOT, options.planPath) : undefined,
181
+ changeId: options.changeId,
156
182
  runtimes: parseRuntimes(),
157
183
  command: dispatch.command,
158
184
  commandArgs: dispatch.commandArgs,
@@ -187,7 +213,7 @@ function cmdRun(opts = {}) {
187
213
  const { runDaemon } = require('../orchestrator/daemon.cjs');
188
214
  const runtimes = parseRuntimes();
189
215
  const dispatch = resolveDispatchCommand(settings);
190
- const { db } = initStateDb(path.join(REPO_ROOT, '.ultra', 'state.db'));
216
+ const { db } = initStateDb(runtimePaths.ensureRuntimeState(REPO_ROOT).stateDbPath);
191
217
  const handle = runDaemon({
192
218
  db,
193
219
  repoRoot: REPO_ROOT,
@@ -221,39 +247,51 @@ function cmdStart() {
221
247
  process.exit(2);
222
248
  }
223
249
  resolveDispatchCommand(settings);
224
- if (fs.existsSync(PIDFILE)) {
225
- const existing = Number(fs.readFileSync(PIDFILE, 'utf8'));
250
+ const layout = validateOrchestratorRuntime(REPO_ROOT, { forMutation: true });
251
+ const pidfile = layout.orchestratorPidPath;
252
+ const logfile = layout.orchestratorLogPath;
253
+ if (fs.existsSync(pidfile)) {
254
+ const existing = Number(fs.readFileSync(pidfile, 'utf8'));
226
255
  try { process.kill(existing, 0); }
227
- catch (_) { fs.unlinkSync(PIDFILE); }
228
- if (fs.existsSync(PIDFILE)) {
256
+ catch (_) { fs.unlinkSync(pidfile); }
257
+ if (fs.existsSync(pidfile)) {
229
258
  process.stderr.write(`orchestrator already running (pid=${existing}).\n`);
230
259
  process.exit(1);
231
260
  }
232
261
  }
233
- fs.mkdirSync(path.dirname(PIDFILE), { recursive: true });
234
- const out = fs.openSync(LOGFILE, 'a');
235
- const err = fs.openSync(LOGFILE, 'a');
262
+ fs.mkdirSync(layout.orchestratorDir, { recursive: true });
263
+ const appendFlags = fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_WRONLY;
264
+ const out = openNoFollow(logfile, appendFlags);
265
+ const err = openNoFollow(logfile, appendFlags);
236
266
  const child = spawn(process.execPath, [__filename, 'run'], {
237
267
  detached: true,
238
268
  stdio: ['ignore', out, err],
239
269
  env: { ...process.env, UBP_REPO_ROOT: REPO_ROOT },
240
270
  });
241
271
  child.unref();
242
- fs.writeFileSync(PIDFILE, String(child.pid));
243
- process.stdout.write(`orchestrator started (pid=${child.pid}, log=${LOGFILE})\n`);
272
+ const pidFd = openNoFollow(
273
+ pidfile,
274
+ fs.constants.O_CREAT | fs.constants.O_TRUNC | fs.constants.O_WRONLY,
275
+ );
276
+ try { fs.writeFileSync(pidFd, String(child.pid)); }
277
+ finally { fs.closeSync(pidFd); }
278
+ process.stdout.write(`orchestrator started (pid=${child.pid}, log=${logfile})\n`);
244
279
  }
245
280
 
246
281
  function cmdStop() {
247
- if (!fs.existsSync(PIDFILE)) {
282
+ const { orchestratorPidPath: pidfile } = validateOrchestratorRuntime(REPO_ROOT, {
283
+ forMutation: true,
284
+ });
285
+ if (!fs.existsSync(pidfile)) {
248
286
  process.stderr.write('no pidfile found; orchestrator not running.\n');
249
287
  process.exit(1);
250
288
  }
251
- const pid = Number(fs.readFileSync(PIDFILE, 'utf8'));
289
+ const pid = Number(fs.readFileSync(pidfile, 'utf8'));
252
290
  try { process.kill(pid, 'SIGTERM'); }
253
291
  catch (err) {
254
292
  if (err.code === 'ESRCH') {
255
293
  process.stderr.write(`pid ${pid} already dead; cleaning pidfile.\n`);
256
- fs.unlinkSync(PIDFILE);
294
+ fs.unlinkSync(pidfile);
257
295
  return;
258
296
  }
259
297
  throw err;
@@ -262,22 +300,23 @@ function cmdStop() {
262
300
  setTimeout(() => {
263
301
  try { process.kill(pid, 0); }
264
302
  catch (_) { /* dead; good */ }
265
- if (fs.existsSync(PIDFILE)) fs.unlinkSync(PIDFILE);
303
+ if (fs.existsSync(pidfile)) fs.unlinkSync(pidfile);
266
304
  process.stdout.write(`orchestrator stopped (pid=${pid})\n`);
267
305
  }, 200);
268
306
  }
269
307
 
270
308
  function cmdStatus() {
271
- const out = { pidfile: PIDFILE, running: false };
272
- if (fs.existsSync(PIDFILE)) {
273
- const pid = Number(fs.readFileSync(PIDFILE, 'utf8'));
309
+ const { orchestratorPidPath: pidfile } = validateOrchestratorRuntime(REPO_ROOT);
310
+ const out = { pidfile, running: false };
311
+ if (fs.existsSync(pidfile)) {
312
+ const pid = Number(fs.readFileSync(pidfile, 'utf8'));
274
313
  try { process.kill(pid, 0); out.pid = pid; out.running = true; }
275
314
  catch (_) { out.pid = pid; out.running = false; out.note = 'pidfile stale'; }
276
315
  }
277
316
  // Session summary.
278
317
  try {
279
318
  const Database = require('better-sqlite3');
280
- const dbPath = path.join(REPO_ROOT, '.ultra', 'state.db');
319
+ const dbPath = runtimePaths.locateStateDb(REPO_ROOT);
281
320
  if (fs.existsSync(dbPath)) {
282
321
  const db = new Database(dbPath, { readonly: true });
283
322
  const counts = db.prepare(
@@ -297,7 +336,7 @@ function usage() {
297
336
  process.stderr.write(
298
337
  'usage: ubp-orchestrator <execute-plan|run|start|stop|status>\n' +
299
338
  '\n' +
300
- ' execute-plan resume the first unfinished dependency wave [--plan <path>] [--auto-merge] [--base-branch <name>]\n' +
339
+ ' execute-plan resume the first unfinished dependency wave [--change <id>] [--plan <legacy-path>] [--auto-merge] [--base-branch <name>]\n' +
301
340
  ' run foreground daemon (requires opt-in)\n' +
302
341
  ' start detached background daemon (requires opt-in)\n' +
303
342
  ' stop terminate running daemon\n' +
@@ -343,6 +382,7 @@ module.exports = {
343
382
  parseRuntimes,
344
383
  parseExecutePlanArgs,
345
384
  resolveDispatchCommand,
385
+ validateOrchestratorRuntime,
346
386
  cmdExecutePlan,
347
387
  cmdRun,
348
388
  cmdStart,
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Build evidence with model-selected semantic coverage and adaptive user alignment
2
+ description: Build evidence with recommended, owner-selected semantic coverage and adaptive alignment
3
3
  argument-hint: "[scope-or-question]"
4
4
  workflow-ref: "@skills/ultra-research/SKILL.md"
5
5
  ---