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