ultra-builder-pro-cli 0.13.0 → 0.15.0
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 +11 -7
- package/.ultra-template/specs/architecture.md +36 -36
- package/.ultra-template/specs/discovery.md +46 -33
- package/.ultra-template/specs/product.md +38 -29
- package/.ultra-template/specs/research-distillate.md +48 -0
- package/.ultra-template/tasks/contexts/TEMPLATE.md +11 -28
- package/CHANGELOG.md +86 -1
- package/README.md +40 -26
- package/adapters/_shared/codex-assets.cjs +29 -165
- package/adapters/_shared/file-ops.cjs +6 -2
- package/adapters/_shared/runtime-assets.cjs +1 -0
- package/adapters/claude.js +3 -2
- package/adapters/codex.js +1 -0
- package/adapters/kimi.js +3 -2
- package/adapters/opencode.js +33 -122
- package/agents/review-coordinator.md +9 -6
- package/docs/AGENT-CONTEXT.md +21 -11
- package/docs/ARCHITECTURE.md +41 -13
- package/docs/PLAN.zh-CN.md +3 -2
- package/docs/ROADMAP.md +9 -5
- package/docs/RUNTIME-COMPAT-MATRIX.md +6 -6
- package/docs/STATE-DB-ACCESS-POLICY.md +27 -25
- package/docs/WORKFLOW-LIFECYCLE.md +266 -0
- package/hooks/README.md +6 -1
- package/hooks/active_task_context.py +22 -50
- package/hooks/context_spine.py +61 -332
- package/hooks/health_check.py +29 -18
- package/hooks/pre_stop_check.py +28 -33
- package/hooks/subagent_tracker.py +54 -85
- package/hooks/workflow_checkpoint.py +39 -47
- package/hooks/workflow_context.py +4 -9
- package/hooks/workflow_resume.py +48 -128
- package/mcp-server/breadcrumb.cjs +22 -0
- package/mcp-server/hook-event.cjs +34 -0
- package/mcp-server/lib/archive-journal.cjs +17 -5
- package/mcp-server/lib/baseline-workflow.cjs +158 -18
- package/mcp-server/lib/change-workflow.cjs +550 -85
- package/mcp-server/lib/context-spine.cjs +242 -24
- package/mcp-server/lib/doctor.cjs +33 -3
- package/mcp-server/lib/init-project.cjs +210 -16
- package/mcp-server/lib/plan-store.cjs +4 -1
- package/mcp-server/lib/prd-parser.cjs +14 -41
- package/mcp-server/lib/pricing.cjs +7 -10
- package/mcp-server/lib/project-breadcrumb.cjs +145 -0
- package/mcp-server/lib/projector.cjs +57 -3
- package/mcp-server/lib/spec-learning.cjs +79 -2
- package/mcp-server/lib/state-db.cjs +145 -8
- package/mcp-server/lib/state-ops.cjs +160 -9
- package/mcp-server/lib/task-expander.cjs +9 -38
- package/mcp-server/lib/workflow-state.cjs +2292 -0
- package/mcp-server/server.cjs +182 -50
- package/orchestrator/daemon.cjs +1 -2
- package/orchestrator/dispatch-rules.cjs +5 -31
- package/orchestrator/planner/plan-builder.cjs +21 -12
- package/package.json +14 -6
- package/skills/cc-collab/SKILL.md +2 -2
- package/skills/codex-collab/SKILL.md +2 -3
- package/skills/ultra-change/SKILL.md +53 -68
- package/skills/ultra-change/references/change-contract.md +36 -0
- package/skills/ultra-deliver/SKILL.md +69 -79
- package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
- package/skills/ultra-dev/SKILL.md +54 -59
- package/skills/ultra-doctor/SKILL.md +40 -71
- package/skills/ultra-init/SKILL.md +96 -103
- package/skills/ultra-init/references/brownfield-adoption.md +2 -2
- package/skills/ultra-plan/SKILL.md +79 -75
- package/skills/ultra-plan/references/semantic-preflight.md +18 -0
- package/skills/ultra-research/SKILL.md +87 -60
- package/skills/ultra-research/references/00-problem-validation.md +36 -0
- package/skills/ultra-research/references/01-opportunity-discovery.md +33 -0
- package/skills/ultra-research/references/02-market-assessment.md +30 -0
- package/skills/ultra-research/references/03-competitive-landscape.md +31 -0
- package/skills/ultra-research/references/04-product-strategy.md +32 -0
- package/skills/ultra-research/references/05-assumptions-validation.md +31 -0
- package/skills/ultra-research/references/10-user-personas.md +29 -0
- package/skills/ultra-research/references/11-user-scenarios.md +28 -0
- package/skills/ultra-research/references/20-user-stories.md +30 -0
- package/skills/ultra-research/references/21-features-scope.md +29 -0
- package/skills/ultra-research/references/22-success-metrics.md +28 -0
- package/skills/ultra-research/references/30-architecture-context.md +30 -0
- package/skills/ultra-research/references/31-solution-strategy.md +28 -0
- package/skills/ultra-research/references/32-building-blocks.md +28 -0
- package/skills/ultra-research/references/40-deployment.md +29 -0
- package/skills/ultra-research/references/41-quality-risks.md +28 -0
- package/skills/ultra-research/references/99-synthesis.md +35 -0
- package/skills/ultra-review/SKILL.md +57 -73
- package/skills/ultra-review/references/review-modes.md +27 -0
- package/skills/ultra-review/references/unified-schema.md +25 -0
- package/skills/ultra-review/scripts/review_wait.py +40 -3
- package/skills/ultra-status/SKILL.md +41 -45
- package/skills/ultra-test/SKILL.md +48 -55
- package/skills/ultra-verify/SKILL.md +22 -31
- package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
- package/skills/ultra-verify/scripts/verify_wait.py +54 -54
- package/spec/cli-protocol.md +7 -2
- package/spec/mcp-tools.yaml +422 -73
- package/spec/schemas/baseline-reconciliation.v1.schema.json +73 -0
- package/spec/schemas/delivery-report.v1.schema.json +111 -0
- package/spec/schemas/mcp-tools.schema.json +1 -1
- package/spec/schemas/state-db.sql +82 -1
- package/spec/schemas/tasks.v4.5.schema.json +8 -1
- package/spec/schemas/test-report.v1.schema.json +194 -0
- package/ultra-tools/cli.cjs +1 -1
- package/ultra-tools/commands/migrate.cjs +3 -3
- package/ultra-tools/commands/status.cjs +213 -12
- package/.ultra-template/delivery-report.json +0 -9
- package/.ultra-template/test-report.json +0 -8
- package/adapters/_shared/tests/file-ops.test.cjs +0 -54
- package/adapters/_shared/tests/frontmatter.test.cjs +0 -47
- package/adapters/_shared/tests/handbook.test.cjs +0 -101
- package/adapters/_shared/tests/path-rewrite.test.cjs +0 -60
- package/adapters/_shared/tests/provenance.test.cjs +0 -133
- package/adapters/_shared/tests/resolve-target.test.cjs +0 -75
- package/adapters/_shared/tests/runtime-assets.test.cjs +0 -131
- package/adapters/_shared/tests/settings-merge.test.cjs +0 -85
- package/adapters/_shared/tests/validate.test.cjs +0 -30
- package/adapters/tests/claude.test.cjs +0 -137
- package/adapters/tests/codex-hook.test.cjs +0 -115
- package/adapters/tests/codex.test.cjs +0 -423
- package/adapters/tests/kimi-hook.test.cjs +0 -122
- package/adapters/tests/kimi.test.cjs +0 -259
- package/adapters/tests/opencode.test.cjs +0 -371
- package/mcp-server/lib/archive-journal.test.cjs +0 -35
- package/mcp-server/lib/baseline-workflow.test.cjs +0 -477
- package/mcp-server/lib/change-workflow.test.cjs +0 -971
- package/mcp-server/lib/context-spine.test.cjs +0 -93
- package/mcp-server/lib/doctor.test.cjs +0 -148
- package/mcp-server/lib/llm-client.cjs +0 -157
- package/mcp-server/lib/llm-client.test.cjs +0 -156
- package/mcp-server/lib/plan-store.test.cjs +0 -132
- package/mcp-server/lib/prd-parser.test.cjs +0 -171
- package/mcp-server/lib/runtime-state.test.cjs +0 -92
- package/mcp-server/lib/spec-learning.test.cjs +0 -67
- package/mcp-server/lib/state-db.test.cjs +0 -314
- package/mcp-server/lib/task-expander.test.cjs +0 -324
- package/mcp-server/lib/topo.test.cjs +0 -158
- package/mcp-server/tests/circuit-breaker.test.cjs +0 -125
- package/mcp-server/tests/commit-hash-flow.test.cjs +0 -117
- package/mcp-server/tests/concurrency-update.test.cjs +0 -96
- package/mcp-server/tests/concurrency.test.cjs +0 -112
- package/mcp-server/tests/db-ops.test.cjs +0 -121
- package/mcp-server/tests/init-project.test.cjs +0 -393
- package/mcp-server/tests/migrate.test.cjs +0 -232
- package/mcp-server/tests/projector.test.cjs +0 -162
- package/mcp-server/tests/server-contract.test.cjs +0 -1471
- package/mcp-server/tests/staleness.test.cjs +0 -190
- package/mcp-server/tests/state-db.test.cjs +0 -106
- package/mcp-server/tests/state-ops.test.cjs +0 -449
- package/mcp-server/tests/tagged-tasks.test.cjs +0 -163
- package/mcp-server/tests/telemetry.test.cjs +0 -219
- package/orchestrator/planner/expand-prompt.cjs +0 -62
- package/orchestrator/planner/expand-prompt.test.cjs +0 -52
- package/orchestrator/planner/plan-builder.test.cjs +0 -165
- package/orchestrator/planner/prd-prompt.cjs +0 -44
- package/orchestrator/planner/prd-prompt.test.cjs +0 -30
- package/orchestrator/tests/auto-merge.test.cjs +0 -216
- package/orchestrator/tests/daemon.test.cjs +0 -311
- package/orchestrator/tests/dispatch-rules.test.cjs +0 -124
- package/orchestrator/tests/parallel-orchestrator.test.cjs +0 -233
- package/orchestrator/tests/phase-8b-gate.test.cjs +0 -183
- package/orchestrator/tests/recovery.test.cjs +0 -190
- package/orchestrator/tests/session-runner.test.cjs +0 -314
- package/orchestrator/tests/skill-miner.test.cjs +0 -147
- package/orchestrator/tests/worktree-manager.test.cjs +0 -148
- package/skills/ultra-research/references/architecture.md +0 -38
- package/skills/ultra-research/references/problem-product.md +0 -39
- package/skills/ultra-research/references/quality-delivery.md +0 -34
- package/skills/ultra-research/references/synthesis.md +0 -38
- package/skills/ultra-research/references/users-scope.md +0 -40
- package/ultra-tools/legacy-memory.test.cjs +0 -80
- package/ultra-tools/status-command.test.cjs +0 -181
- package/ultra-tools/system-command.test.cjs +0 -309
package/mcp-server/server.cjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// MCP server (stdio JSON-RPC) exposing task.* + session.* tools backed by
|
|
4
4
|
// .ultra/state.db. Uses the low-level Server API from
|
|
5
|
-
// @modelcontextprotocol/
|
|
5
|
+
// @modelcontextprotocol/server so we can pass raw JSON Schema (Draft 2020-12)
|
|
6
6
|
// straight from spec/mcp-tools.yaml without translating to zod.
|
|
7
7
|
|
|
8
8
|
const fs = require('node:fs');
|
|
@@ -12,18 +12,18 @@ const yaml = require('js-yaml');
|
|
|
12
12
|
const Ajv = require('ajv/dist/2020');
|
|
13
13
|
const addFormats = require('ajv-formats');
|
|
14
14
|
|
|
15
|
-
const { Server } = require('@modelcontextprotocol/
|
|
16
|
-
const { StdioServerTransport } = require('@modelcontextprotocol/
|
|
17
|
-
const { ListToolsRequestSchema, CallToolRequestSchema } = require('@modelcontextprotocol/sdk/types.js');
|
|
15
|
+
const { Server } = require('@modelcontextprotocol/server');
|
|
16
|
+
const { StdioServerTransport } = require('@modelcontextprotocol/server/stdio');
|
|
18
17
|
const { version: PACKAGE_VERSION } = require('../package.json');
|
|
19
18
|
|
|
20
|
-
const {
|
|
19
|
+
const {
|
|
20
|
+
initStateDb, closeStateDb, openStateDb, ensureSchemaVersion,
|
|
21
|
+
} = require('./lib/state-db.cjs');
|
|
21
22
|
const { assertStateAuthority } = require('./lib/state-authority.cjs');
|
|
22
23
|
const ops = require('./lib/state-ops.cjs');
|
|
23
24
|
const projector = require('./lib/projector.cjs');
|
|
24
25
|
const telemetry = require('./lib/telemetry.cjs');
|
|
25
26
|
const topo = require('./lib/topo.cjs');
|
|
26
|
-
const llm = require('./lib/llm-client.cjs');
|
|
27
27
|
const parser = require('./lib/prd-parser.cjs');
|
|
28
28
|
const expander = require('./lib/task-expander.cjs');
|
|
29
29
|
const planStore = require('./lib/plan-store.cjs');
|
|
@@ -31,7 +31,12 @@ const { initProject } = require('./lib/init-project.cjs');
|
|
|
31
31
|
const baselines = require('./lib/baseline-workflow.cjs');
|
|
32
32
|
const changes = require('./lib/change-workflow.cjs');
|
|
33
33
|
const runtimeState = require('./lib/runtime-state.cjs');
|
|
34
|
+
const workflows = require('./lib/workflow-state.cjs');
|
|
34
35
|
const doctor = require('./lib/doctor.cjs');
|
|
36
|
+
const {
|
|
37
|
+
readProjectBreadcrumb,
|
|
38
|
+
renderProjectBreadcrumb,
|
|
39
|
+
} = require('./lib/project-breadcrumb.cjs');
|
|
35
40
|
|
|
36
41
|
const REPO_ROOT = process.env.UBP_RUNTIME_ROOT
|
|
37
42
|
? path.resolve(process.env.UBP_RUNTIME_ROOT)
|
|
@@ -88,12 +93,21 @@ const CHANGE_TOOLS = Object.freeze([
|
|
|
88
93
|
'change.archive',
|
|
89
94
|
]);
|
|
90
95
|
|
|
96
|
+
const WORKFLOW_TOOLS = Object.freeze([
|
|
97
|
+
'workflow.start',
|
|
98
|
+
'workflow.get',
|
|
99
|
+
'workflow.list',
|
|
100
|
+
'workflow.step',
|
|
101
|
+
'workflow.complete',
|
|
102
|
+
]);
|
|
103
|
+
|
|
91
104
|
const SYSTEM_TOOLS = Object.freeze([
|
|
92
105
|
'system.doctor',
|
|
93
106
|
]);
|
|
94
107
|
|
|
95
108
|
const REGISTERED_TOOLS = Object.freeze([
|
|
96
109
|
...TASK_TOOLS, ...SESSION_TOOLS, ...PLAN_TOOLS, ...BASELINE_TOOLS, ...CHANGE_TOOLS,
|
|
110
|
+
...WORKFLOW_TOOLS,
|
|
97
111
|
...SYSTEM_TOOLS,
|
|
98
112
|
]);
|
|
99
113
|
|
|
@@ -109,6 +123,7 @@ const MUTATING_TOOLS = new Set([
|
|
|
109
123
|
'baseline.start', 'baseline.record', 'baseline.converge',
|
|
110
124
|
'change.create', 'change.update', 'change.context', 'change.converge', 'change.archive',
|
|
111
125
|
'change.learning_propose', 'change.learning_resolve',
|
|
126
|
+
'workflow.start', 'workflow.step', 'workflow.complete',
|
|
112
127
|
]);
|
|
113
128
|
|
|
114
129
|
function loadRegisteredTools() {
|
|
@@ -127,22 +142,6 @@ function mintSessionId() {
|
|
|
127
142
|
return `sess-${randomUUID().slice(0, 8)}`;
|
|
128
143
|
}
|
|
129
144
|
|
|
130
|
-
function resolvePrdText(input, ctx) {
|
|
131
|
-
if (input.prd_text && String(input.prd_text).trim()) return String(input.prd_text);
|
|
132
|
-
if (input.prd_path) {
|
|
133
|
-
const abs = path.isAbsolute(input.prd_path)
|
|
134
|
-
? input.prd_path
|
|
135
|
-
: path.join(ctx.rootDir || process.cwd(), input.prd_path);
|
|
136
|
-
return fs.readFileSync(abs, 'utf8');
|
|
137
|
-
}
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function buildLlmClient(ctx) {
|
|
142
|
-
if (ctx && ctx.llmClient) return ctx.llmClient;
|
|
143
|
-
return llm.createLlmClient({});
|
|
144
|
-
}
|
|
145
|
-
|
|
146
145
|
async function dispatchTool(name, input, db, ctx = {}) {
|
|
147
146
|
switch (name) {
|
|
148
147
|
case 'task.create': {
|
|
@@ -265,10 +264,33 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
265
264
|
return ops.switchTaskTag(db, input.id, input.tag);
|
|
266
265
|
}
|
|
267
266
|
case 'task.dependency_topo': {
|
|
268
|
-
const
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
267
|
+
const hasTaskIds = Array.isArray(input && input.task_ids);
|
|
268
|
+
const hasChangeId = typeof input?.change_id === 'string' && input.change_id.trim() !== '';
|
|
269
|
+
if (hasTaskIds === hasChangeId) {
|
|
270
|
+
const err = new Error('provide exactly one of task_ids or change_id');
|
|
271
|
+
err.code = 'VALIDATION_ERROR';
|
|
272
|
+
throw err;
|
|
273
|
+
}
|
|
274
|
+
let rows;
|
|
275
|
+
if (hasTaskIds) {
|
|
276
|
+
rows = input.task_ids.map((id) => {
|
|
277
|
+
const task = ops.readTask(db, id);
|
|
278
|
+
if (!task) {
|
|
279
|
+
const err = new Error(`task ${id} not found`);
|
|
280
|
+
err.code = 'TASK_NOT_FOUND';
|
|
281
|
+
throw err;
|
|
282
|
+
}
|
|
283
|
+
return task;
|
|
284
|
+
});
|
|
285
|
+
} else {
|
|
286
|
+
const change = changes.readChange(db, input.change_id);
|
|
287
|
+
if (!change) {
|
|
288
|
+
const err = new Error(`change ${input.change_id} not found`);
|
|
289
|
+
err.code = 'CHANGE_NOT_FOUND';
|
|
290
|
+
throw err;
|
|
291
|
+
}
|
|
292
|
+
rows = ops.listTasks(db, { change_id: change.id });
|
|
293
|
+
}
|
|
272
294
|
const graph = rows.map((t) => ({
|
|
273
295
|
id: t.id,
|
|
274
296
|
deps: Array.isArray(t.deps) ? t.deps : [],
|
|
@@ -283,23 +305,32 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
283
305
|
return { waves: result.waves };
|
|
284
306
|
}
|
|
285
307
|
case 'task.parse_prd': {
|
|
286
|
-
const prdText = resolvePrdText(input, ctx);
|
|
287
|
-
if (!prdText || !prdText.trim()) {
|
|
288
|
-
const err = new Error('one of prd_path or prd_text required');
|
|
289
|
-
err.code = 'NO_INPUT';
|
|
290
|
-
throw err;
|
|
291
|
-
}
|
|
292
308
|
const dryRun = input.dry_run === true;
|
|
293
309
|
const rootDir = ctx.rootDir || process.cwd();
|
|
310
|
+
if (!dryRun && !String(input.change_id || '').trim()) {
|
|
311
|
+
const err = new Error('persisting a parsed task graph requires change_id');
|
|
312
|
+
err.code = 'CHANGE_REQUIRED';
|
|
313
|
+
throw err;
|
|
314
|
+
}
|
|
294
315
|
const creationAuthority = { change_id: input.change_id };
|
|
295
316
|
if (!dryRun) changes.assertTaskCreationAllowed(db, creationAuthority, { rootDir });
|
|
296
|
-
const
|
|
297
|
-
|
|
317
|
+
const parsed = parser.parsePrd(input.tasks, {
|
|
318
|
+
tag: input.tag,
|
|
319
|
+
changeId: input.change_id,
|
|
320
|
+
});
|
|
298
321
|
const shaped = parsed.tasks.map((t) => ({
|
|
299
322
|
id: t.id, title: t.title, type: t.type, priority: t.priority,
|
|
300
323
|
complexity: t.complexity, deps: t.deps, files_modified: t.files_modified,
|
|
301
|
-
tag: t.tag, change_id:
|
|
324
|
+
tag: t.tag, change_id: t.change_id,
|
|
302
325
|
}));
|
|
326
|
+
const graph = shaped.map((t) => ({ id: t.id, deps: t.deps || [] }));
|
|
327
|
+
const topoResult = topo.computeWaves(graph);
|
|
328
|
+
if (topoResult.cycles.length > 0) {
|
|
329
|
+
const err = new Error(`parsed task graph has ${topoResult.cycles.length} cycle(s)`);
|
|
330
|
+
err.code = 'CYCLE_DETECTED';
|
|
331
|
+
err.details = { cycles: topoResult.cycles };
|
|
332
|
+
throw err;
|
|
333
|
+
}
|
|
303
334
|
if (!dryRun) {
|
|
304
335
|
try {
|
|
305
336
|
ops.tx(db, () => {
|
|
@@ -314,13 +345,9 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
314
345
|
throw wrap;
|
|
315
346
|
}
|
|
316
347
|
}
|
|
317
|
-
const graph = shaped.map((t) => ({ id: t.id, deps: t.deps || [] }));
|
|
318
|
-
const topoResult = topo.computeWaves(graph);
|
|
319
348
|
return { tasks: shaped, topo: topoResult.waves };
|
|
320
349
|
}
|
|
321
350
|
case 'task.expand': {
|
|
322
|
-
// Cheap guards before paying for LLM client construction (which would
|
|
323
|
-
// throw NO_LLM_CREDENTIALS when env keys are missing).
|
|
324
351
|
const parent = ops.readTask(db, input.id);
|
|
325
352
|
if (!parent) {
|
|
326
353
|
const err = new Error(`no task ${input.id}`);
|
|
@@ -334,32 +361,43 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
334
361
|
}
|
|
335
362
|
const rootDir = ctx.rootDir || process.cwd();
|
|
336
363
|
changes.assertTaskCreationAllowed(db, { change_id: parent.change_id }, { rootDir });
|
|
337
|
-
const
|
|
338
|
-
const result = await expander.expandTask(db, {
|
|
364
|
+
const result = expander.expandTask(db, {
|
|
339
365
|
id: input.id,
|
|
340
|
-
|
|
341
|
-
strategy: input.strategy,
|
|
342
|
-
llmClient: client,
|
|
366
|
+
children: input.children,
|
|
343
367
|
rootDir,
|
|
344
368
|
});
|
|
345
369
|
return { parent_id: result.parent_id, children: result.children };
|
|
346
370
|
}
|
|
347
371
|
case 'plan.export': {
|
|
348
372
|
const rootDir = ctx.rootDir || process.cwd();
|
|
373
|
+
const change = changes.readChange(db, input.change_id);
|
|
374
|
+
if (!change) {
|
|
375
|
+
const err = new Error(`change ${input.change_id} not found`);
|
|
376
|
+
err.code = 'CHANGE_NOT_FOUND';
|
|
377
|
+
throw err;
|
|
378
|
+
}
|
|
379
|
+
if (!['active', 'blocked'].includes(change.status)) {
|
|
380
|
+
const err = new Error(`change ${change.id} is ${change.status}`);
|
|
381
|
+
err.code = 'CHANGE_NOT_MUTABLE';
|
|
382
|
+
throw err;
|
|
383
|
+
}
|
|
349
384
|
const abs = path.isAbsolute(input.out_path)
|
|
350
385
|
? input.out_path
|
|
351
386
|
: path.join(rootDir, input.out_path);
|
|
352
|
-
const tasks = ops.listTasks(db, { tag: input.tag });
|
|
387
|
+
const tasks = ops.listTasks(db, { tag: input.tag, change_id: change.id });
|
|
353
388
|
if (tasks.length === 0) {
|
|
354
389
|
const err = new Error('no tasks to plan');
|
|
355
390
|
err.code = 'NO_TASKS';
|
|
356
391
|
throw err;
|
|
357
392
|
}
|
|
358
|
-
const plan = planStore.buildPlan(tasks, {});
|
|
393
|
+
const plan = planStore.buildPlan(tasks, { changeId: change.id });
|
|
359
394
|
const { plan_path } = planStore.savePlanArtifact(plan, abs, input.format || 'json');
|
|
360
395
|
ops.appendEvent(db, {
|
|
361
|
-
type: '
|
|
362
|
-
|
|
396
|
+
type: 'plan_exported',
|
|
397
|
+
change_id: change.id,
|
|
398
|
+
payload: {
|
|
399
|
+
change_id: change.id, plan_path, wave_count: plan.waves.length, tag: input.tag || null,
|
|
400
|
+
},
|
|
363
401
|
});
|
|
364
402
|
return { plan_path, wave_count: plan.waves.length };
|
|
365
403
|
}
|
|
@@ -371,6 +409,13 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
371
409
|
err.code = 'NO_PLAN';
|
|
372
410
|
throw err;
|
|
373
411
|
}
|
|
412
|
+
if (loaded.change_id !== input.change_id) {
|
|
413
|
+
const err = new Error(
|
|
414
|
+
`execution plan belongs to ${loaded.change_id || '(unbound)'}, not ${input.change_id}`,
|
|
415
|
+
);
|
|
416
|
+
err.code = 'PLAN_CHANGE_MISMATCH';
|
|
417
|
+
throw err;
|
|
418
|
+
}
|
|
374
419
|
return { plan: planStore.selectSection(loaded, input.section) };
|
|
375
420
|
}
|
|
376
421
|
case 'baseline.start': {
|
|
@@ -472,6 +517,44 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
472
517
|
case 'change.archive': {
|
|
473
518
|
return changes.archiveChange(db, input, { rootDir: ctx.rootDir || process.cwd() });
|
|
474
519
|
}
|
|
520
|
+
case 'workflow.start': {
|
|
521
|
+
return {
|
|
522
|
+
workflow: workflows.startWorkflow(
|
|
523
|
+
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
524
|
+
),
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
case 'workflow.get': {
|
|
528
|
+
const workflow = workflows.readWorkflow(
|
|
529
|
+
db, input.id, { rootDir: ctx.rootDir || process.cwd() },
|
|
530
|
+
);
|
|
531
|
+
if (!workflow) {
|
|
532
|
+
const error = new Error(`workflow ${input.id} not found`);
|
|
533
|
+
error.code = 'WORKFLOW_NOT_FOUND';
|
|
534
|
+
throw error;
|
|
535
|
+
}
|
|
536
|
+
return { workflow };
|
|
537
|
+
}
|
|
538
|
+
case 'workflow.list': {
|
|
539
|
+
const rows = workflows.listWorkflows(
|
|
540
|
+
db, input || {}, { rootDir: ctx.rootDir || process.cwd() },
|
|
541
|
+
);
|
|
542
|
+
return { workflows: rows, count: rows.length };
|
|
543
|
+
}
|
|
544
|
+
case 'workflow.step': {
|
|
545
|
+
return {
|
|
546
|
+
workflow: workflows.recordWorkflowStep(
|
|
547
|
+
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
548
|
+
),
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
case 'workflow.complete': {
|
|
552
|
+
return {
|
|
553
|
+
workflow: workflows.completeWorkflow(
|
|
554
|
+
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
555
|
+
),
|
|
556
|
+
};
|
|
557
|
+
}
|
|
475
558
|
case 'system.doctor': {
|
|
476
559
|
return doctor.runDoctor(db, {
|
|
477
560
|
rootDir: ctx.rootDir || process.cwd(),
|
|
@@ -517,7 +600,7 @@ function startServer({ dbPath, rootDir, projectOnWrite = true, project = project
|
|
|
517
600
|
{ capabilities: { tools: {} } },
|
|
518
601
|
);
|
|
519
602
|
|
|
520
|
-
server.setRequestHandler(
|
|
603
|
+
server.setRequestHandler('tools/list', async () => ({
|
|
521
604
|
tools: tools.map((t) => ({
|
|
522
605
|
name: t.name,
|
|
523
606
|
description: t.description,
|
|
@@ -525,7 +608,7 @@ function startServer({ dbPath, rootDir, projectOnWrite = true, project = project
|
|
|
525
608
|
})),
|
|
526
609
|
}));
|
|
527
610
|
|
|
528
|
-
server.setRequestHandler(
|
|
611
|
+
server.setRequestHandler('tools/call', async (request) => {
|
|
529
612
|
const { name, arguments: args = {} } = request.params;
|
|
530
613
|
const toolStart = Date.now();
|
|
531
614
|
let toolError = null;
|
|
@@ -639,6 +722,51 @@ async function main() {
|
|
|
639
722
|
process.on('SIGINT', cleanup);
|
|
640
723
|
}
|
|
641
724
|
|
|
725
|
+
function hookMetadata(value, fallback = 'unknown', maxLength = 256) {
|
|
726
|
+
const normalized = typeof value === 'string' ? value.trim() : '';
|
|
727
|
+
return (normalized || fallback).slice(0, maxLength);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function appendHookLifecycleEvent({ rootDir, action, hookInput = {} } = {}) {
|
|
731
|
+
if (!['start', 'stop'].includes(action)) {
|
|
732
|
+
const error = new Error('hook lifecycle action must be start or stop');
|
|
733
|
+
error.code = 'VALIDATION_ERROR';
|
|
734
|
+
throw error;
|
|
735
|
+
}
|
|
736
|
+
const root = path.resolve(rootDir || process.cwd());
|
|
737
|
+
const dbPath = path.join(root, '.ultra', 'state.db');
|
|
738
|
+
if (!fs.existsSync(dbPath)) return { recorded: false, reason: 'STATE_DB_MISSING' };
|
|
739
|
+
|
|
740
|
+
const db = openStateDb(dbPath);
|
|
741
|
+
try {
|
|
742
|
+
ensureSchemaVersion(db);
|
|
743
|
+
const change = db.prepare(
|
|
744
|
+
`SELECT id FROM changes WHERE status IN ('active', 'blocked', 'ready')
|
|
745
|
+
ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
|
|
746
|
+
).get();
|
|
747
|
+
if (!change) return { recorded: false, reason: 'NO_ACTIVE_CHANGE' };
|
|
748
|
+
const task = db.prepare(
|
|
749
|
+
`SELECT id FROM tasks WHERE change_id = ? AND status IN ('in_progress', 'pending', 'blocked')
|
|
750
|
+
ORDER BY CASE status WHEN 'in_progress' THEN 0 WHEN 'blocked' THEN 1 ELSE 2 END,
|
|
751
|
+
updated_at DESC, rowid DESC LIMIT 1`,
|
|
752
|
+
).get(change.id);
|
|
753
|
+
const payload = {
|
|
754
|
+
agent_id: hookMetadata(hookInput.agent_id),
|
|
755
|
+
agent_type: hookMetadata(hookInput.agent_type),
|
|
756
|
+
host_session_id: hookMetadata(hookInput.session_id, '', 256),
|
|
757
|
+
};
|
|
758
|
+
ops.appendEvent(db, {
|
|
759
|
+
type: action === 'start' ? 'subagent_started' : 'subagent_stopped',
|
|
760
|
+
change_id: change.id,
|
|
761
|
+
task_id: task?.id || null,
|
|
762
|
+
payload,
|
|
763
|
+
});
|
|
764
|
+
return { recorded: true, change_id: change.id, task_id: task?.id || null };
|
|
765
|
+
} finally {
|
|
766
|
+
closeStateDb(db);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
642
770
|
if (require.main === module) {
|
|
643
771
|
main().catch((err) => {
|
|
644
772
|
process.stderr.write(`mcp-server fatal: ${err.message}\n`);
|
|
@@ -655,8 +783,12 @@ module.exports = {
|
|
|
655
783
|
PLAN_TOOLS,
|
|
656
784
|
BASELINE_TOOLS,
|
|
657
785
|
CHANGE_TOOLS,
|
|
786
|
+
WORKFLOW_TOOLS,
|
|
658
787
|
SYSTEM_TOOLS,
|
|
659
788
|
REGISTERED_TOOLS,
|
|
660
789
|
STATELESS_TOOLS,
|
|
661
790
|
MUTATING_TOOLS,
|
|
791
|
+
appendHookLifecycleEvent,
|
|
792
|
+
readProjectBreadcrumb,
|
|
793
|
+
renderProjectBreadcrumb,
|
|
662
794
|
};
|
package/orchestrator/daemon.cjs
CHANGED
|
@@ -16,7 +16,7 @@ const runtimeState = require('../mcp-server/lib/runtime-state.cjs');
|
|
|
16
16
|
const projector = require('../mcp-server/lib/projector.cjs');
|
|
17
17
|
const runner = require('./session-runner.cjs');
|
|
18
18
|
const recovery = require('./recovery.cjs');
|
|
19
|
-
const { evaluate, DEFAULT_RULES
|
|
19
|
+
const { evaluate, DEFAULT_RULES } = require('./dispatch-rules.cjs');
|
|
20
20
|
|
|
21
21
|
// Phase 8B.1 — routeTask is now a thin wrapper over evaluate() so Phase 5.4
|
|
22
22
|
// callers see identical behavior while the parallel orchestrator (8B.2) can
|
|
@@ -146,6 +146,5 @@ function runDaemon({
|
|
|
146
146
|
module.exports = {
|
|
147
147
|
runDaemon,
|
|
148
148
|
routeTask,
|
|
149
|
-
ROUTE_PREFERENCES,
|
|
150
149
|
maintainState,
|
|
151
150
|
};
|
|
@@ -8,28 +8,13 @@
|
|
|
8
8
|
// • defer — leave task pending, revisit next tick (deps/wave wait)
|
|
9
9
|
// • block — terminal: don't spawn (breaker, no runtimes, etc.)
|
|
10
10
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
11
|
+
// Runtime order is caller-owned. The orchestrator does not infer model quality,
|
|
12
|
+
// price, or capability from a host name because every host can be configured
|
|
13
|
+
// with different models. The parallel orchestrator supplies richer dimensions:
|
|
14
|
+
// wave state, dependency readiness, and explicit custom rules.
|
|
15
15
|
|
|
16
16
|
const { isSupportedRuntime } = require('../adapters/_shared/runtime-assets.cjs');
|
|
17
17
|
|
|
18
|
-
const ROUTE_PREFERENCES = Object.freeze({
|
|
19
|
-
haiku: ['opencode', 'claude', 'codex'],
|
|
20
|
-
sonnet: ['claude', 'codex', 'opencode'],
|
|
21
|
-
opus: ['claude', 'codex'],
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
function resolveByPreference(ctx) {
|
|
25
|
-
const hint = ctx.task && ctx.task.complexity_hint;
|
|
26
|
-
const pref = hint ? ROUTE_PREFERENCES[hint] : null;
|
|
27
|
-
if (pref) {
|
|
28
|
-
for (const r of pref) if (ctx.available_runtimes.includes(r)) return r;
|
|
29
|
-
}
|
|
30
|
-
return ctx.available_runtimes[0];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
18
|
const DEFAULT_RULES = Object.freeze([
|
|
34
19
|
{
|
|
35
20
|
id: 'breaker-blocked',
|
|
@@ -56,17 +41,7 @@ const DEFAULT_RULES = Object.freeze([
|
|
|
56
41
|
action: 'defer',
|
|
57
42
|
},
|
|
58
43
|
{
|
|
59
|
-
id: '
|
|
60
|
-
priority: 10,
|
|
61
|
-
when: (ctx) => {
|
|
62
|
-
const hint = ctx.task && ctx.task.complexity_hint;
|
|
63
|
-
return !!(hint && ROUTE_PREFERENCES[hint]);
|
|
64
|
-
},
|
|
65
|
-
action: 'spawn_agent',
|
|
66
|
-
resolve: resolveByPreference,
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
id: 'fallback-first-available',
|
|
44
|
+
id: 'first-authorized-runtime',
|
|
70
45
|
priority: 0,
|
|
71
46
|
when: () => true,
|
|
72
47
|
action: 'spawn_agent',
|
|
@@ -100,5 +75,4 @@ function evaluate(ctx, rules = DEFAULT_RULES) {
|
|
|
100
75
|
module.exports = {
|
|
101
76
|
evaluate,
|
|
102
77
|
DEFAULT_RULES,
|
|
103
|
-
ROUTE_PREFERENCES,
|
|
104
78
|
};
|
|
@@ -15,18 +15,22 @@ const DEFAULT_COMPLEXITY = 3;
|
|
|
15
15
|
const TOKENS_PER_COMPLEXITY = Object.freeze({ input: 5000, output: 2000 });
|
|
16
16
|
const DURATION_MIN_PER_COMPLEXITY = 5;
|
|
17
17
|
|
|
18
|
-
function buildPlan(tasks, { runtime =
|
|
18
|
+
function buildPlan(tasks, { runtime = null, model = null, changeId = null } = {}) {
|
|
19
19
|
if (!Array.isArray(tasks)) {
|
|
20
20
|
throw new TypeError('buildPlan: tasks must be an array');
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
// Plan artifacts are durable and compared by digest. Keep their ordering
|
|
24
|
+
// independent from SQLite insertion order or caller enumeration order.
|
|
25
|
+
const orderedTasks = [...tasks].sort((left, right) => String(left.id).localeCompare(String(right.id)));
|
|
26
|
+
|
|
27
|
+
const graph = orderedTasks.map((t) => ({
|
|
24
28
|
id: t.id,
|
|
25
29
|
deps: Array.isArray(t.deps) ? t.deps : [],
|
|
26
30
|
}));
|
|
27
31
|
const { waves, cycles } = computeWaves(graph);
|
|
28
32
|
|
|
29
|
-
const byId = new Map(
|
|
33
|
+
const byId = new Map(orderedTasks.map((t) => [t.id, t]));
|
|
30
34
|
|
|
31
35
|
const conflict_surface = [];
|
|
32
36
|
const wavePayload = waves.map((waveTasks, idx) => {
|
|
@@ -52,18 +56,21 @@ function buildPlan(tasks, { runtime = 'claude', model = null } = {}) {
|
|
|
52
56
|
});
|
|
53
57
|
|
|
54
58
|
const ownership_forecast = {};
|
|
55
|
-
for (const t of
|
|
59
|
+
for (const t of orderedTasks) {
|
|
56
60
|
ownership_forecast[t.id] = Array.isArray(t.files_modified) ? t.files_modified : [];
|
|
57
61
|
}
|
|
58
62
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
const taskCosts = orderedTasks.map((t) => {
|
|
64
|
+
const complexity = Number(t.complexity || DEFAULT_COMPLEXITY);
|
|
65
|
+
const ti = complexity * TOKENS_PER_COMPLEXITY.input;
|
|
66
|
+
const to = complexity * TOKENS_PER_COMPLEXITY.output;
|
|
67
|
+
return computeCost(runtime, model, ti, to);
|
|
68
|
+
});
|
|
69
|
+
const estimated_cost_usd = orderedTasks.length === 0
|
|
70
|
+
? 0
|
|
71
|
+
: (taskCosts.some((cost) => cost === null)
|
|
72
|
+
? null
|
|
73
|
+
: round4(taskCosts.reduce((sum, cost) => sum + cost, 0)));
|
|
67
74
|
|
|
68
75
|
const estimated_duration_min = wavePayload.reduce((acc, wv) => {
|
|
69
76
|
const complexities = wv.tasks.map(
|
|
@@ -76,6 +83,8 @@ function buildPlan(tasks, { runtime = 'claude', model = null } = {}) {
|
|
|
76
83
|
}, 0);
|
|
77
84
|
|
|
78
85
|
return {
|
|
86
|
+
schema_version: '1.0',
|
|
87
|
+
change_id: changeId,
|
|
79
88
|
waves: wavePayload,
|
|
80
89
|
ownership_forecast,
|
|
81
90
|
conflict_surface,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra-builder-pro-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Multi-runtime installer for the Ultra Builder Pro agent engineering system — deploys commands, agents, skills, hooks, and MCP server to Claude Code, OpenCode, Codex CLI, and Kimi Code.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ultra-builder-pro-cli": "bin/install.js",
|
|
@@ -49,13 +49,20 @@
|
|
|
49
49
|
".ultra-template",
|
|
50
50
|
"docs/ROADMAP.md",
|
|
51
51
|
"docs/ARCHITECTURE.md",
|
|
52
|
+
"docs/WORKFLOW-LIFECYCLE.md",
|
|
52
53
|
"docs/AGENT-CONTEXT.md",
|
|
53
54
|
"docs/SKILL-AUTHORING.md",
|
|
54
55
|
"docs/COMMIT-HASH-BACKFILL.md",
|
|
55
56
|
"docs/PLAN.zh-CN.md",
|
|
56
57
|
"docs/STATE-DB-ACCESS-POLICY.md",
|
|
57
58
|
"docs/USER-HANDBOOK-CONTRACT.md",
|
|
58
|
-
"docs/RUNTIME-COMPAT-MATRIX.md"
|
|
59
|
+
"docs/RUNTIME-COMPAT-MATRIX.md",
|
|
60
|
+
"!**/__pycache__/**",
|
|
61
|
+
"!**/*.pyc",
|
|
62
|
+
"!**/*.pyo",
|
|
63
|
+
"!**/*.test.cjs",
|
|
64
|
+
"!**/tests/**",
|
|
65
|
+
"!mcp-server/test-support/**"
|
|
59
66
|
],
|
|
60
67
|
"keywords": [
|
|
61
68
|
"claude-code",
|
|
@@ -98,13 +105,14 @@
|
|
|
98
105
|
"install:all": "node bin/install.js --all --local"
|
|
99
106
|
},
|
|
100
107
|
"dependencies": {
|
|
101
|
-
"@
|
|
102
|
-
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
108
|
+
"@modelcontextprotocol/server": "2.0.0-beta.5",
|
|
103
109
|
"@vercel/ncc": "0.38.4",
|
|
104
110
|
"ajv": "^8.17.1",
|
|
105
111
|
"ajv-formats": "^3.0.1",
|
|
106
112
|
"better-sqlite3": "^11.5.0",
|
|
107
|
-
"js-yaml": "^4.1.0"
|
|
108
|
-
|
|
113
|
+
"js-yaml": "^4.1.0"
|
|
114
|
+
},
|
|
115
|
+
"devDependencies": {
|
|
116
|
+
"@modelcontextprotocol/client": "2.0.0-beta.5"
|
|
109
117
|
}
|
|
110
118
|
}
|
|
@@ -5,8 +5,8 @@ description: Ask Claude Code for an independent read-only analysis of a bounded
|
|
|
5
5
|
|
|
6
6
|
# Consult Claude Code read-only
|
|
7
7
|
|
|
8
|
-
Treat Claude Code as an untrusted advisor.
|
|
9
|
-
decisions, edits, and final verification.
|
|
8
|
+
Treat Claude Code as an untrusted advisor. Keep the current host responsible for
|
|
9
|
+
scope, evidence, decisions, edits, and final verification.
|
|
10
10
|
|
|
11
11
|
## Preconditions
|
|
12
12
|
|
|
@@ -5,8 +5,8 @@ description: Ask Codex CLI for an independent read-only analysis of a bounded ar
|
|
|
5
5
|
|
|
6
6
|
# Consult Codex read-only
|
|
7
7
|
|
|
8
|
-
Treat Codex as an untrusted advisor.
|
|
9
|
-
verification, and the final answer.
|
|
8
|
+
Treat Codex as an untrusted advisor. Keep the current host responsible for scope,
|
|
9
|
+
evidence, edits, verification, and the final answer.
|
|
10
10
|
|
|
11
11
|
## Preconditions
|
|
12
12
|
|
|
@@ -23,7 +23,6 @@ Create a session directory under `.ultra/collab/`, then run in a read-only sandb
|
|
|
23
23
|
SESSION_PATH=".ultra/collab/$(date +%Y%m%d-%H%M%S)-codex"
|
|
24
24
|
mkdir -p "${SESSION_PATH}"
|
|
25
25
|
codex exec -s read-only \
|
|
26
|
-
-a never \
|
|
27
26
|
--ephemeral \
|
|
28
27
|
--ignore-user-config \
|
|
29
28
|
--ignore-rules \
|