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.
- package/.ultra-template/docs/research/README.md +26 -5
- package/.ultra-template/specs/discovery.md +1 -1
- package/.ultra-template/tasks/tasks.json +1 -1
- package/CHANGELOG.md +90 -1
- package/README.md +62 -29
- package/adapters/_shared/codex-assets.cjs +69 -11
- package/adapters/_shared/interaction-contract.cjs +60 -5
- package/adapters/_shared/runtime-assets.cjs +41 -1
- package/adapters/claude.js +7 -5
- package/adapters/codex.js +1 -1
- package/adapters/kimi.js +2 -8
- package/adapters/opencode.js +30 -28
- package/bin/orchestrator.js +64 -24
- package/commands/ultra-research.md +1 -1
- package/docs/AGENT-CONTEXT.md +79 -45
- package/docs/ARCHITECTURE.md +75 -38
- package/docs/ARTIFACT-AUTHORITY.md +234 -0
- package/docs/DECISIONS.md +20 -10
- package/docs/LEGACY-CLI-CROSSWALK.md +15 -11
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +4 -3
- package/docs/ROADMAP.md +21 -6
- package/docs/RUNTIME-COMPAT-MATRIX.md +30 -13
- package/docs/STATE-DB-ACCESS-POLICY.md +11 -9
- package/docs/WORKFLOW-LIFECYCLE.md +105 -54
- package/hooks/README.md +6 -5
- package/hooks/active_task_context.py +97 -12
- package/hooks/context_spine.py +16 -4
- package/hooks/health_check.py +5 -4
- package/hooks/pre_stop_check.py +5 -2
- package/hooks/runtime_paths.py +396 -0
- package/hooks/subagent_tracker.py +5 -2
- package/hooks/workflow_checkpoint.py +16 -3
- package/hooks/workflow_context.py +11 -3
- package/hooks/workflow_resume.py +7 -4
- package/mcp-server/breadcrumb.cjs +20 -2
- package/mcp-server/lib/archive-journal.cjs +1026 -111
- package/mcp-server/lib/archive-mutation-worker.py +365 -0
- package/mcp-server/lib/artifact-registry.cjs +1126 -0
- package/mcp-server/lib/baseline-workflow.cjs +9 -4
- package/mcp-server/lib/change-packet.cjs +818 -0
- package/mcp-server/lib/change-workflow.cjs +932 -176
- package/mcp-server/lib/context-paths.cjs +111 -0
- package/mcp-server/lib/context-spine.cjs +511 -45
- package/mcp-server/lib/decision-dialogue.cjs +427 -12
- package/mcp-server/lib/delivery-transaction.cjs +471 -0
- package/mcp-server/lib/doctor-backup-worker.cjs +328 -0
- package/mcp-server/lib/doctor.cjs +375 -12
- package/mcp-server/lib/git-bootstrap.cjs +459 -47
- package/mcp-server/lib/init-project.cjs +190 -95
- package/mcp-server/lib/plan-publication-worker.cjs +830 -0
- package/mcp-server/lib/plan-store.cjs +700 -5
- package/mcp-server/lib/project-breadcrumb.cjs +23 -4
- package/mcp-server/lib/projector.cjs +174 -40
- package/mcp-server/lib/runtime-paths.cjs +1687 -0
- package/mcp-server/lib/safe-project-file.cjs +385 -0
- package/mcp-server/lib/spec-learning.cjs +28 -12
- package/mcp-server/lib/state-db.cjs +397 -13
- package/mcp-server/lib/state-ops.cjs +248 -49
- package/mcp-server/lib/telemetry.cjs +12 -3
- package/mcp-server/lib/workflow-state.cjs +614 -62
- package/mcp-server/server.cjs +282 -33
- package/orchestrator/daemon.cjs +2 -10
- package/orchestrator/parallel-orchestrator.cjs +314 -50
- package/orchestrator/planner/plan-builder.cjs +1 -1
- package/orchestrator/recovery.cjs +125 -7
- package/orchestrator/session-close-journal-worker.cjs +5 -0
- package/orchestrator/session-close-journal.cjs +1014 -0
- package/orchestrator/session-runner.cjs +679 -69
- package/orchestrator/worktree-manager.cjs +186 -35
- package/package.json +2 -3
- package/skills/cc-collab/SKILL.md +4 -1
- package/skills/codex-collab/SKILL.md +5 -2
- package/skills/ultra-change/SKILL.md +38 -13
- package/skills/ultra-deliver/SKILL.md +23 -11
- package/skills/ultra-dev/SKILL.md +33 -6
- package/skills/ultra-doctor/SKILL.md +6 -2
- package/skills/ultra-init/SKILL.md +4 -2
- package/skills/ultra-plan/SKILL.md +68 -18
- package/skills/ultra-research/SKILL.md +45 -32
- package/skills/ultra-review/SKILL.md +9 -5
- package/skills/ultra-status/SKILL.md +9 -4
- package/skills/ultra-test/SKILL.md +15 -7
- package/skills/ultra-think/SKILL.md +6 -0
- package/skills/ultra-think/references/decision-dialogue.md +61 -14
- package/skills/ultra-verify/SKILL.md +4 -1
- package/spec/cli-protocol.md +19 -7
- package/spec/mcp-tools.yaml +531 -46
- package/spec/schemas/change-delta.v1.schema.json +153 -0
- package/spec/schemas/documentation-reconciliation.v1.schema.json +106 -0
- package/spec/schemas/mcp-tools.schema.json +1 -1
- package/spec/schemas/state-db.sql +57 -17
- package/spec/schemas/tasks.v4.5.schema.json +1 -1
- package/ultra-tools/cli.cjs +3 -2
- package/ultra-tools/commands/db.cjs +76 -25
- package/ultra-tools/commands/migrate.cjs +995 -80
- package/ultra-tools/commands/session.cjs +34 -8
- package/ultra-tools/commands/status.cjs +34 -7
- package/ultra-tools/commands/system.cjs +129 -22
- package/commands/learn.md +0 -10
- package/orchestrator/skill-miner.cjs +0 -133
- package/output-styles/coding-vibes.md +0 -67
- package/output-styles/structural-thinking.md +0 -61
- 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,
|
package/adapters/claude.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: 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')
|
|
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 =
|
|
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',
|
package/adapters/opencode.js
CHANGED
|
@@ -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 =
|
|
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,
|
|
366
|
-
cwd:
|
|
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
|
-
|
|
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
|
|
385
|
+
function projectionRootForTarget(baseRoot, candidate) {
|
|
396
386
|
const target = path.isAbsolute(candidate)
|
|
397
387
|
? path.resolve(candidate)
|
|
398
|
-
: path.resolve(
|
|
399
|
-
|
|
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(
|
|
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
|
},
|
package/bin/orchestrator.js
CHANGED
|
@@ -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
|
|
29
|
-
const
|
|
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(
|
|
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(
|
|
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
|
-
|
|
225
|
-
|
|
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(
|
|
228
|
-
if (fs.existsSync(
|
|
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(
|
|
234
|
-
const
|
|
235
|
-
const
|
|
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
|
-
|
|
243
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
272
|
-
|
|
273
|
-
|
|
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 =
|
|
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
|
|
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
|
---
|