ultra-builder-pro-cli 0.23.0 → 0.24.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 +4 -4
- package/CHANGELOG.md +45 -0
- package/README.md +21 -18
- package/adapters/_shared/codex-assets.cjs +121 -65
- package/adapters/_shared/interaction-contract.cjs +7 -1
- package/adapters/_shared/provenance.cjs +7 -2
- package/adapters/_shared/runtime-assets.cjs +3 -2
- package/adapters/_shared/runtime-native.cjs +132 -0
- package/adapters/claude.js +60 -30
- package/adapters/codex.js +178 -81
- package/adapters/grok.js +372 -0
- package/adapters/kimi.js +7 -3
- package/adapters/opencode.js +210 -32
- package/agents/code-reviewer.md +4 -2
- package/agents/debugger.md +4 -1
- package/agents/review-code.md +7 -3
- package/agents/review-comments.md +7 -4
- package/agents/review-coordinator.md +11 -7
- package/agents/review-design.md +8 -4
- package/agents/review-errors.md +8 -4
- package/agents/review-spec.md +8 -5
- package/agents/review-tests.md +7 -4
- package/agents/tdd-runner.md +5 -2
- package/bin/install.js +9 -4
- package/bin/orchestrator.js +4 -13
- package/docs/AGENT-CONTEXT.md +141 -229
- package/docs/ARCHITECTURE.md +203 -494
- package/docs/ARTIFACT-AUTHORITY.md +26 -22
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +12 -9
- package/docs/ROADMAP.md +85 -110
- package/docs/RUNTIME-COMPAT-MATRIX.md +139 -175
- package/docs/SKILL-AUTHORING.md +3 -3
- package/docs/STATE-DB-ACCESS-POLICY.md +13 -8
- package/docs/WORKFLOW-LIFECYCLE.md +174 -448
- package/hooks/README.md +11 -8
- package/hooks/active_task_context.py +16 -16
- package/hooks/adapters/grok.py +134 -0
- package/hooks/{context_spine.py → context_envelope.py} +26 -22
- package/hooks/health_check.py +19 -29
- package/hooks/pre_stop_check.py +22 -12
- package/hooks/subagent_tracker.py +1 -1
- package/hooks/workflow_checkpoint.py +14 -13
- package/hooks/workflow_context.py +11 -12
- package/hooks/workflow_resume.py +14 -13
- package/mcp-server/{breadcrumb.cjs → hook-context.cjs} +7 -9
- package/mcp-server/lib/baseline-workflow.cjs +36 -38
- package/mcp-server/lib/canonical-json.cjs +19 -0
- package/mcp-server/lib/change-workflow.cjs +596 -44
- package/mcp-server/lib/context-envelope.cjs +508 -0
- package/mcp-server/lib/decision-records.cjs +337 -0
- package/mcp-server/lib/doctor.cjs +91 -33
- package/mcp-server/lib/init-project.cjs +78 -39
- package/mcp-server/lib/managed-file-write.cjs +116 -0
- package/mcp-server/lib/plan-checkpoint.cjs +177 -0
- package/mcp-server/lib/project-context.cjs +178 -0
- package/mcp-server/lib/stage-checkpoints.cjs +309 -0
- package/mcp-server/lib/state-db.cjs +155 -24
- package/mcp-server/lib/task-ledger.cjs +522 -13
- package/mcp-server/lib/ultra-facade.cjs +630 -442
- package/mcp-server/lib/worker-packet.cjs +358 -0
- package/mcp-server/server.cjs +200 -1103
- package/orchestrator/daemon.cjs +27 -9
- package/orchestrator/parallel-orchestrator.cjs +111 -76
- package/orchestrator/session-runner.cjs +135 -34
- package/package.json +10 -3
- package/skills/ultra-change/SKILL.md +9 -8
- package/skills/ultra-deliver/SKILL.md +3 -3
- package/skills/ultra-dev/SKILL.md +18 -11
- package/skills/ultra-doctor/SKILL.md +2 -2
- package/skills/ultra-init/SKILL.md +6 -5
- package/skills/ultra-init/references/brownfield-adoption.md +11 -8
- package/skills/ultra-plan/SKILL.md +10 -8
- package/skills/ultra-research/SKILL.md +12 -7
- package/skills/ultra-review/SKILL.md +9 -6
- package/skills/ultra-review/references/unified-schema.md +2 -0
- package/skills/ultra-review/scripts/review_wait.py +8 -0
- package/skills/ultra-status/SKILL.md +2 -2
- package/skills/ultra-test/SKILL.md +4 -2
- package/skills/ultra-think/SKILL.md +7 -7
- package/skills/ultra-think/references/interaction-boundary.md +56 -0
- package/spec/cli-protocol.md +80 -199
- package/spec/mcp-tools.yaml +37 -2804
- package/spec/schemas/state-db.sql +107 -4
- package/spec/schemas/{task-ledger.v1.schema.json → task-ledger.v2.schema.json} +70 -6
- package/ultra-tools/commands/status.cjs +369 -243
- package/mcp-server/lib/context-spine.cjs +0 -1208
- package/mcp-server/lib/decision-dialogue.cjs +0 -1128
- package/mcp-server/lib/project-breadcrumb.cjs +0 -173
- package/mcp-server/lib/spec-learning.cjs +0 -284
- package/mcp-server/lib/workflow-state.cjs +0 -3129
- package/skills/ultra-think/references/decision-dialogue.md +0 -155
package/mcp-server/server.cjs
CHANGED
|
@@ -1,194 +1,53 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// MCP server (stdio JSON-RPC) exposing task.* + session.* tools backed by
|
|
4
|
-
// .ultra/.runtime/state.db. Uses the low-level Server API from
|
|
5
|
-
// @modelcontextprotocol/server so we can pass raw JSON Schema (Draft 2020-12)
|
|
6
|
-
// straight from spec/mcp-tools.yaml without translating to zod.
|
|
7
|
-
|
|
8
3
|
const fs = require('node:fs');
|
|
9
4
|
const path = require('node:path');
|
|
10
5
|
|
|
11
|
-
const yaml = require('js-yaml');
|
|
12
6
|
const Ajv = require('ajv/dist/2020');
|
|
13
7
|
const addFormats = require('ajv-formats');
|
|
14
|
-
|
|
8
|
+
const yaml = require('js-yaml');
|
|
15
9
|
const { Server } = require('@modelcontextprotocol/server');
|
|
16
10
|
const { StdioServerTransport } = require('@modelcontextprotocol/server/stdio');
|
|
17
|
-
const { version: PACKAGE_VERSION } = require('../package.json');
|
|
18
11
|
|
|
12
|
+
const { version: PACKAGE_VERSION } = require('../package.json');
|
|
19
13
|
const {
|
|
20
|
-
|
|
14
|
+
closeStateDb,
|
|
15
|
+
ensureSchemaVersion,
|
|
16
|
+
initStateDb,
|
|
17
|
+
openStateDb,
|
|
21
18
|
} = require('./lib/state-db.cjs');
|
|
22
19
|
const { assertStateAuthority } = require('./lib/state-authority.cjs');
|
|
20
|
+
const gitBootstrap = require('./lib/git-bootstrap.cjs');
|
|
23
21
|
const ops = require('./lib/state-ops.cjs');
|
|
22
|
+
const planStore = require('./lib/plan-store.cjs');
|
|
24
23
|
const projector = require('./lib/projector.cjs');
|
|
24
|
+
const {
|
|
25
|
+
readProjectContextEnvelope,
|
|
26
|
+
renderProjectContextEnvelope,
|
|
27
|
+
} = require('./lib/project-context.cjs');
|
|
28
|
+
const runtimePaths = require('./lib/runtime-paths.cjs');
|
|
29
|
+
const runtimeState = require('./lib/runtime-state.cjs');
|
|
25
30
|
const taskLedger = require('./lib/task-ledger.cjs');
|
|
26
31
|
const telemetry = require('./lib/telemetry.cjs');
|
|
27
|
-
const topo = require('./lib/topo.cjs');
|
|
28
|
-
const parser = require('./lib/prd-parser.cjs');
|
|
29
|
-
const expander = require('./lib/task-expander.cjs');
|
|
30
|
-
const planStore = require('./lib/plan-store.cjs');
|
|
31
|
-
const { initProject } = require('./lib/init-project.cjs');
|
|
32
|
-
const baselines = require('./lib/baseline-workflow.cjs');
|
|
33
|
-
const changes = require('./lib/change-workflow.cjs');
|
|
34
|
-
const contextSpine = require('./lib/context-spine.cjs');
|
|
35
|
-
const runtimeState = require('./lib/runtime-state.cjs');
|
|
36
|
-
const runtimePaths = require('./lib/runtime-paths.cjs');
|
|
37
|
-
const gitBootstrap = require('./lib/git-bootstrap.cjs');
|
|
38
|
-
const workflows = require('./lib/workflow-state.cjs');
|
|
39
|
-
const decisions = require('./lib/decision-dialogue.cjs');
|
|
40
|
-
const doctor = require('./lib/doctor.cjs');
|
|
41
|
-
const artifactRegistry = require('./lib/artifact-registry.cjs');
|
|
42
32
|
const ultraFacade = require('./lib/ultra-facade.cjs');
|
|
43
|
-
const {
|
|
44
|
-
readProjectBreadcrumb,
|
|
45
|
-
renderProjectBreadcrumb,
|
|
46
|
-
} = require('./lib/project-breadcrumb.cjs');
|
|
47
|
-
const sessionRunner = require('../orchestrator/session-runner.cjs');
|
|
48
33
|
|
|
49
34
|
const REPO_ROOT = process.env.UBP_RUNTIME_ROOT
|
|
50
35
|
? path.resolve(process.env.UBP_RUNTIME_ROOT)
|
|
51
36
|
: path.resolve(__dirname, '..');
|
|
52
37
|
const TOOLS_FILE = path.join(REPO_ROOT, 'spec', 'mcp-tools.yaml');
|
|
53
|
-
|
|
54
|
-
const TASK_TOOLS = Object.freeze([
|
|
55
|
-
'task.create',
|
|
56
|
-
'task.update',
|
|
57
|
-
'task.list',
|
|
58
|
-
'task.get',
|
|
59
|
-
'task.delete',
|
|
60
|
-
'task.init_project',
|
|
61
|
-
'task.append_event',
|
|
62
|
-
'task.subscribe_events',
|
|
63
|
-
'task.switch_tag',
|
|
64
|
-
'task.dependency_topo',
|
|
65
|
-
'task.parse_prd',
|
|
66
|
-
'task.expand',
|
|
67
|
-
'task.ledger_get',
|
|
68
|
-
'task.ledger_publish',
|
|
69
|
-
'task.ledger_import',
|
|
70
|
-
]);
|
|
71
|
-
|
|
72
|
-
const SESSION_TOOLS = Object.freeze([
|
|
73
|
-
'session.spawn',
|
|
74
|
-
'session.close',
|
|
75
|
-
'session.get',
|
|
76
|
-
'session.list',
|
|
77
|
-
'session.admission_check',
|
|
78
|
-
'session.heartbeat',
|
|
79
|
-
'session.subscribe_events',
|
|
80
|
-
]);
|
|
81
|
-
|
|
82
|
-
const PLAN_TOOLS = Object.freeze([
|
|
83
|
-
'plan.export',
|
|
84
|
-
'plan.get',
|
|
85
|
-
]);
|
|
86
|
-
|
|
87
|
-
const BASELINE_TOOLS = Object.freeze([
|
|
88
|
-
'baseline.start',
|
|
89
|
-
'baseline.record',
|
|
90
|
-
'baseline.get',
|
|
91
|
-
'baseline.converge',
|
|
92
|
-
]);
|
|
93
|
-
|
|
94
|
-
const CHANGE_TOOLS = Object.freeze([
|
|
95
|
-
'change.create',
|
|
96
|
-
'change.update',
|
|
97
|
-
'change.delta',
|
|
98
|
-
'change.documentation_reconcile',
|
|
99
|
-
'change.get',
|
|
100
|
-
'change.list',
|
|
101
|
-
'change.context',
|
|
102
|
-
'change.breadcrumb',
|
|
103
|
-
'change.learning_propose',
|
|
104
|
-
'change.learning_resolve',
|
|
105
|
-
'change.converge',
|
|
106
|
-
'change.archive',
|
|
107
|
-
]);
|
|
108
|
-
|
|
109
|
-
const WORKFLOW_TOOLS = Object.freeze([
|
|
110
|
-
'workflow.start',
|
|
111
|
-
'workflow.get',
|
|
112
|
-
'workflow.list',
|
|
113
|
-
'workflow.step',
|
|
114
|
-
'workflow.revise',
|
|
115
|
-
'workflow.supersede',
|
|
116
|
-
'workflow.complete',
|
|
117
|
-
]);
|
|
118
|
-
|
|
119
|
-
const DECISION_TOOLS = Object.freeze([
|
|
120
|
-
'decision.thread_start',
|
|
121
|
-
'decision.get',
|
|
122
|
-
'decision.list',
|
|
123
|
-
'decision.open',
|
|
124
|
-
'decision.resolve',
|
|
125
|
-
'decision.delegate',
|
|
126
|
-
'decision.defer',
|
|
127
|
-
'decision.supersede',
|
|
128
|
-
'decision.complete',
|
|
129
|
-
'decision.checkpoint',
|
|
130
|
-
]);
|
|
131
|
-
|
|
132
|
-
const SYSTEM_TOOLS = Object.freeze([
|
|
133
|
-
'system.doctor',
|
|
134
|
-
]);
|
|
135
|
-
|
|
136
|
-
const ARTIFACT_TOOLS = Object.freeze([
|
|
137
|
-
'artifact.record',
|
|
138
|
-
'artifact.get',
|
|
139
|
-
]);
|
|
140
|
-
|
|
141
|
-
const LEGACY_TOOLS = Object.freeze([
|
|
142
|
-
...TASK_TOOLS, ...SESSION_TOOLS, ...PLAN_TOOLS, ...BASELINE_TOOLS, ...CHANGE_TOOLS,
|
|
143
|
-
...DECISION_TOOLS,
|
|
144
|
-
...WORKFLOW_TOOLS,
|
|
145
|
-
...ARTIFACT_TOOLS,
|
|
146
|
-
...SYSTEM_TOOLS,
|
|
147
|
-
]);
|
|
148
38
|
const PUBLIC_TOOLS = ultraFacade.PUBLIC_TOOLS;
|
|
149
|
-
const REGISTERED_TOOLS =
|
|
150
|
-
|
|
151
|
-
const STATELESS_TOOLS = new Set(['task.init_project']);
|
|
152
|
-
|
|
153
|
-
function isStatelessPublicCall(name, input = {}) {
|
|
154
|
-
return name === 'ultra.record'
|
|
155
|
-
&& Array.isArray(input.entries)
|
|
156
|
-
&& input.entries.length === 1
|
|
157
|
-
&& input.entries[0]?.operation === 'task.init_project';
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// init_project owns its target project's schema, baseline seed, and first
|
|
161
|
-
// projection internally. Do not enqueue a second projection on the caller DB.
|
|
162
|
-
const MUTATING_TOOLS = new Set([
|
|
163
|
-
'ultra.record', 'ultra.checkpoint', 'ultra.sync', 'ultra.session', 'ultra.archive',
|
|
164
|
-
'task.create', 'task.update', 'task.delete', 'task.append_event', 'task.switch_tag',
|
|
165
|
-
'task.parse_prd', 'task.expand',
|
|
166
|
-
'task.ledger_import',
|
|
167
|
-
'session.spawn', 'session.close', 'session.heartbeat',
|
|
168
|
-
'plan.export',
|
|
169
|
-
'baseline.start', 'baseline.record', 'baseline.converge',
|
|
170
|
-
'change.create', 'change.update', 'change.delta', 'change.documentation_reconcile',
|
|
171
|
-
'change.context', 'change.converge', 'change.archive',
|
|
172
|
-
'change.learning_propose', 'change.learning_resolve',
|
|
173
|
-
'decision.thread_start', 'decision.open', 'decision.resolve', 'decision.delegate',
|
|
174
|
-
'decision.defer', 'decision.supersede', 'decision.complete', 'decision.checkpoint',
|
|
175
|
-
'workflow.start', 'workflow.step', 'workflow.revise', 'workflow.supersede',
|
|
176
|
-
'workflow.complete',
|
|
177
|
-
'artifact.record',
|
|
178
|
-
]);
|
|
39
|
+
const REGISTERED_TOOLS = PUBLIC_TOOLS;
|
|
179
40
|
|
|
180
|
-
|
|
181
|
-
'ultra.record', 'ultra.checkpoint', 'ultra.archive',
|
|
182
|
-
'task.create', 'task.update', 'task.delete', 'task.parse_prd', 'task.expand',
|
|
183
|
-
'plan.export',
|
|
184
|
-
'baseline.start', 'baseline.record', 'baseline.converge',
|
|
185
|
-
'change.create', 'change.update', 'change.delta', 'change.documentation_reconcile',
|
|
186
|
-
'change.converge', 'change.archive',
|
|
187
|
-
]);
|
|
188
|
-
|
|
189
|
-
function loadRegisteredTools() {
|
|
41
|
+
function loadToolContracts() {
|
|
190
42
|
const manifest = yaml.load(fs.readFileSync(TOOLS_FILE, 'utf8'));
|
|
191
|
-
|
|
43
|
+
const names = manifest.tools.map((tool) => tool.name).sort();
|
|
44
|
+
const expected = [...PUBLIC_TOOLS].sort();
|
|
45
|
+
if (JSON.stringify(names) !== JSON.stringify(expected)) {
|
|
46
|
+
const error = new Error('published MCP manifest does not match the seven-tool kernel');
|
|
47
|
+
error.code = 'PUBLIC_CONTRACT_DRIFT';
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
return manifest.tools;
|
|
192
51
|
}
|
|
193
52
|
|
|
194
53
|
function buildAjv() {
|
|
@@ -197,754 +56,6 @@ function buildAjv() {
|
|
|
197
56
|
return ajv;
|
|
198
57
|
}
|
|
199
58
|
|
|
200
|
-
async function dispatchTool(name, input, db, ctx = {}) {
|
|
201
|
-
switch (name) {
|
|
202
|
-
case 'task.create': {
|
|
203
|
-
const { randomUUID } = require('node:crypto');
|
|
204
|
-
const id = input.id || `task-${randomUUID()}`;
|
|
205
|
-
const changeId = ops.resolveTaskCreationChangeId(db, input);
|
|
206
|
-
const authorizedInput = { ...input, change_id: changeId };
|
|
207
|
-
changes.assertTaskCreationAllowed(db, authorizedInput, { rootDir: ctx.rootDir || process.cwd() });
|
|
208
|
-
const task = ops.createTask(db, { ...authorizedInput, id });
|
|
209
|
-
return { id: task.id, status: task.status, created_at: task.created_at };
|
|
210
|
-
}
|
|
211
|
-
case 'task.update': {
|
|
212
|
-
const before = ops.readTask(db, input.id);
|
|
213
|
-
const task = ops.patchTask(db, input.id, input.patch || {});
|
|
214
|
-
const durableChanged = before
|
|
215
|
-
&& taskLedger.durableTask(before).digest !== taskLedger.durableTask(task).digest;
|
|
216
|
-
if (durableChanged) {
|
|
217
|
-
taskLedger.publishTaskLedger(db, {
|
|
218
|
-
rootDir: ctx.rootDir || process.cwd(),
|
|
219
|
-
reason: before.status !== task.status
|
|
220
|
-
? `task_${task.status}`
|
|
221
|
-
: 'task_contract_updated',
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
return { ok: true, task };
|
|
225
|
-
}
|
|
226
|
-
case 'task.list': {
|
|
227
|
-
const tasks = ops.listTasks(db, input || {});
|
|
228
|
-
return { tasks, count: tasks.length };
|
|
229
|
-
}
|
|
230
|
-
case 'task.get': {
|
|
231
|
-
const task = ops.readTask(db, input.id);
|
|
232
|
-
if (!task) {
|
|
233
|
-
const err = new Error(`task ${input.id} not found`);
|
|
234
|
-
err.code = 'TASK_NOT_FOUND';
|
|
235
|
-
throw err;
|
|
236
|
-
}
|
|
237
|
-
return { task };
|
|
238
|
-
}
|
|
239
|
-
case 'task.delete': {
|
|
240
|
-
const result = ops.deleteTask(db, input.id, { force: !!input.force });
|
|
241
|
-
taskLedger.publishTaskLedger(db, {
|
|
242
|
-
rootDir: ctx.rootDir || process.cwd(),
|
|
243
|
-
reason: 'task_deleted',
|
|
244
|
-
});
|
|
245
|
-
return result;
|
|
246
|
-
}
|
|
247
|
-
case 'task.init_project': {
|
|
248
|
-
return initProject(input);
|
|
249
|
-
}
|
|
250
|
-
case 'task.append_event': {
|
|
251
|
-
const r = ops.appendEvent(db, {
|
|
252
|
-
type: input.type,
|
|
253
|
-
task_id: input.task_id,
|
|
254
|
-
change_id: input.change_id,
|
|
255
|
-
session_id: input.session_id,
|
|
256
|
-
runtime: input.runtime,
|
|
257
|
-
payload: input.payload,
|
|
258
|
-
});
|
|
259
|
-
return { event_id: r.event_id, ts: r.ts };
|
|
260
|
-
}
|
|
261
|
-
case 'task.subscribe_events': {
|
|
262
|
-
return ops.subscribeEventsSince(db, input || {});
|
|
263
|
-
}
|
|
264
|
-
case 'session.spawn': {
|
|
265
|
-
if (ctx.sessionId) {
|
|
266
|
-
const error = new Error(
|
|
267
|
-
`worker session ${ctx.sessionId} already owns an isolated worktree; `
|
|
268
|
-
+ 'resume that session instead of recursively spawning another one',
|
|
269
|
-
);
|
|
270
|
-
error.code = 'NESTED_SESSION_FORBIDDEN';
|
|
271
|
-
throw error;
|
|
272
|
-
}
|
|
273
|
-
const rootDir = ctx.rootDir || process.cwd();
|
|
274
|
-
const handle = sessionRunner.spawnSession({
|
|
275
|
-
db,
|
|
276
|
-
repoRoot: rootDir,
|
|
277
|
-
task_id: input.task_id,
|
|
278
|
-
runtime: input.runtime,
|
|
279
|
-
takeover: Boolean(input.takeover),
|
|
280
|
-
worktree_base: input.worktree_base,
|
|
281
|
-
});
|
|
282
|
-
return {
|
|
283
|
-
sid: handle.sid,
|
|
284
|
-
worktree_path: handle.worktree_path,
|
|
285
|
-
artifact_dir: handle.artifact_dir,
|
|
286
|
-
lease_expires_at: handle.lease_expires_at,
|
|
287
|
-
worktree_created: true,
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
case 'session.close': {
|
|
291
|
-
if (ctx.sessionId) {
|
|
292
|
-
const error = new Error(
|
|
293
|
-
ctx.sessionId === input.sid
|
|
294
|
-
? `worker session ${ctx.sessionId} is supervised by its parent; save evidence and exit so the parent can settle the lease`
|
|
295
|
-
: `worker session ${ctx.sessionId} cannot close unrelated session ${input.sid}`,
|
|
296
|
-
);
|
|
297
|
-
error.code = ctx.sessionId === input.sid
|
|
298
|
-
? 'WORKER_SESSION_PARENT_OWNED'
|
|
299
|
-
: 'SESSION_SCOPE_VIOLATION';
|
|
300
|
-
throw error;
|
|
301
|
-
}
|
|
302
|
-
const closed = sessionRunner.closeSession(
|
|
303
|
-
{ db, repoRoot: ctx.rootDir || process.cwd(), sid: input.sid },
|
|
304
|
-
{ status: input.status, remove_worktree: input.remove_worktree === true },
|
|
305
|
-
);
|
|
306
|
-
return {
|
|
307
|
-
ok: true,
|
|
308
|
-
worktree_preserved: closed.worktree_preserved,
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
case 'session.get': {
|
|
312
|
-
const session = ops.readSession(db, input.sid);
|
|
313
|
-
if (!session) {
|
|
314
|
-
const err = new Error(`session ${input.sid} not found`);
|
|
315
|
-
err.code = 'SESSION_NOT_FOUND';
|
|
316
|
-
throw err;
|
|
317
|
-
}
|
|
318
|
-
return { session };
|
|
319
|
-
}
|
|
320
|
-
case 'session.admission_check': {
|
|
321
|
-
return sessionRunner.admissionCheck(
|
|
322
|
-
db,
|
|
323
|
-
ctx.rootDir || process.cwd(),
|
|
324
|
-
input.task_id,
|
|
325
|
-
);
|
|
326
|
-
}
|
|
327
|
-
case 'session.list': {
|
|
328
|
-
const sessions = ops.listActiveSessions(db, { task_id: input.task_id });
|
|
329
|
-
const status = input.status || 'running';
|
|
330
|
-
const filtered = status === 'running'
|
|
331
|
-
? sessions
|
|
332
|
-
: db.prepare(
|
|
333
|
-
"SELECT * FROM sessions WHERE status = ? AND (? IS NULL OR task_id = ?) ORDER BY started_at ASC LIMIT ?",
|
|
334
|
-
).all(status, input.task_id || null, input.task_id || null, Math.min(input.limit || 100, 500));
|
|
335
|
-
const limit = Math.min(input.limit || 100, 500);
|
|
336
|
-
const trimmed = filtered.slice(0, limit);
|
|
337
|
-
return { sessions: trimmed, count: trimmed.length };
|
|
338
|
-
}
|
|
339
|
-
case 'session.heartbeat': {
|
|
340
|
-
return ops.heartbeatSession(db, input.sid);
|
|
341
|
-
}
|
|
342
|
-
case 'session.subscribe_events': {
|
|
343
|
-
return ops.subscribeEventsSince(db, {
|
|
344
|
-
since_id: input.since_id,
|
|
345
|
-
session_id: input.sid,
|
|
346
|
-
limit: input.limit,
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
case 'task.switch_tag': {
|
|
350
|
-
return ops.switchTaskTag(db, input.id, input.tag);
|
|
351
|
-
}
|
|
352
|
-
case 'task.dependency_topo': {
|
|
353
|
-
const hasTaskIds = Array.isArray(input && input.task_ids);
|
|
354
|
-
const hasChangeId = typeof input?.change_id === 'string' && input.change_id.trim() !== '';
|
|
355
|
-
if (hasTaskIds === hasChangeId) {
|
|
356
|
-
const err = new Error('provide exactly one of task_ids or change_id');
|
|
357
|
-
err.code = 'VALIDATION_ERROR';
|
|
358
|
-
throw err;
|
|
359
|
-
}
|
|
360
|
-
let rows;
|
|
361
|
-
if (hasTaskIds) {
|
|
362
|
-
rows = input.task_ids.map((id) => {
|
|
363
|
-
const task = ops.readTask(db, id);
|
|
364
|
-
if (!task) {
|
|
365
|
-
const err = new Error(`task ${id} not found`);
|
|
366
|
-
err.code = 'TASK_NOT_FOUND';
|
|
367
|
-
throw err;
|
|
368
|
-
}
|
|
369
|
-
return task;
|
|
370
|
-
});
|
|
371
|
-
} else {
|
|
372
|
-
const change = changes.readChange(db, input.change_id);
|
|
373
|
-
if (!change) {
|
|
374
|
-
const err = new Error(`change ${input.change_id} not found`);
|
|
375
|
-
err.code = 'CHANGE_NOT_FOUND';
|
|
376
|
-
throw err;
|
|
377
|
-
}
|
|
378
|
-
rows = ops.listTasks(db, { change_id: change.id });
|
|
379
|
-
}
|
|
380
|
-
const graph = rows.map((t) => ({
|
|
381
|
-
id: t.id,
|
|
382
|
-
deps: Array.isArray(t.deps) ? t.deps : [],
|
|
383
|
-
}));
|
|
384
|
-
const result = topo.computeWaves(graph);
|
|
385
|
-
if (result.cycles.length > 0) {
|
|
386
|
-
const err = new Error(`dependency graph has ${result.cycles.length} cycle(s)`);
|
|
387
|
-
err.code = 'CYCLE_DETECTED';
|
|
388
|
-
err.details = { cycles: result.cycles };
|
|
389
|
-
throw err;
|
|
390
|
-
}
|
|
391
|
-
return { waves: result.waves };
|
|
392
|
-
}
|
|
393
|
-
case 'task.parse_prd': {
|
|
394
|
-
const dryRun = input.dry_run === true;
|
|
395
|
-
const rootDir = ctx.rootDir || process.cwd();
|
|
396
|
-
if (!dryRun && !String(input.change_id || '').trim()) {
|
|
397
|
-
const err = new Error('persisting a parsed task graph requires change_id');
|
|
398
|
-
err.code = 'CHANGE_REQUIRED';
|
|
399
|
-
throw err;
|
|
400
|
-
}
|
|
401
|
-
const creationAuthority = { change_id: input.change_id };
|
|
402
|
-
if (!dryRun) changes.assertTaskCreationAllowed(db, creationAuthority, { rootDir });
|
|
403
|
-
const parsed = parser.parsePrd(input.tasks, {
|
|
404
|
-
tag: input.tag,
|
|
405
|
-
changeId: input.change_id,
|
|
406
|
-
});
|
|
407
|
-
const shaped = parsed.tasks.map((t) => ({
|
|
408
|
-
id: t.id, title: t.title, type: t.type, priority: t.priority,
|
|
409
|
-
complexity: t.complexity, deps: t.deps, files_modified: t.files_modified,
|
|
410
|
-
tag: t.tag, change_id: t.change_id,
|
|
411
|
-
}));
|
|
412
|
-
const graph = shaped.map((t) => ({ id: t.id, deps: t.deps || [] }));
|
|
413
|
-
const topoResult = topo.computeWaves(graph);
|
|
414
|
-
if (topoResult.cycles.length > 0) {
|
|
415
|
-
const err = new Error(`parsed task graph has ${topoResult.cycles.length} cycle(s)`);
|
|
416
|
-
err.code = 'CYCLE_DETECTED';
|
|
417
|
-
err.details = { cycles: topoResult.cycles };
|
|
418
|
-
throw err;
|
|
419
|
-
}
|
|
420
|
-
if (!dryRun) {
|
|
421
|
-
try {
|
|
422
|
-
ops.tx(db, () => {
|
|
423
|
-
changes.assertTaskCreationAllowed(db, creationAuthority, { rootDir });
|
|
424
|
-
for (const t of shaped) ops.createTask(db, t);
|
|
425
|
-
});
|
|
426
|
-
} catch (err) {
|
|
427
|
-
if (['BASELINE_NOT_READY', 'CHANGE_NOT_FOUND', 'CHANGE_NOT_MUTABLE'].includes(err.code)) throw err;
|
|
428
|
-
const wrap = new Error(`failed to persist parsed tasks: ${err.message}`);
|
|
429
|
-
wrap.code = 'PARSE_FAILED';
|
|
430
|
-
wrap.cause = err;
|
|
431
|
-
throw wrap;
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
return { tasks: shaped, topo: topoResult.waves };
|
|
435
|
-
}
|
|
436
|
-
case 'task.expand': {
|
|
437
|
-
const parent = ops.readTask(db, input.id);
|
|
438
|
-
if (!parent) {
|
|
439
|
-
const err = new Error(`no task ${input.id}`);
|
|
440
|
-
err.code = 'TASK_NOT_FOUND';
|
|
441
|
-
throw err;
|
|
442
|
-
}
|
|
443
|
-
if (parent.status === 'expanded') {
|
|
444
|
-
const err = new Error(`task ${input.id} is already expanded`);
|
|
445
|
-
err.code = 'ALREADY_EXPANDED';
|
|
446
|
-
throw err;
|
|
447
|
-
}
|
|
448
|
-
const rootDir = ctx.rootDir || process.cwd();
|
|
449
|
-
changes.assertTaskCreationAllowed(db, { change_id: parent.change_id }, { rootDir });
|
|
450
|
-
const result = expander.expandTask(db, {
|
|
451
|
-
id: input.id,
|
|
452
|
-
children: input.children,
|
|
453
|
-
rootDir,
|
|
454
|
-
});
|
|
455
|
-
taskLedger.publishTaskLedger(db, {
|
|
456
|
-
rootDir,
|
|
457
|
-
reason: 'task_expanded',
|
|
458
|
-
});
|
|
459
|
-
return { parent_id: result.parent_id, children: result.children };
|
|
460
|
-
}
|
|
461
|
-
case 'task.ledger_get': {
|
|
462
|
-
const rootDir = ctx.rootDir || process.cwd();
|
|
463
|
-
return {
|
|
464
|
-
ledger: taskLedger.readTaskLedger(rootDir),
|
|
465
|
-
health: taskLedger.inspectTaskLedger(db, { rootDir }),
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
case 'task.ledger_publish': {
|
|
469
|
-
const result = taskLedger.publishTaskLedger(db, {
|
|
470
|
-
rootDir: ctx.rootDir || process.cwd(),
|
|
471
|
-
reason: input.reason || 'manual_checkpoint',
|
|
472
|
-
});
|
|
473
|
-
const output = {
|
|
474
|
-
changed: result.changed,
|
|
475
|
-
path: path.relative(ctx.rootDir || process.cwd(), result.path),
|
|
476
|
-
generation: result.ledger.generation,
|
|
477
|
-
state_digest: result.ledger.state_digest,
|
|
478
|
-
};
|
|
479
|
-
if (result.migrated_legacy_projection) {
|
|
480
|
-
output.migrated_legacy_projection = true;
|
|
481
|
-
output.legacy_backup_path = path.relative(
|
|
482
|
-
ctx.rootDir || process.cwd(),
|
|
483
|
-
result.legacy_backup_path,
|
|
484
|
-
);
|
|
485
|
-
}
|
|
486
|
-
return output;
|
|
487
|
-
}
|
|
488
|
-
case 'task.ledger_import': {
|
|
489
|
-
const result = taskLedger.importTaskLedger(db, {
|
|
490
|
-
rootDir: ctx.rootDir || process.cwd(),
|
|
491
|
-
});
|
|
492
|
-
return {
|
|
493
|
-
imported: result.imported,
|
|
494
|
-
preserved: result.preserved,
|
|
495
|
-
deleted: result.deleted,
|
|
496
|
-
imported_changes: result.imported_changes,
|
|
497
|
-
preserved_changes: result.preserved_changes,
|
|
498
|
-
imported_baseline: result.imported_baseline,
|
|
499
|
-
preserved_baseline: result.preserved_baseline,
|
|
500
|
-
generation: result.generation,
|
|
501
|
-
state_digest: result.state_digest,
|
|
502
|
-
requires_plan_revalidation: result.requires_plan_revalidation,
|
|
503
|
-
requires_baseline_revalidation: result.requires_baseline_revalidation,
|
|
504
|
-
already_current: result.already_current,
|
|
505
|
-
};
|
|
506
|
-
}
|
|
507
|
-
case 'plan.export': {
|
|
508
|
-
const rootDir = ctx.rootDir || process.cwd();
|
|
509
|
-
const change = changes.readChange(db, input.change_id);
|
|
510
|
-
if (!change) {
|
|
511
|
-
const err = new Error(`change ${input.change_id} not found`);
|
|
512
|
-
err.code = 'CHANGE_NOT_FOUND';
|
|
513
|
-
throw err;
|
|
514
|
-
}
|
|
515
|
-
if (!['active', 'blocked'].includes(change.status)) {
|
|
516
|
-
const err = new Error(`change ${change.id} is ${change.status}`);
|
|
517
|
-
err.code = 'CHANGE_NOT_MUTABLE';
|
|
518
|
-
throw err;
|
|
519
|
-
}
|
|
520
|
-
const tasks = ops.listTasks(db, { change_id: change.id });
|
|
521
|
-
if (tasks.length === 0) {
|
|
522
|
-
const err = new Error('no tasks to plan');
|
|
523
|
-
err.code = 'NO_TASKS';
|
|
524
|
-
throw err;
|
|
525
|
-
}
|
|
526
|
-
const context = contextSpine.validateContextSnapshot(db, {
|
|
527
|
-
change_id: change.id,
|
|
528
|
-
task_id: null,
|
|
529
|
-
role: 'plan',
|
|
530
|
-
gate: 'planning',
|
|
531
|
-
}, { rootDir });
|
|
532
|
-
if (!context.snapshot || context.blockers.length > 0) {
|
|
533
|
-
const err = new Error('plan export requires a current planning Context Manifest');
|
|
534
|
-
err.code = context.snapshot ? 'PLAN_CONTEXT_STALE' : 'PLAN_CONTEXT_REQUIRED';
|
|
535
|
-
err.details = { blockers: context.blockers };
|
|
536
|
-
throw err;
|
|
537
|
-
}
|
|
538
|
-
const plan = planStore.buildPlan(tasks, { changeId: change.id });
|
|
539
|
-
const planRun = db.prepare(
|
|
540
|
-
`SELECT id FROM workflow_runs
|
|
541
|
-
WHERE kind = 'plan' AND change_id = ? AND task_id IS NULL
|
|
542
|
-
AND status IN ('active', 'blocked', 'ready')
|
|
543
|
-
ORDER BY started_at DESC, rowid DESC LIMIT 1`,
|
|
544
|
-
).get(change.id);
|
|
545
|
-
const owner = { type: 'change', id: change.id };
|
|
546
|
-
const contextArtifact = db.prepare(
|
|
547
|
-
`SELECT id FROM artifacts
|
|
548
|
-
WHERE path = ? AND status <> 'archived'
|
|
549
|
-
ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
|
|
550
|
-
).get(context.snapshot.manifest_path);
|
|
551
|
-
const publication = planStore.prepareChangePlanPublication(plan, {
|
|
552
|
-
rootDir,
|
|
553
|
-
change,
|
|
554
|
-
tasks,
|
|
555
|
-
context: {
|
|
556
|
-
snapshot_id: context.snapshot.id,
|
|
557
|
-
manifest_path: context.snapshot.manifest_path,
|
|
558
|
-
manifest_digest: context.snapshot.manifest_hash,
|
|
559
|
-
},
|
|
560
|
-
});
|
|
561
|
-
const artifactInputs = publication.artifacts.map(({ kind, path: file, digest }) => ({
|
|
562
|
-
kind,
|
|
563
|
-
file,
|
|
564
|
-
digest,
|
|
565
|
-
input: {
|
|
566
|
-
owner_type: owner.type,
|
|
567
|
-
owner_id: owner.id,
|
|
568
|
-
kind,
|
|
569
|
-
path: path.relative(rootDir, file),
|
|
570
|
-
source_refs: [
|
|
571
|
-
{ type: 'change', id: change.id, relation: 'planned_for' },
|
|
572
|
-
...(contextArtifact
|
|
573
|
-
? [{ type: 'artifact', id: contextArtifact.id, relation: 'compiled_from' }]
|
|
574
|
-
: []),
|
|
575
|
-
...tasks.map((task) => ({
|
|
576
|
-
type: 'task', id: task.id, relation: 'compiled_from_task_contract',
|
|
577
|
-
})),
|
|
578
|
-
],
|
|
579
|
-
consumer_refs: planRun
|
|
580
|
-
? [{ type: 'workflow', id: planRun.id, relation: 'verified_by' }]
|
|
581
|
-
: [],
|
|
582
|
-
provenance: {
|
|
583
|
-
writer: 'plan.export',
|
|
584
|
-
workflow_run_id: planRun?.id || null,
|
|
585
|
-
publication_transaction_id: publication.transaction_id,
|
|
586
|
-
},
|
|
587
|
-
metadata: {
|
|
588
|
-
context_snapshot_id: context.snapshot.id,
|
|
589
|
-
context_digest: context.snapshot.manifest_hash,
|
|
590
|
-
terminal_role: !planRun,
|
|
591
|
-
},
|
|
592
|
-
},
|
|
593
|
-
}));
|
|
594
|
-
try {
|
|
595
|
-
ops.tx(db, () => {
|
|
596
|
-
const preflight = artifactInputs.map((artifact) => (
|
|
597
|
-
artifactRegistry.preflightArtifactPublication(
|
|
598
|
-
db, artifact.input, { rootDir },
|
|
599
|
-
)
|
|
600
|
-
));
|
|
601
|
-
publication.publish();
|
|
602
|
-
const recorded = artifactInputs.map((artifact, index) => (
|
|
603
|
-
artifactRegistry.recordArtifactInTx(db, {
|
|
604
|
-
...artifact.input,
|
|
605
|
-
id: preflight[index].artifact_id || undefined,
|
|
606
|
-
expected_before_digest: preflight[index].expected_before_digest,
|
|
607
|
-
content_digest: artifact.digest,
|
|
608
|
-
}, { rootDir })
|
|
609
|
-
));
|
|
610
|
-
ops.appendEventInTx(db, {
|
|
611
|
-
type: 'plan_exported',
|
|
612
|
-
change_id: change.id,
|
|
613
|
-
payload: {
|
|
614
|
-
change_id: change.id,
|
|
615
|
-
plan_path: publication.plan_path,
|
|
616
|
-
plan_md_path: publication.plan_md_path,
|
|
617
|
-
publication_transaction_id: publication.transaction_id,
|
|
618
|
-
wave_count: plan.waves.length,
|
|
619
|
-
},
|
|
620
|
-
});
|
|
621
|
-
return recorded;
|
|
622
|
-
});
|
|
623
|
-
} catch (error) {
|
|
624
|
-
const rollback = publication.rollback();
|
|
625
|
-
const failure = rollback?.rolled_back === false
|
|
626
|
-
? planStore.planRecoveryRequiredError(
|
|
627
|
-
error,
|
|
628
|
-
rollback,
|
|
629
|
-
publication.transaction_id,
|
|
630
|
-
)
|
|
631
|
-
: error;
|
|
632
|
-
if (failure.code === 'PLAN_RECOVERY_REQUIRED') {
|
|
633
|
-
const issue = failure.details?.rollback_issue
|
|
634
|
-
|| failure.details?.issue
|
|
635
|
-
|| {
|
|
636
|
-
code: failure.cause?.code || 'PLAN_RECOVERY_FAILED',
|
|
637
|
-
message: failure.cause?.message || failure.message,
|
|
638
|
-
};
|
|
639
|
-
ctx.markPlanRecoveryRequired?.({
|
|
640
|
-
recovered: 0,
|
|
641
|
-
finalized: 0,
|
|
642
|
-
pending: 1,
|
|
643
|
-
issues: [issue],
|
|
644
|
-
});
|
|
645
|
-
}
|
|
646
|
-
throw failure;
|
|
647
|
-
}
|
|
648
|
-
try {
|
|
649
|
-
publication.commit();
|
|
650
|
-
} catch (error) {
|
|
651
|
-
const issue = error.details?.issue || {
|
|
652
|
-
code: error.cause?.code || 'PLAN_RECOVERY_FAILED',
|
|
653
|
-
message: error.cause?.message || error.message,
|
|
654
|
-
};
|
|
655
|
-
ctx.markPlanRecoveryRequired?.({
|
|
656
|
-
recovered: 0,
|
|
657
|
-
finalized: 0,
|
|
658
|
-
pending: 1,
|
|
659
|
-
issues: [issue],
|
|
660
|
-
});
|
|
661
|
-
// The registry and target bytes committed atomically before cleanup.
|
|
662
|
-
// The remaining journal is still authoritative recovery work, so the
|
|
663
|
-
// caller must see a typed failure and later mutations must stop.
|
|
664
|
-
throw error;
|
|
665
|
-
}
|
|
666
|
-
return {
|
|
667
|
-
plan_path: publication.plan_path,
|
|
668
|
-
plan_md_path: publication.plan_md_path,
|
|
669
|
-
wave_count: plan.waves.length,
|
|
670
|
-
};
|
|
671
|
-
}
|
|
672
|
-
case 'plan.get': {
|
|
673
|
-
const rootDir = ctx.rootDir || process.cwd();
|
|
674
|
-
const change = changes.readChange(db, input.change_id);
|
|
675
|
-
if (!change) {
|
|
676
|
-
const err = new Error(`change ${input.change_id} not found`);
|
|
677
|
-
err.code = 'CHANGE_NOT_FOUND';
|
|
678
|
-
throw err;
|
|
679
|
-
}
|
|
680
|
-
const loaded = planStore.loadChangePlanArtifact(rootDir, change, {
|
|
681
|
-
db,
|
|
682
|
-
strict: true,
|
|
683
|
-
});
|
|
684
|
-
if (!loaded) {
|
|
685
|
-
const err = new Error('no plan has been computed yet');
|
|
686
|
-
err.code = 'NO_PLAN';
|
|
687
|
-
throw err;
|
|
688
|
-
}
|
|
689
|
-
return { plan: planStore.selectSection(loaded, input.section) };
|
|
690
|
-
}
|
|
691
|
-
case 'baseline.start': {
|
|
692
|
-
return {
|
|
693
|
-
baseline: baselines.startBaseline(
|
|
694
|
-
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
695
|
-
),
|
|
696
|
-
};
|
|
697
|
-
}
|
|
698
|
-
case 'baseline.record': {
|
|
699
|
-
return {
|
|
700
|
-
baseline: baselines.recordBaseline(
|
|
701
|
-
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
702
|
-
),
|
|
703
|
-
};
|
|
704
|
-
}
|
|
705
|
-
case 'baseline.get': {
|
|
706
|
-
const baseline = baselines.readBaseline(db, input.id);
|
|
707
|
-
if (!baseline) {
|
|
708
|
-
const err = new Error(input.id ? `baseline ${input.id} not found` : 'current baseline not found');
|
|
709
|
-
err.code = 'BASELINE_NOT_FOUND';
|
|
710
|
-
throw err;
|
|
711
|
-
}
|
|
712
|
-
return {
|
|
713
|
-
baseline,
|
|
714
|
-
health: baselines.inspectBaseline(db, {
|
|
715
|
-
rootDir: ctx.rootDir || process.cwd(), id: input.id,
|
|
716
|
-
}),
|
|
717
|
-
};
|
|
718
|
-
}
|
|
719
|
-
case 'baseline.converge': {
|
|
720
|
-
const result = baselines.convergeBaseline(
|
|
721
|
-
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
722
|
-
);
|
|
723
|
-
if (result.ready) {
|
|
724
|
-
taskLedger.publishTaskLedger(db, {
|
|
725
|
-
rootDir: ctx.rootDir || process.cwd(),
|
|
726
|
-
reason: 'baseline_converged',
|
|
727
|
-
});
|
|
728
|
-
}
|
|
729
|
-
return result;
|
|
730
|
-
}
|
|
731
|
-
case 'change.create': {
|
|
732
|
-
const { randomUUID } = require('node:crypto');
|
|
733
|
-
const id = input.id || `chg-${randomUUID().slice(0, 12)}`;
|
|
734
|
-
const result = changes.createChange(
|
|
735
|
-
db,
|
|
736
|
-
{ ...input, id },
|
|
737
|
-
{ rootDir: ctx.rootDir || process.cwd() },
|
|
738
|
-
);
|
|
739
|
-
taskLedger.publishTaskLedger(db, {
|
|
740
|
-
rootDir: ctx.rootDir || process.cwd(),
|
|
741
|
-
reason: 'change_created',
|
|
742
|
-
});
|
|
743
|
-
return result;
|
|
744
|
-
}
|
|
745
|
-
case 'change.update': {
|
|
746
|
-
const change = changes.updateChange(
|
|
747
|
-
db, input.id, input.patch || {}, { rootDir: ctx.rootDir || process.cwd() },
|
|
748
|
-
);
|
|
749
|
-
taskLedger.publishTaskLedger(db, {
|
|
750
|
-
rootDir: ctx.rootDir || process.cwd(),
|
|
751
|
-
reason: 'change_updated',
|
|
752
|
-
});
|
|
753
|
-
return {
|
|
754
|
-
ok: true,
|
|
755
|
-
change,
|
|
756
|
-
};
|
|
757
|
-
}
|
|
758
|
-
case 'change.get': {
|
|
759
|
-
const change = changes.readChange(db, input.id);
|
|
760
|
-
if (!change) {
|
|
761
|
-
const err = new Error(`change ${input.id} not found`);
|
|
762
|
-
err.code = 'CHANGE_NOT_FOUND';
|
|
763
|
-
throw err;
|
|
764
|
-
}
|
|
765
|
-
return {
|
|
766
|
-
change: {
|
|
767
|
-
...change,
|
|
768
|
-
learning_candidates: changes.listSpecLearning(db, change.id),
|
|
769
|
-
},
|
|
770
|
-
};
|
|
771
|
-
}
|
|
772
|
-
case 'change.list': {
|
|
773
|
-
const rows = changes.listChanges(db, input || {});
|
|
774
|
-
return { changes: rows, count: rows.length };
|
|
775
|
-
}
|
|
776
|
-
case 'change.context': {
|
|
777
|
-
const out = changes.compileContext(db, input, { rootDir: ctx.rootDir || process.cwd() });
|
|
778
|
-
return {
|
|
779
|
-
manifest_path: out.context_manifest_path,
|
|
780
|
-
manifest_hash: out.manifest_hash,
|
|
781
|
-
manifest: out.manifest,
|
|
782
|
-
};
|
|
783
|
-
}
|
|
784
|
-
case 'change.breadcrumb': {
|
|
785
|
-
return {
|
|
786
|
-
breadcrumb: changes.readBreadcrumb(
|
|
787
|
-
db, input || {}, { rootDir: ctx.rootDir || process.cwd() },
|
|
788
|
-
),
|
|
789
|
-
};
|
|
790
|
-
}
|
|
791
|
-
case 'change.learning_propose': {
|
|
792
|
-
return {
|
|
793
|
-
candidate: changes.proposeSpecLearning(
|
|
794
|
-
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
795
|
-
),
|
|
796
|
-
};
|
|
797
|
-
}
|
|
798
|
-
case 'change.learning_resolve': {
|
|
799
|
-
return {
|
|
800
|
-
candidate: changes.resolveSpecLearning(
|
|
801
|
-
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
802
|
-
),
|
|
803
|
-
};
|
|
804
|
-
}
|
|
805
|
-
case 'change.converge': {
|
|
806
|
-
const result = changes.convergeChange(
|
|
807
|
-
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
808
|
-
);
|
|
809
|
-
taskLedger.publishTaskLedger(db, {
|
|
810
|
-
rootDir: ctx.rootDir || process.cwd(),
|
|
811
|
-
reason: 'change_converged',
|
|
812
|
-
});
|
|
813
|
-
return result;
|
|
814
|
-
}
|
|
815
|
-
case 'change.archive': {
|
|
816
|
-
const result = changes.archiveChange(
|
|
817
|
-
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
818
|
-
);
|
|
819
|
-
taskLedger.publishTaskLedger(db, {
|
|
820
|
-
rootDir: ctx.rootDir || process.cwd(),
|
|
821
|
-
reason: 'change_archived',
|
|
822
|
-
});
|
|
823
|
-
return result;
|
|
824
|
-
}
|
|
825
|
-
case 'change.delta': {
|
|
826
|
-
return changes.recordDelta(db, input, { rootDir: ctx.rootDir || process.cwd() });
|
|
827
|
-
}
|
|
828
|
-
case 'change.documentation_reconcile': {
|
|
829
|
-
return changes.recordDocumentationReconciliation(
|
|
830
|
-
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
831
|
-
);
|
|
832
|
-
}
|
|
833
|
-
case 'decision.thread_start': {
|
|
834
|
-
return { thread: decisions.startDecisionThread(db, input) };
|
|
835
|
-
}
|
|
836
|
-
case 'decision.get': {
|
|
837
|
-
const thread = decisions.readDecisionThread(db, input.id);
|
|
838
|
-
if (!thread) {
|
|
839
|
-
const error = new Error(`decision thread ${input.id} not found`);
|
|
840
|
-
error.code = 'DECISION_THREAD_NOT_FOUND';
|
|
841
|
-
throw error;
|
|
842
|
-
}
|
|
843
|
-
return { thread };
|
|
844
|
-
}
|
|
845
|
-
case 'decision.list': {
|
|
846
|
-
const threads = decisions.listDecisionThreads(db, input || {});
|
|
847
|
-
return { threads, count: threads.length };
|
|
848
|
-
}
|
|
849
|
-
case 'decision.open': {
|
|
850
|
-
return { thread: decisions.openDecision(db, input) };
|
|
851
|
-
}
|
|
852
|
-
case 'decision.resolve': {
|
|
853
|
-
return { thread: decisions.resolveDecision(db, input) };
|
|
854
|
-
}
|
|
855
|
-
case 'decision.delegate': {
|
|
856
|
-
return { thread: decisions.delegateDecision(db, input) };
|
|
857
|
-
}
|
|
858
|
-
case 'decision.defer': {
|
|
859
|
-
return { thread: decisions.deferDecision(db, input) };
|
|
860
|
-
}
|
|
861
|
-
case 'decision.supersede': {
|
|
862
|
-
return { thread: decisions.supersedeDecision(db, input) };
|
|
863
|
-
}
|
|
864
|
-
case 'decision.complete': {
|
|
865
|
-
return {
|
|
866
|
-
thread: decisions.completeDecisionThread(
|
|
867
|
-
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
868
|
-
),
|
|
869
|
-
};
|
|
870
|
-
}
|
|
871
|
-
case 'decision.checkpoint': {
|
|
872
|
-
return {
|
|
873
|
-
thread: decisions.checkpointDecisionThread(
|
|
874
|
-
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
875
|
-
),
|
|
876
|
-
};
|
|
877
|
-
}
|
|
878
|
-
case 'workflow.start': {
|
|
879
|
-
return {
|
|
880
|
-
workflow: workflows.startWorkflow(
|
|
881
|
-
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
882
|
-
),
|
|
883
|
-
};
|
|
884
|
-
}
|
|
885
|
-
case 'workflow.get': {
|
|
886
|
-
const workflow = workflows.readWorkflow(
|
|
887
|
-
db, input.id, { rootDir: ctx.rootDir || process.cwd() },
|
|
888
|
-
);
|
|
889
|
-
if (!workflow) {
|
|
890
|
-
const error = new Error(`workflow ${input.id} not found`);
|
|
891
|
-
error.code = 'WORKFLOW_NOT_FOUND';
|
|
892
|
-
throw error;
|
|
893
|
-
}
|
|
894
|
-
return { workflow };
|
|
895
|
-
}
|
|
896
|
-
case 'workflow.list': {
|
|
897
|
-
const rows = workflows.listWorkflows(
|
|
898
|
-
db, input || {}, { rootDir: ctx.rootDir || process.cwd() },
|
|
899
|
-
);
|
|
900
|
-
return { workflows: rows, count: rows.length };
|
|
901
|
-
}
|
|
902
|
-
case 'workflow.step': {
|
|
903
|
-
return {
|
|
904
|
-
workflow: workflows.recordWorkflowStep(
|
|
905
|
-
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
906
|
-
),
|
|
907
|
-
};
|
|
908
|
-
}
|
|
909
|
-
case 'workflow.revise': {
|
|
910
|
-
return {
|
|
911
|
-
workflow: workflows.reviseWorkflow(
|
|
912
|
-
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
913
|
-
),
|
|
914
|
-
};
|
|
915
|
-
}
|
|
916
|
-
case 'workflow.supersede': {
|
|
917
|
-
return workflows.supersedeWorkflow(
|
|
918
|
-
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
919
|
-
);
|
|
920
|
-
}
|
|
921
|
-
case 'workflow.complete': {
|
|
922
|
-
return {
|
|
923
|
-
workflow: workflows.completeWorkflow(
|
|
924
|
-
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
925
|
-
),
|
|
926
|
-
};
|
|
927
|
-
}
|
|
928
|
-
case 'artifact.record': {
|
|
929
|
-
return artifactRegistry.recordArtifact(
|
|
930
|
-
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
931
|
-
);
|
|
932
|
-
}
|
|
933
|
-
case 'artifact.get': {
|
|
934
|
-
return { artifact: artifactRegistry.getArtifact(db, input) };
|
|
935
|
-
}
|
|
936
|
-
case 'system.doctor': {
|
|
937
|
-
return doctor.runDoctor(db, {
|
|
938
|
-
rootDir: ctx.rootDir || process.cwd(),
|
|
939
|
-
repair: input.repair === true,
|
|
940
|
-
project: ctx.projector || projector.projectAll,
|
|
941
|
-
});
|
|
942
|
-
}
|
|
943
|
-
default:
|
|
944
|
-
throw new Error(`unhandled tool ${name}`);
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
|
|
948
59
|
function errorResponse(code, message, retriable = false, details = undefined) {
|
|
949
60
|
const error = { code, message, retriable };
|
|
950
61
|
if (details !== undefined) error.details = details;
|
|
@@ -957,16 +68,48 @@ function errorResponse(code, message, retriable = false, details = undefined) {
|
|
|
957
68
|
};
|
|
958
69
|
}
|
|
959
70
|
|
|
71
|
+
function isInitializeCall(name, input = {}) {
|
|
72
|
+
return name === 'ultra.record'
|
|
73
|
+
&& Array.isArray(input.entries)
|
|
74
|
+
&& input.entries.length === 1
|
|
75
|
+
&& input.entries[0]?.kind === 'baseline'
|
|
76
|
+
&& input.entries[0]?.action === 'initialize';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isMutation(name, input = {}) {
|
|
80
|
+
if (name === 'ultra.record' || name === 'ultra.checkpoint' || name === 'ultra.archive') {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
if (name === 'ultra.sync') return input.action !== 'inspect';
|
|
84
|
+
if (name === 'ultra.session') {
|
|
85
|
+
return ['acquire', 'heartbeat', 'release'].includes(input.action);
|
|
86
|
+
}
|
|
87
|
+
return name === 'ultra.doctor' && input.repair === true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function requiresTeamSync(name, input = {}) {
|
|
91
|
+
return name === 'ultra.record'
|
|
92
|
+
|| name === 'ultra.checkpoint'
|
|
93
|
+
|| name === 'ultra.archive'
|
|
94
|
+
|| (name === 'ultra.session' && input.action === 'acquire');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function ownsTeamAuthorityInspection(name) {
|
|
98
|
+
return ['ultra.context', 'ultra.sync', 'ultra.doctor'].includes(name);
|
|
99
|
+
}
|
|
100
|
+
|
|
960
101
|
function startServer({
|
|
961
102
|
dbPath,
|
|
962
103
|
rootDir,
|
|
963
104
|
sessionId = null,
|
|
964
105
|
projectOnWrite = true,
|
|
965
106
|
project = projector.projectAll,
|
|
107
|
+
runtime = process.env.UBP_RUNTIME || 'unknown',
|
|
966
108
|
}) {
|
|
967
109
|
let db = null;
|
|
968
|
-
let planRecovery = null;
|
|
969
110
|
let authorityDbPath = path.resolve(dbPath);
|
|
111
|
+
let planRecovery = null;
|
|
112
|
+
|
|
970
113
|
const markPlanRecoveryRequired = (details) => {
|
|
971
114
|
planRecovery = {
|
|
972
115
|
recovered: Number(details?.recovered || 0),
|
|
@@ -975,33 +118,31 @@ function startServer({
|
|
|
975
118
|
issues: Array.isArray(details?.issues) ? details.issues : [],
|
|
976
119
|
};
|
|
977
120
|
};
|
|
978
|
-
|
|
121
|
+
|
|
122
|
+
const getDb = (toolName, input = {}) => {
|
|
979
123
|
if (!db) {
|
|
980
|
-
if (toolName === 'ultra.context') {
|
|
981
|
-
if (!fs.existsSync(authorityDbPath)) {
|
|
982
|
-
const error = new Error(
|
|
983
|
-
'Ultra authority is not initialized; invoke ultra-init before reading project context',
|
|
984
|
-
);
|
|
985
|
-
error.code = 'STATE_DB_MISSING';
|
|
986
|
-
throw error;
|
|
987
|
-
}
|
|
988
|
-
db = openStateDb(authorityDbPath);
|
|
989
|
-
ensureSchemaVersion(db);
|
|
990
|
-
assertStateAuthority(db, rootDir, { importTeamLedger: false });
|
|
991
|
-
return db;
|
|
992
|
-
}
|
|
993
124
|
const projectPaths = runtimePaths.pathsFor(rootDir);
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
125
|
+
const usesProjectAuthority = [
|
|
126
|
+
projectPaths.legacyStateDbPath,
|
|
127
|
+
projectPaths.stateDbPath,
|
|
128
|
+
].includes(authorityDbPath);
|
|
129
|
+
const projectExists = fs.existsSync(path.join(rootDir, '.ultra'));
|
|
130
|
+
|
|
131
|
+
if (!projectExists) {
|
|
132
|
+
const error = new Error(
|
|
133
|
+
'Ultra authority is not initialized; invoke ultra-init before using project tools',
|
|
134
|
+
);
|
|
135
|
+
error.code = 'STATE_DB_MISSING';
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (usesProjectAuthority) {
|
|
999
140
|
authorityDbPath = runtimePaths.ensureRuntimeState(rootDir, {
|
|
1000
|
-
admitStorageBoundary: () =>
|
|
141
|
+
admitStorageBoundary: () => (
|
|
142
|
+
gitBootstrap.ensureExistingProjectStorageBoundary(rootDir)
|
|
143
|
+
),
|
|
1001
144
|
}).stateDbPath;
|
|
1002
145
|
} else {
|
|
1003
|
-
// Only a real external/task-linked override reaches binding
|
|
1004
|
-
// validation. It may never invent or borrow project authority.
|
|
1005
146
|
runtimePaths.locateStateDb(rootDir, {
|
|
1006
147
|
env: { UBP_DB_PATH: authorityDbPath },
|
|
1007
148
|
});
|
|
@@ -1009,29 +150,51 @@ function startServer({
|
|
|
1009
150
|
env: { UBP_DB_PATH: authorityDbPath },
|
|
1010
151
|
allowConfiguredRuntimeLink: true,
|
|
1011
152
|
migrateState: false,
|
|
1012
|
-
admitStorageBoundary: () =>
|
|
153
|
+
admitStorageBoundary: () => (
|
|
154
|
+
gitBootstrap.ensureExistingProjectStorageBoundary(rootDir)
|
|
155
|
+
),
|
|
1013
156
|
});
|
|
1014
157
|
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
158
|
+
|
|
159
|
+
if (['ultra.context', 'ultra.doctor'].includes(toolName)
|
|
160
|
+
&& !fs.existsSync(authorityDbPath)) {
|
|
161
|
+
const error = new Error('Ultra runtime state is missing');
|
|
162
|
+
error.code = 'STATE_DB_MISSING';
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
db = fs.existsSync(authorityDbPath)
|
|
167
|
+
? openStateDb(authorityDbPath)
|
|
168
|
+
: initStateDb(authorityDbPath).db;
|
|
169
|
+
ensureSchemaVersion(db);
|
|
170
|
+
|
|
171
|
+
if (!ownsTeamAuthorityInspection(toolName)) {
|
|
172
|
+
const teamSync = assertStateAuthority(db, rootDir, {
|
|
173
|
+
importTeamLedger: toolName !== 'ultra.context',
|
|
1025
174
|
});
|
|
175
|
+
if (teamSync?.status === 'imported') {
|
|
176
|
+
const job = runtimeState.enqueueProjection(db, { tool_name: 'ultra.sync' });
|
|
177
|
+
runtimeState.processProjectionJobs(db, {
|
|
178
|
+
rootDir,
|
|
179
|
+
project,
|
|
180
|
+
limit: 500,
|
|
181
|
+
});
|
|
182
|
+
if (!job?.id) {
|
|
183
|
+
const error = new Error('team authority import projection was not scheduled');
|
|
184
|
+
error.code = 'STATE_CORRUPT';
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
1026
188
|
}
|
|
1027
189
|
planRecovery = planStore.recoverPlanPublications(db, { rootDir });
|
|
1028
190
|
}
|
|
1029
|
-
|
|
1030
|
-
|
|
191
|
+
|
|
192
|
+
if (!['ultra.context', 'ultra.doctor'].includes(toolName)
|
|
193
|
+
&& (planRecovery?.pending > 0 || planRecovery?.issues?.length > 0)) {
|
|
1031
194
|
planRecovery = planStore.recoverPlanPublications(db, { rootDir });
|
|
1032
195
|
if (planRecovery.pending > 0 || planRecovery.issues.length > 0) {
|
|
1033
196
|
const error = new Error(
|
|
1034
|
-
'plan publication recovery is incomplete; run
|
|
197
|
+
'plan publication recovery is incomplete; run ultra.doctor with repair=true',
|
|
1035
198
|
);
|
|
1036
199
|
error.code = 'PLAN_RECOVERY_REQUIRED';
|
|
1037
200
|
error.details = planRecovery;
|
|
@@ -1041,14 +204,13 @@ function startServer({
|
|
|
1041
204
|
return db;
|
|
1042
205
|
};
|
|
1043
206
|
|
|
1044
|
-
const
|
|
1045
|
-
const publicTools = tools.filter((tool) => PUBLIC_TOOLS.includes(tool.name));
|
|
207
|
+
const toolContracts = loadToolContracts();
|
|
1046
208
|
const ajv = buildAjv();
|
|
1047
209
|
const inputValidators = new Map();
|
|
1048
210
|
const outputValidators = new Map();
|
|
1049
|
-
for (const
|
|
1050
|
-
inputValidators.set(
|
|
1051
|
-
outputValidators.set(
|
|
211
|
+
for (const tool of toolContracts) {
|
|
212
|
+
inputValidators.set(tool.name, ajv.compile(tool.input_schema));
|
|
213
|
+
outputValidators.set(tool.name, ajv.compile(tool.output_schema));
|
|
1052
214
|
}
|
|
1053
215
|
|
|
1054
216
|
const server = new Server(
|
|
@@ -1057,156 +219,113 @@ function startServer({
|
|
|
1057
219
|
);
|
|
1058
220
|
|
|
1059
221
|
server.setRequestHandler('tools/list', async () => ({
|
|
1060
|
-
tools:
|
|
1061
|
-
name:
|
|
1062
|
-
description:
|
|
1063
|
-
inputSchema:
|
|
222
|
+
tools: toolContracts.map((tool) => ({
|
|
223
|
+
name: tool.name,
|
|
224
|
+
description: tool.description,
|
|
225
|
+
inputSchema: tool.input_schema,
|
|
1064
226
|
})),
|
|
1065
227
|
}));
|
|
1066
228
|
|
|
1067
229
|
server.setRequestHandler('tools/call', async (request) => {
|
|
1068
|
-
const { name, arguments:
|
|
1069
|
-
const
|
|
230
|
+
const { name, arguments: input = {} } = request.params;
|
|
231
|
+
const startedAt = Date.now();
|
|
1070
232
|
let toolError = null;
|
|
233
|
+
let toolDb = null;
|
|
1071
234
|
|
|
1072
235
|
const emitTelemetry = () => {
|
|
1073
|
-
if (!
|
|
236
|
+
if (!toolDb) return;
|
|
1074
237
|
try {
|
|
1075
|
-
telemetry.appendTelemetry(
|
|
238
|
+
telemetry.appendTelemetry(toolDb, {
|
|
1076
239
|
event_type: 'tool_call',
|
|
1077
240
|
tool_name: name,
|
|
1078
|
-
session_id:
|
|
241
|
+
session_id: input.scope?.sid || null,
|
|
1079
242
|
rootDir,
|
|
1080
243
|
payload: {
|
|
1081
|
-
duration_ms: Date.now() -
|
|
1082
|
-
task_id:
|
|
244
|
+
duration_ms: Date.now() - startedAt,
|
|
245
|
+
task_id: input.scope?.task_id || null,
|
|
1083
246
|
error: toolError,
|
|
1084
247
|
},
|
|
1085
248
|
});
|
|
1086
|
-
} catch (
|
|
1087
|
-
process.stderr.write(`telemetry warning: ${
|
|
249
|
+
} catch (error) {
|
|
250
|
+
process.stderr.write(`telemetry warning: ${error.message}\n`);
|
|
1088
251
|
}
|
|
1089
252
|
};
|
|
1090
253
|
|
|
1091
254
|
if (!REGISTERED_TOOLS.includes(name)) {
|
|
1092
|
-
toolError = 'UNKNOWN_TOOL';
|
|
1093
|
-
emitTelemetry();
|
|
1094
255
|
return errorResponse('UNKNOWN_TOOL', `tool ${name} is not registered on this server`);
|
|
1095
256
|
}
|
|
1096
257
|
const validateInput = inputValidators.get(name);
|
|
1097
|
-
if (!validateInput(
|
|
1098
|
-
toolError = 'VALIDATION_ERROR';
|
|
1099
|
-
emitTelemetry();
|
|
258
|
+
if (!validateInput(input)) {
|
|
1100
259
|
return errorResponse('VALIDATION_ERROR', ajv.errorsText(validateInput.errors));
|
|
1101
260
|
}
|
|
1102
261
|
|
|
1103
|
-
let result;
|
|
1104
|
-
let toolDb = null;
|
|
1105
262
|
try {
|
|
1106
|
-
toolDb = (
|
|
1107
|
-
|
|
1108
|
-
: getDb(name);
|
|
1109
|
-
if (toolDb) assertStateAuthority(toolDb, rootDir);
|
|
1110
|
-
if (toolDb && TEAM_LEDGER_SYNC_TOOLS.has(name)) {
|
|
263
|
+
toolDb = isInitializeCall(name, input) ? null : getDb(name, input);
|
|
264
|
+
if (toolDb && requiresTeamSync(name, input)) {
|
|
1111
265
|
taskLedger.syncTaskLedger(toolDb, { rootDir });
|
|
1112
266
|
}
|
|
1113
|
-
const
|
|
267
|
+
const result = await ultraFacade.dispatch(name, input, toolDb, {
|
|
1114
268
|
rootDir,
|
|
269
|
+
runtime,
|
|
1115
270
|
sessionId,
|
|
1116
271
|
projector: project,
|
|
1117
272
|
markPlanRecoveryRequired,
|
|
1118
|
-
};
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
const
|
|
1126
|
-
|
|
1127
|
-
const error = new Error(ajv.errorsText(validateLegacyInput.errors));
|
|
1128
|
-
error.code = 'VALIDATION_ERROR';
|
|
1129
|
-
error.details = validateLegacyInput.errors;
|
|
1130
|
-
throw error;
|
|
1131
|
-
}
|
|
1132
|
-
const legacyResult = await dispatchTool(
|
|
1133
|
-
legacyName,
|
|
1134
|
-
legacyArgs,
|
|
273
|
+
});
|
|
274
|
+
if (name === 'ultra.doctor' && input.repair === true) {
|
|
275
|
+
planRecovery = result.repair?.plan_publications || null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
let runtimeMeta = null;
|
|
279
|
+
if (toolDb && projectOnWrite && isMutation(name, input)) {
|
|
280
|
+
const job = runtimeState.enqueueProjection(toolDb, { tool_name: name });
|
|
281
|
+
const processed = runtimeState.processProjectionJobs(
|
|
1135
282
|
toolDb,
|
|
1136
|
-
|
|
283
|
+
{ rootDir, project, limit: 500 },
|
|
284
|
+
);
|
|
285
|
+
const own = processed.jobs.find((item) => item.id === job.id);
|
|
286
|
+
runtimeMeta = {
|
|
287
|
+
state_commit: 'committed',
|
|
288
|
+
projection_status: own?.status || 'failed',
|
|
289
|
+
projection_job_id: job.id,
|
|
290
|
+
};
|
|
291
|
+
if (own?.incident_id) runtimeMeta.incident_id = own.incident_id;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const validateOutput = outputValidators.get(name);
|
|
295
|
+
if (!validateOutput(result)) {
|
|
296
|
+
toolError = 'OUTPUT_SCHEMA_DRIFT';
|
|
297
|
+
emitTelemetry();
|
|
298
|
+
return errorResponse(
|
|
299
|
+
'OUTPUT_SCHEMA_DRIFT',
|
|
300
|
+
ajv.errorsText(validateOutput.errors),
|
|
301
|
+
false,
|
|
302
|
+
runtimeMeta ? { _ultra: runtimeMeta } : undefined,
|
|
1137
303
|
);
|
|
1138
|
-
const validateLegacyOutput = outputValidators.get(legacyName);
|
|
1139
|
-
if (!validateLegacyOutput(legacyResult)) {
|
|
1140
|
-
const error = new Error(ajv.errorsText(validateLegacyOutput.errors));
|
|
1141
|
-
error.code = 'OUTPUT_SCHEMA_DRIFT';
|
|
1142
|
-
error.details = validateLegacyOutput.errors;
|
|
1143
|
-
throw error;
|
|
1144
|
-
}
|
|
1145
|
-
return legacyResult;
|
|
1146
|
-
};
|
|
1147
|
-
result = PUBLIC_TOOLS.includes(name)
|
|
1148
|
-
? await ultraFacade.dispatch(name, args, toolDb, { ...callContext, callLegacy })
|
|
1149
|
-
: await dispatchTool(name, args, toolDb, callContext);
|
|
1150
|
-
if (['system.doctor', 'ultra.doctor'].includes(name) && args.repair === true) {
|
|
1151
|
-
planRecovery = result.repair?.plan_publications || null;
|
|
1152
304
|
}
|
|
1153
|
-
} catch (err) {
|
|
1154
|
-
const code = err.code || (err instanceof ops.StateOpsError ? err.code : 'STATE_DB_ERROR');
|
|
1155
|
-
toolError = code;
|
|
1156
|
-
emitTelemetry();
|
|
1157
|
-
return errorResponse(code, err.message, !!err.retriable, err.details);
|
|
1158
|
-
}
|
|
1159
305
|
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
const job = runtimeState.enqueueProjection(toolDb, { tool_name: name });
|
|
1167
|
-
const processed = runtimeState.processProjectionJobs(toolDb, { rootDir, project, limit: 500 });
|
|
1168
|
-
const own = processed.jobs.find((item) => item.id === job.id);
|
|
1169
|
-
runtimeMeta = {
|
|
1170
|
-
state_commit: 'committed',
|
|
1171
|
-
projection_status: own ? own.status : 'failed',
|
|
1172
|
-
projection_job_id: job.id,
|
|
1173
|
-
};
|
|
1174
|
-
if (own && own.incident_id) runtimeMeta.incident_id = own.incident_id;
|
|
1175
|
-
} else if (toolDb && ['system.doctor', 'ultra.doctor'].includes(name) && args.repair === true) {
|
|
1176
|
-
runtimeMeta = {
|
|
1177
|
-
state_commit: 'committed',
|
|
1178
|
-
projection_status: result.checks.projections.status === 'pass' ? 'completed' : 'failed',
|
|
1179
|
-
backup_path: result.backup_path,
|
|
306
|
+
emitTelemetry();
|
|
307
|
+
const visible = runtimeMeta ? { ...result, _ultra: runtimeMeta } : result;
|
|
308
|
+
return {
|
|
309
|
+
content: [{ type: 'text', text: JSON.stringify(visible) }],
|
|
310
|
+
structuredContent: result,
|
|
311
|
+
...(runtimeMeta ? { _meta: { ultra: runtimeMeta } } : {}),
|
|
1180
312
|
};
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
const validateOutput = outputValidators.get(name);
|
|
1184
|
-
if (!validateOutput(result)) {
|
|
1185
|
-
toolError = 'OUTPUT_SCHEMA_DRIFT';
|
|
313
|
+
} catch (error) {
|
|
314
|
+
toolError = error.code || 'STATE_DB_ERROR';
|
|
1186
315
|
emitTelemetry();
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
316
|
+
return errorResponse(
|
|
317
|
+
toolError,
|
|
318
|
+
error.message,
|
|
319
|
+
Boolean(error.retriable),
|
|
320
|
+
error.details,
|
|
1192
321
|
);
|
|
1193
|
-
if (runtimeMeta) response._meta = { ultra: runtimeMeta };
|
|
1194
|
-
return response;
|
|
1195
322
|
}
|
|
1196
|
-
|
|
1197
|
-
emitTelemetry();
|
|
1198
|
-
const visible = runtimeMeta ? { ...result, _ultra: runtimeMeta } : result;
|
|
1199
|
-
return {
|
|
1200
|
-
content: [{ type: 'text', text: JSON.stringify(visible) }],
|
|
1201
|
-
structuredContent: result,
|
|
1202
|
-
...(runtimeMeta ? { _meta: { ultra: runtimeMeta } } : {}),
|
|
1203
|
-
};
|
|
1204
323
|
});
|
|
1205
324
|
|
|
1206
325
|
return {
|
|
1207
326
|
server,
|
|
1208
327
|
get db() { return db; },
|
|
1209
|
-
tools,
|
|
328
|
+
tools: toolContracts,
|
|
1210
329
|
async close() { closeStateDb(db); },
|
|
1211
330
|
};
|
|
1212
331
|
}
|
|
@@ -1215,17 +334,15 @@ async function main() {
|
|
|
1215
334
|
const rootDir = process.env.UBP_ROOT_DIR
|
|
1216
335
|
? path.resolve(process.env.UBP_ROOT_DIR)
|
|
1217
336
|
: path.resolve('.');
|
|
1218
|
-
|
|
1219
|
-
// first state-backed tool call so clients receive a typed MCP error instead
|
|
1220
|
-
// of a transport-level startup failure.
|
|
1221
|
-
const configuredDb = typeof process.env.UBP_DB_PATH === 'string'
|
|
1222
|
-
? process.env.UBP_DB_PATH.trim()
|
|
1223
|
-
: '';
|
|
337
|
+
const configuredDb = String(process.env.UBP_DB_PATH || '').trim();
|
|
1224
338
|
const dbPath = configuredDb
|
|
1225
339
|
? path.resolve(configuredDb)
|
|
1226
340
|
: runtimePaths.pathsFor(rootDir).stateDbPath;
|
|
1227
|
-
const
|
|
1228
|
-
|
|
341
|
+
const handle = startServer({
|
|
342
|
+
dbPath,
|
|
343
|
+
rootDir,
|
|
344
|
+
sessionId: process.env.UBP_SESSION_ID || null,
|
|
345
|
+
});
|
|
1229
346
|
const transport = new StdioServerTransport();
|
|
1230
347
|
await handle.server.connect(transport);
|
|
1231
348
|
const cleanup = () => handle.close().finally(() => process.exit(0));
|
|
@@ -1251,37 +368,29 @@ function appendHookLifecycleEvent({ rootDir, action, hookInput = {} } = {}) {
|
|
|
1251
368
|
const db = openStateDb(dbPath);
|
|
1252
369
|
try {
|
|
1253
370
|
ensureSchemaVersion(db);
|
|
1254
|
-
const workflow = db.prepare(
|
|
1255
|
-
`SELECT change_id, task_id FROM workflow_runs
|
|
1256
|
-
WHERE status IN ('active', 'blocked', 'ready') AND change_id IS NOT NULL
|
|
1257
|
-
ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
|
|
1258
|
-
).get();
|
|
1259
|
-
if (!workflow) return { recorded: false, reason: 'NO_ACTIVE_WORKFLOW' };
|
|
1260
371
|
const change = db.prepare(
|
|
1261
372
|
`SELECT id FROM changes
|
|
1262
|
-
WHERE
|
|
1263
|
-
|
|
373
|
+
WHERE status = 'active'
|
|
374
|
+
ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
|
|
375
|
+
).get();
|
|
1264
376
|
if (!change) return { recorded: false, reason: 'NO_ACTIVE_CHANGE' };
|
|
1265
|
-
const task =
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
: db.prepare(
|
|
1272
|
-
`SELECT id FROM tasks WHERE change_id = ? AND status IN ('in_progress', 'pending', 'blocked')
|
|
1273
|
-
ORDER BY CASE status WHEN 'in_progress' THEN 0 WHEN 'blocked' THEN 1 ELSE 2 END,
|
|
1274
|
-
updated_at DESC, rowid DESC LIMIT 1`,
|
|
1275
|
-
).get(change.id);
|
|
377
|
+
const task = db.prepare(
|
|
378
|
+
`SELECT id FROM tasks
|
|
379
|
+
WHERE change_id = ? AND status IN ('in_progress', 'pending', 'blocked')
|
|
380
|
+
ORDER BY CASE status WHEN 'in_progress' THEN 0 WHEN 'blocked' THEN 1 ELSE 2 END,
|
|
381
|
+
updated_at DESC, rowid DESC LIMIT 1`,
|
|
382
|
+
).get(change.id);
|
|
1276
383
|
const payload = {
|
|
1277
384
|
agent_id: hookMetadata(hookInput.agent_id),
|
|
1278
385
|
agent_type: hookMetadata(hookInput.agent_type),
|
|
1279
386
|
host_session_id: hookMetadata(hookInput.session_id, '', 256),
|
|
1280
387
|
};
|
|
388
|
+
const hookRuntime = hookMetadata(hookInput.runtime, '', 64) || null;
|
|
1281
389
|
ops.appendEvent(db, {
|
|
1282
390
|
type: action === 'start' ? 'subagent_started' : 'subagent_stopped',
|
|
1283
391
|
change_id: change.id,
|
|
1284
392
|
task_id: task?.id || null,
|
|
393
|
+
runtime: hookRuntime,
|
|
1285
394
|
payload,
|
|
1286
395
|
});
|
|
1287
396
|
return { recorded: true, change_id: change.id, task_id: task?.id || null };
|
|
@@ -1291,8 +400,8 @@ function appendHookLifecycleEvent({ rootDir, action, hookInput = {} } = {}) {
|
|
|
1291
400
|
}
|
|
1292
401
|
|
|
1293
402
|
if (require.main === module) {
|
|
1294
|
-
main().catch((
|
|
1295
|
-
process.stderr.write(`mcp-server fatal: ${
|
|
403
|
+
main().catch((error) => {
|
|
404
|
+
process.stderr.write(`mcp-server fatal: ${error.message}\n`);
|
|
1296
405
|
process.exit(1);
|
|
1297
406
|
});
|
|
1298
407
|
}
|
|
@@ -1300,22 +409,10 @@ if (require.main === module) {
|
|
|
1300
409
|
module.exports = {
|
|
1301
410
|
main,
|
|
1302
411
|
startServer,
|
|
1303
|
-
dispatchTool,
|
|
1304
|
-
TASK_TOOLS,
|
|
1305
|
-
SESSION_TOOLS,
|
|
1306
|
-
PLAN_TOOLS,
|
|
1307
|
-
BASELINE_TOOLS,
|
|
1308
|
-
CHANGE_TOOLS,
|
|
1309
|
-
WORKFLOW_TOOLS,
|
|
1310
|
-
ARTIFACT_TOOLS,
|
|
1311
|
-
SYSTEM_TOOLS,
|
|
1312
412
|
REGISTERED_TOOLS,
|
|
1313
|
-
LEGACY_TOOLS,
|
|
1314
413
|
PUBLIC_TOOLS,
|
|
1315
|
-
STATELESS_TOOLS,
|
|
1316
|
-
MUTATING_TOOLS,
|
|
1317
414
|
appendHookLifecycleEvent,
|
|
1318
415
|
findProjectRoot: runtimePaths.findProjectRoot,
|
|
1319
|
-
|
|
1320
|
-
|
|
416
|
+
readProjectContextEnvelope,
|
|
417
|
+
renderProjectContextEnvelope,
|
|
1321
418
|
};
|