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
|
@@ -0,0 +1,834 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const artifactRegistry = require('./artifact-registry.cjs');
|
|
7
|
+
const baselines = require('./baseline-workflow.cjs');
|
|
8
|
+
const changes = require('./change-workflow.cjs');
|
|
9
|
+
const contextEnvelope = require('./context-envelope.cjs');
|
|
10
|
+
const decisionRecords = require('./decision-records.cjs');
|
|
11
|
+
const doctor = require('./doctor.cjs');
|
|
12
|
+
const gitBootstrap = require('./git-bootstrap.cjs');
|
|
13
|
+
const { initProject } = require('./init-project.cjs');
|
|
14
|
+
const ops = require('./state-ops.cjs');
|
|
15
|
+
const planCheckpoint = require('./plan-checkpoint.cjs');
|
|
16
|
+
const { readStableProjectFile } = require('./safe-project-file.cjs');
|
|
17
|
+
const stageCheckpoints = require('./stage-checkpoints.cjs');
|
|
18
|
+
const taskLedger = require('./task-ledger.cjs');
|
|
19
|
+
const workerPacket = require('./worker-packet.cjs');
|
|
20
|
+
const sessionRunner = require('../../orchestrator/session-runner.cjs');
|
|
21
|
+
|
|
22
|
+
const PUBLIC_TOOLS = Object.freeze([
|
|
23
|
+
'ultra.context',
|
|
24
|
+
'ultra.record',
|
|
25
|
+
'ultra.checkpoint',
|
|
26
|
+
'ultra.sync',
|
|
27
|
+
'ultra.session',
|
|
28
|
+
'ultra.archive',
|
|
29
|
+
'ultra.doctor',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const RECORD_KINDS = Object.freeze([
|
|
33
|
+
'baseline',
|
|
34
|
+
'change_contract',
|
|
35
|
+
'decision',
|
|
36
|
+
'task_contract',
|
|
37
|
+
'task_outcome',
|
|
38
|
+
'artifact',
|
|
39
|
+
'event',
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
const HARD_ERROR_CODES = new Set([
|
|
43
|
+
'ARCHIVE_PATH_UNSAFE',
|
|
44
|
+
'ARCHIVE_RECOVERY_REQUIRED',
|
|
45
|
+
'ARCHIVE_RUNTIME_UNAVAILABLE',
|
|
46
|
+
'BACKUP_FAILED',
|
|
47
|
+
'CHECKPOINT_DIGEST_MISMATCH',
|
|
48
|
+
'CONTEXT_ENVELOPE_LIMIT_EXCEEDED',
|
|
49
|
+
'CONTEXT_ENVELOPE_DIGEST_MISMATCH',
|
|
50
|
+
'CONTEXT_ENVELOPE_FILE_DRIFT',
|
|
51
|
+
'DECISION_DIGEST_MISMATCH',
|
|
52
|
+
'DECISION_FILE_DRIFT',
|
|
53
|
+
'DECISION_ID_CONFLICT',
|
|
54
|
+
'DECISION_SCOPE_CONFLICT',
|
|
55
|
+
'OUTPUT_SCHEMA_DRIFT',
|
|
56
|
+
'PATH_AUTHORITY_VIOLATION',
|
|
57
|
+
'PLAN_RECOVERY_REQUIRED',
|
|
58
|
+
'RUNTIME_ABI_MISMATCH',
|
|
59
|
+
'RUNTIME_NATIVE_MISSING',
|
|
60
|
+
'SCHEMA_VERSION_MISMATCH',
|
|
61
|
+
'SESSION_STATUS_CONFLICT',
|
|
62
|
+
'STATE_CORRUPT',
|
|
63
|
+
'STATE_DB_ERROR',
|
|
64
|
+
'STATE_DB_MISSING',
|
|
65
|
+
'TASK_LEDGER_BASELINE_CONFLICT',
|
|
66
|
+
'TASK_LEDGER_CHANGE_CONFLICT',
|
|
67
|
+
'TASK_LEDGER_HISTORY_CONFLICT',
|
|
68
|
+
'TASK_LEDGER_TASK_CONFLICT',
|
|
69
|
+
'WORKER_PACKET_DIGEST_MISMATCH',
|
|
70
|
+
'WORKER_PACKET_FILE_DRIFT',
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
function errorCode(error) {
|
|
74
|
+
return String(error?.code || error?.name || 'NEEDS_ATTENTION');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isHardError(error) {
|
|
78
|
+
const code = errorCode(error);
|
|
79
|
+
return HARD_ERROR_CODES.has(code)
|
|
80
|
+
|| code.startsWith('PATH_')
|
|
81
|
+
|| code.startsWith('SQLITE_')
|
|
82
|
+
|| code.startsWith('STATE_CORRUPT')
|
|
83
|
+
|| code.startsWith('TASK_LEDGER_') && code.endsWith('_CONFLICT')
|
|
84
|
+
|| code.startsWith('SESSION_CLOSE_') && code.endsWith('_CONFLICT')
|
|
85
|
+
|| code.startsWith('WORKTREE_') && (
|
|
86
|
+
code.endsWith('_UNSAFE') || code.endsWith('_CONFLICT')
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function parseJson(value, fallback = null) {
|
|
91
|
+
try { return value == null ? fallback : JSON.parse(value); }
|
|
92
|
+
catch { return fallback; }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function priorResult(db, idempotencyKey, operation) {
|
|
96
|
+
if (!db || !idempotencyKey) return null;
|
|
97
|
+
const rows = db.prepare(
|
|
98
|
+
`SELECT payload_json FROM events
|
|
99
|
+
WHERE type = 'ultra_kernel_call'
|
|
100
|
+
ORDER BY id DESC LIMIT 2000`,
|
|
101
|
+
).all();
|
|
102
|
+
for (const row of rows) {
|
|
103
|
+
const payload = parseJson(row.payload_json, {});
|
|
104
|
+
if (payload.idempotency_key === idempotencyKey
|
|
105
|
+
&& payload.operation === operation
|
|
106
|
+
&& payload.accepted === true) {
|
|
107
|
+
return payload.result;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function rememberResult(db, {
|
|
114
|
+
idempotencyKey,
|
|
115
|
+
operation,
|
|
116
|
+
result,
|
|
117
|
+
changeId = null,
|
|
118
|
+
taskId = null,
|
|
119
|
+
}) {
|
|
120
|
+
if (!db || !idempotencyKey) return;
|
|
121
|
+
ops.appendEvent(db, {
|
|
122
|
+
type: 'ultra_kernel_call',
|
|
123
|
+
change_id: changeId,
|
|
124
|
+
task_id: taskId,
|
|
125
|
+
payload: {
|
|
126
|
+
idempotency_key: idempotencyKey,
|
|
127
|
+
operation,
|
|
128
|
+
accepted: true,
|
|
129
|
+
result,
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function diagnostic(error, severity = 'needs_attention') {
|
|
135
|
+
return {
|
|
136
|
+
code: errorCode(error),
|
|
137
|
+
severity,
|
|
138
|
+
message: error.message,
|
|
139
|
+
...(error.details === undefined ? {} : { details: error.details }),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function selectedScope(db, input = {}) {
|
|
144
|
+
const scope = input.scope || {};
|
|
145
|
+
if (scope.task_id) {
|
|
146
|
+
const task = ops.readTask(db, scope.task_id);
|
|
147
|
+
if (!task) return { task_id: scope.task_id };
|
|
148
|
+
return {
|
|
149
|
+
change_id: scope.change_id || task.change_id || undefined,
|
|
150
|
+
task_id: task.id,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
if (scope.change_id) return { change_id: scope.change_id };
|
|
154
|
+
const baseline = baselines.readBaseline(db);
|
|
155
|
+
return baseline ? { baseline_id: baseline.id } : { project_id: 'project' };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function readContext(db, input = {}, {
|
|
159
|
+
rootDir = process.cwd(),
|
|
160
|
+
runtime = 'unknown',
|
|
161
|
+
} = {}) {
|
|
162
|
+
return contextEnvelope.buildEnvelope(db, input, { rootDir, runtime });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function initializeProject(data = {}) {
|
|
166
|
+
const targetDir = path.resolve(data.target_dir || process.cwd());
|
|
167
|
+
const alreadyInitialized = fs.existsSync(path.join(targetDir, '.ultra'));
|
|
168
|
+
return initProject({
|
|
169
|
+
...data,
|
|
170
|
+
target_dir: targetDir,
|
|
171
|
+
resume: alreadyInitialized || data.resume === true,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function baselineRecord(db, action, data, rootDir) {
|
|
176
|
+
if (action === 'initialize') return initializeProject(data);
|
|
177
|
+
if (action === 'start') {
|
|
178
|
+
return { baseline: baselines.startBaseline(db, data, { rootDir }) };
|
|
179
|
+
}
|
|
180
|
+
if (action === 'observe' || action === 'revise') {
|
|
181
|
+
return { baseline: baselines.recordBaseline(db, data, { rootDir }) };
|
|
182
|
+
}
|
|
183
|
+
if (action === 'accept') {
|
|
184
|
+
const result = baselines.convergeBaseline(db, data, { rootDir });
|
|
185
|
+
return {
|
|
186
|
+
...result,
|
|
187
|
+
diagnostics: result.ready
|
|
188
|
+
? []
|
|
189
|
+
: (result.blockers || []).map((code) => ({
|
|
190
|
+
code,
|
|
191
|
+
severity: 'needs_attention',
|
|
192
|
+
})),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
throw Object.assign(new Error(`unsupported baseline action: ${action}`), {
|
|
196
|
+
code: 'VALIDATION_ERROR',
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function changeContractRecord(db, action, data, rootDir) {
|
|
201
|
+
if (action === 'open') {
|
|
202
|
+
return changes.createChange(db, data, { rootDir, kernelMode: true });
|
|
203
|
+
}
|
|
204
|
+
if (action === 'revise') {
|
|
205
|
+
return {
|
|
206
|
+
change: changes.updateChange(
|
|
207
|
+
db,
|
|
208
|
+
data.id,
|
|
209
|
+
data.patch || {},
|
|
210
|
+
{ rootDir, kernelMode: true },
|
|
211
|
+
),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
if (action === 'cancel') {
|
|
215
|
+
return {
|
|
216
|
+
change: changes.updateChange(
|
|
217
|
+
db,
|
|
218
|
+
data.id,
|
|
219
|
+
{ status: 'cancelled' },
|
|
220
|
+
{ rootDir, kernelMode: true },
|
|
221
|
+
),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
throw Object.assign(new Error(`unsupported change_contract action: ${action}`), {
|
|
225
|
+
code: 'VALIDATION_ERROR',
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function taskContractRecord(db, action, data) {
|
|
230
|
+
if (action === 'define') return { task: ops.createTask(db, data) };
|
|
231
|
+
if (action === 'revise') return { task: ops.patchTask(db, data.id, data.patch || {}) };
|
|
232
|
+
if (action === 'remove') return ops.deleteTask(db, data.id, { force: data.force === true });
|
|
233
|
+
throw Object.assign(new Error(`unsupported task_contract action: ${action}`), {
|
|
234
|
+
code: 'VALIDATION_ERROR',
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function taskOutcomeRecord(db, action, data, rootDir) {
|
|
239
|
+
const status = {
|
|
240
|
+
start: 'in_progress',
|
|
241
|
+
complete: 'completed',
|
|
242
|
+
block: 'blocked',
|
|
243
|
+
reopen: 'in_progress',
|
|
244
|
+
}[action];
|
|
245
|
+
if (!status) {
|
|
246
|
+
throw Object.assign(new Error(`unsupported task_outcome action: ${action}`), {
|
|
247
|
+
code: 'VALIDATION_ERROR',
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
if (action === 'complete') {
|
|
251
|
+
const packet = db.prepare(
|
|
252
|
+
`SELECT id, packet_digest, packet_path, output_path
|
|
253
|
+
FROM worker_packets
|
|
254
|
+
WHERE scope_type = 'task' AND scope_id = ? AND status = 'assigned'
|
|
255
|
+
ORDER BY created_at DESC, rowid DESC LIMIT 1`,
|
|
256
|
+
).get(data.id);
|
|
257
|
+
if (!packet) {
|
|
258
|
+
throw Object.assign(
|
|
259
|
+
new Error(`task ${data.id} has no assigned Worker Packet`),
|
|
260
|
+
{ code: 'WORKER_PACKET_REQUIRED' },
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
const packetDocument = workerPacket.readWorkerPacket(
|
|
264
|
+
db,
|
|
265
|
+
packet.id,
|
|
266
|
+
{ rootDir },
|
|
267
|
+
);
|
|
268
|
+
if (data.packet_digest !== packet.packet_digest) {
|
|
269
|
+
throw Object.assign(
|
|
270
|
+
new Error('task outcome does not reference the exact assigned Worker Packet'),
|
|
271
|
+
{ code: 'WORKER_PACKET_DIGEST_MISMATCH' },
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
const output = readStableProjectFile(rootDir, packet.output_path, { encoding: 'utf8' });
|
|
275
|
+
let result;
|
|
276
|
+
try {
|
|
277
|
+
result = JSON.parse(output.text);
|
|
278
|
+
} catch (cause) {
|
|
279
|
+
throw Object.assign(
|
|
280
|
+
new Error(`worker output is not valid JSON: ${packet.output_path}`),
|
|
281
|
+
{ code: 'WORKER_OUTPUT_INVALID', cause },
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
workerPacket.verifyWorkerResult(packetDocument, result);
|
|
285
|
+
artifactRegistry.recordArtifact(db, {
|
|
286
|
+
id: `artifact-worker-outcome-${packet.id}`,
|
|
287
|
+
owner_type: 'task',
|
|
288
|
+
owner_id: data.id,
|
|
289
|
+
task_id: data.id,
|
|
290
|
+
change_id: ops.readTask(db, data.id)?.change_id || null,
|
|
291
|
+
kind: 'task_outcome',
|
|
292
|
+
path: packet.output_path,
|
|
293
|
+
content_digest: output.digest,
|
|
294
|
+
source_refs: [{
|
|
295
|
+
type: 'external',
|
|
296
|
+
id: `worker-packet:${packet.id}`,
|
|
297
|
+
relation: 'produced_from_packet',
|
|
298
|
+
}],
|
|
299
|
+
consumer_refs: [{
|
|
300
|
+
type: 'external',
|
|
301
|
+
id: 'ultra-test',
|
|
302
|
+
relation: 'consumed_by',
|
|
303
|
+
}],
|
|
304
|
+
provenance: {
|
|
305
|
+
writer: 'worker-result',
|
|
306
|
+
packet_digest: packet.packet_digest,
|
|
307
|
+
},
|
|
308
|
+
metadata: {
|
|
309
|
+
worker_packet_id: packet.id,
|
|
310
|
+
packet_path: packet.packet_path,
|
|
311
|
+
},
|
|
312
|
+
}, { rootDir });
|
|
313
|
+
}
|
|
314
|
+
if (data.patch && Object.keys(data.patch).length > 0) {
|
|
315
|
+
ops.patchTask(db, data.id, data.patch);
|
|
316
|
+
}
|
|
317
|
+
return { task: ops.updateTaskStatus(db, data.id, status) };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function applyRecord(db, entry, { rootDir }) {
|
|
321
|
+
const { kind, action, data = {} } = entry;
|
|
322
|
+
if (!RECORD_KINDS.includes(kind)) {
|
|
323
|
+
throw Object.assign(new Error(`unsupported record kind: ${kind}`), {
|
|
324
|
+
code: 'VALIDATION_ERROR',
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
if (kind === 'baseline') return baselineRecord(db, action, data, rootDir);
|
|
328
|
+
if (kind === 'change_contract') return changeContractRecord(db, action, data, rootDir);
|
|
329
|
+
if (kind === 'decision' && action === 'accept') {
|
|
330
|
+
return { decision: decisionRecords.acceptDecision(db, data, { rootDir }) };
|
|
331
|
+
}
|
|
332
|
+
if (kind === 'task_contract') return taskContractRecord(db, action, data);
|
|
333
|
+
if (kind === 'task_outcome') return taskOutcomeRecord(db, action, data, rootDir);
|
|
334
|
+
if (kind === 'artifact' && action === 'bind') {
|
|
335
|
+
return artifactRegistry.recordArtifact(db, data, { rootDir });
|
|
336
|
+
}
|
|
337
|
+
if (kind === 'event' && action === 'append') {
|
|
338
|
+
return ops.appendEvent(db, data);
|
|
339
|
+
}
|
|
340
|
+
throw Object.assign(
|
|
341
|
+
new Error(`unsupported ${kind} action: ${action}`),
|
|
342
|
+
{ code: 'VALIDATION_ERROR' },
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function record(db, input = {}, {
|
|
347
|
+
rootDir = process.cwd(),
|
|
348
|
+
} = {}) {
|
|
349
|
+
const results = [];
|
|
350
|
+
for (const entry of input.entries || []) {
|
|
351
|
+
const operation = `${entry.kind}:${entry.action}`;
|
|
352
|
+
const cached = priorResult(db, entry.idempotency_key, operation);
|
|
353
|
+
if (cached) {
|
|
354
|
+
results.push({
|
|
355
|
+
kind: entry.kind,
|
|
356
|
+
action: entry.action,
|
|
357
|
+
accepted: true,
|
|
358
|
+
idempotent: true,
|
|
359
|
+
result: cached,
|
|
360
|
+
});
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
try {
|
|
364
|
+
const result = applyRecord(db, entry, { rootDir });
|
|
365
|
+
rememberResult(db, {
|
|
366
|
+
idempotencyKey: entry.idempotency_key,
|
|
367
|
+
operation,
|
|
368
|
+
result,
|
|
369
|
+
changeId: entry.data?.change_id || entry.data?.id || null,
|
|
370
|
+
taskId: entry.kind.startsWith('task_') ? entry.data?.id || null : null,
|
|
371
|
+
});
|
|
372
|
+
results.push({
|
|
373
|
+
kind: entry.kind,
|
|
374
|
+
action: entry.action,
|
|
375
|
+
accepted: true,
|
|
376
|
+
idempotent: false,
|
|
377
|
+
result,
|
|
378
|
+
diagnostics: result?.diagnostics || [],
|
|
379
|
+
});
|
|
380
|
+
} catch (error) {
|
|
381
|
+
if (isHardError(error)) throw error;
|
|
382
|
+
results.push({
|
|
383
|
+
kind: entry.kind,
|
|
384
|
+
action: entry.action,
|
|
385
|
+
accepted: false,
|
|
386
|
+
mutable: true,
|
|
387
|
+
diagnostics: [diagnostic(error)],
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
accepted: results.every((item) => item.accepted),
|
|
393
|
+
mutable: true,
|
|
394
|
+
results,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function checkpointDiagnostics(input, context) {
|
|
399
|
+
const supplied = Array.isArray(input.payload?.diagnostics)
|
|
400
|
+
? input.payload.diagnostics
|
|
401
|
+
: [];
|
|
402
|
+
const derived = [
|
|
403
|
+
...(context.envelope?.diagnostics?.warnings || []).map((item) => ({
|
|
404
|
+
...item,
|
|
405
|
+
severity: 'warning',
|
|
406
|
+
})),
|
|
407
|
+
...(context.envelope?.diagnostics?.needs_attention || []).map((item) => ({
|
|
408
|
+
...item,
|
|
409
|
+
severity: 'warning',
|
|
410
|
+
})),
|
|
411
|
+
...(context.envelope?.diagnostics?.hard_conflicts || []).map((item) => ({
|
|
412
|
+
...item,
|
|
413
|
+
severity: 'hard_conflict',
|
|
414
|
+
})),
|
|
415
|
+
];
|
|
416
|
+
return [...supplied, ...derived]
|
|
417
|
+
.filter((item) => item && typeof item === 'object' && item.code)
|
|
418
|
+
.map((item) => ({
|
|
419
|
+
code: String(item.code),
|
|
420
|
+
severity: item.severity || 'needs_attention',
|
|
421
|
+
...(item.message ? { message: String(item.message) } : {}),
|
|
422
|
+
...(item.details === undefined ? {} : { details: item.details }),
|
|
423
|
+
}));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function stageReadinessDiagnostics(db, input, scope) {
|
|
427
|
+
const payload = input.payload || {};
|
|
428
|
+
const evidence = Array.isArray(payload.evidence) ? payload.evidence : [];
|
|
429
|
+
const needs = (code, message) => ({ code, severity: 'needs_attention', message });
|
|
430
|
+
if (input.stage === 'research') {
|
|
431
|
+
return evidence.length > 0
|
|
432
|
+
? []
|
|
433
|
+
: [needs('RESEARCH_EVIDENCE_REQUIRED', 'Research checkpoint requires at least one evidence reference.')];
|
|
434
|
+
}
|
|
435
|
+
if (input.stage === 'dev') {
|
|
436
|
+
const task = scope.task_id ? ops.readTask(db, scope.task_id) : null;
|
|
437
|
+
const outcome = task
|
|
438
|
+
? db.prepare(
|
|
439
|
+
`SELECT id FROM artifacts
|
|
440
|
+
WHERE task_id = ? AND kind = 'task_outcome' AND status = 'current'
|
|
441
|
+
ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
|
|
442
|
+
).get(task.id)
|
|
443
|
+
: null;
|
|
444
|
+
const diagnostics = [];
|
|
445
|
+
if (!task) diagnostics.push(needs('TASK_NOT_FOUND', 'Dev checkpoint requires a task scope.'));
|
|
446
|
+
else if (task.status !== 'completed') {
|
|
447
|
+
diagnostics.push(needs('TASK_OUTCOME_INCOMPLETE', `Task ${task.id} is not completed.`));
|
|
448
|
+
}
|
|
449
|
+
if (task && !outcome) {
|
|
450
|
+
diagnostics.push(needs(
|
|
451
|
+
'WORKER_OUTCOME_REQUIRED',
|
|
452
|
+
`Task ${task.id} requires an artifact bound to its exact Worker Packet.`,
|
|
453
|
+
));
|
|
454
|
+
}
|
|
455
|
+
return diagnostics;
|
|
456
|
+
}
|
|
457
|
+
if (input.stage === 'test') {
|
|
458
|
+
const result = String(payload.result || '').toLowerCase();
|
|
459
|
+
const diagnostics = [];
|
|
460
|
+
if (!['pass', 'known_red'].includes(result)) {
|
|
461
|
+
diagnostics.push(needs('TEST_RESULT_REQUIRED', 'Test checkpoint requires result=pass or known_red.'));
|
|
462
|
+
}
|
|
463
|
+
if (evidence.length === 0) {
|
|
464
|
+
diagnostics.push(needs('TEST_EVIDENCE_REQUIRED', 'Test checkpoint requires evidence.'));
|
|
465
|
+
}
|
|
466
|
+
return diagnostics;
|
|
467
|
+
}
|
|
468
|
+
if (input.stage === 'review') {
|
|
469
|
+
const verdict = String(payload.verdict || '').toLowerCase();
|
|
470
|
+
const diagnostics = [];
|
|
471
|
+
if (!['approve', 'pass'].includes(verdict)) {
|
|
472
|
+
diagnostics.push(needs('REVIEW_VERDICT_REQUIRED', 'Review checkpoint requires an approved verdict.'));
|
|
473
|
+
}
|
|
474
|
+
if (evidence.length === 0) {
|
|
475
|
+
diagnostics.push(needs('REVIEW_EVIDENCE_REQUIRED', 'Review checkpoint requires evidence.'));
|
|
476
|
+
}
|
|
477
|
+
return diagnostics;
|
|
478
|
+
}
|
|
479
|
+
if (input.stage === 'deliver') {
|
|
480
|
+
const diagnostics = [];
|
|
481
|
+
for (const stage of ['test', 'review']) {
|
|
482
|
+
if (!stageCheckpoints.currentCheckpoint(
|
|
483
|
+
db,
|
|
484
|
+
stage,
|
|
485
|
+
{ change_id: scope.change_id },
|
|
486
|
+
{ includeDraft: false },
|
|
487
|
+
)) {
|
|
488
|
+
diagnostics.push(needs(
|
|
489
|
+
`${stage.toUpperCase()}_CHECKPOINT_REQUIRED`,
|
|
490
|
+
`Deliver checkpoint requires an accepted ${stage} checkpoint.`,
|
|
491
|
+
));
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (String(payload.summary || '').trim().length < 3) {
|
|
495
|
+
diagnostics.push(needs('DELIVERY_SUMMARY_REQUIRED', 'Deliver checkpoint requires a summary.'));
|
|
496
|
+
}
|
|
497
|
+
if (evidence.length === 0) {
|
|
498
|
+
diagnostics.push(needs('DELIVERY_EVIDENCE_REQUIRED', 'Deliver checkpoint requires evidence.'));
|
|
499
|
+
}
|
|
500
|
+
return diagnostics;
|
|
501
|
+
}
|
|
502
|
+
return [];
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function checkpoint(db, input = {}, {
|
|
506
|
+
rootDir = process.cwd(),
|
|
507
|
+
runtime = 'unknown',
|
|
508
|
+
markPlanRecoveryRequired = null,
|
|
509
|
+
} = {}) {
|
|
510
|
+
const scope = selectedScope(db, input);
|
|
511
|
+
const context = contextEnvelope.persistEnvelope(db, {
|
|
512
|
+
stage: input.stage,
|
|
513
|
+
scope,
|
|
514
|
+
}, { rootDir, runtime });
|
|
515
|
+
const view = contextEnvelope.buildEnvelope(db, {
|
|
516
|
+
stage: input.stage,
|
|
517
|
+
scope,
|
|
518
|
+
detail: 'summary',
|
|
519
|
+
}, { rootDir, runtime });
|
|
520
|
+
const diagnostics = [
|
|
521
|
+
...checkpointDiagnostics(input, view),
|
|
522
|
+
...stageReadinessDiagnostics(db, input, scope),
|
|
523
|
+
];
|
|
524
|
+
let result = null;
|
|
525
|
+
|
|
526
|
+
if (input.stage === 'plan'
|
|
527
|
+
&& !diagnostics.some((item) => item.severity !== 'warning')) {
|
|
528
|
+
try {
|
|
529
|
+
result = planCheckpoint.publishPlan(db, {
|
|
530
|
+
change_id: scope.change_id,
|
|
531
|
+
context,
|
|
532
|
+
}, { rootDir, markRecoveryRequired: markPlanRecoveryRequired });
|
|
533
|
+
} catch (error) {
|
|
534
|
+
if (isHardError(error)) throw error;
|
|
535
|
+
diagnostics.push(diagnostic(error));
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const draft = stageCheckpoints.saveDraft(db, {
|
|
540
|
+
stage: input.stage,
|
|
541
|
+
scope: scope.task_id
|
|
542
|
+
? { task_id: scope.task_id }
|
|
543
|
+
: scope.change_id
|
|
544
|
+
? { change_id: scope.change_id }
|
|
545
|
+
: scope.baseline_id
|
|
546
|
+
? { baseline_id: scope.baseline_id }
|
|
547
|
+
: { project_id: 'project' },
|
|
548
|
+
payload: input.stage === 'plan' && result
|
|
549
|
+
? { ...(input.payload || {}), plan: result }
|
|
550
|
+
: (input.payload || {}),
|
|
551
|
+
evidence: Array.isArray(input.payload?.evidence) ? input.payload.evidence : [],
|
|
552
|
+
diagnostics,
|
|
553
|
+
context_envelope_id: context.id,
|
|
554
|
+
idempotency_key: `${input.idempotency_key}:draft`,
|
|
555
|
+
});
|
|
556
|
+
if (diagnostics.some((item) => item.severity !== 'warning')) {
|
|
557
|
+
return {
|
|
558
|
+
accepted: false,
|
|
559
|
+
mutable: true,
|
|
560
|
+
diagnostics,
|
|
561
|
+
blockers: diagnostics.map((item) => item.code),
|
|
562
|
+
checkpoint: draft,
|
|
563
|
+
context,
|
|
564
|
+
result,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
const accepted = stageCheckpoints.acceptDraft(db, {
|
|
568
|
+
id: draft.id,
|
|
569
|
+
idempotency_key: `${input.idempotency_key}:accept`,
|
|
570
|
+
});
|
|
571
|
+
if (input.stage === 'plan') {
|
|
572
|
+
result = {
|
|
573
|
+
...result,
|
|
574
|
+
team_checkpoint: taskLedger.publishTaskLedger(db, {
|
|
575
|
+
rootDir,
|
|
576
|
+
reason: 'plan_checkpoint_accepted',
|
|
577
|
+
}),
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
return {
|
|
581
|
+
accepted: true,
|
|
582
|
+
mutable: false,
|
|
583
|
+
diagnostics,
|
|
584
|
+
blockers: [],
|
|
585
|
+
checkpoint: accepted,
|
|
586
|
+
context,
|
|
587
|
+
result,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function sync(db, input = {}, { rootDir = process.cwd() } = {}) {
|
|
592
|
+
gitBootstrap.ensureExistingProjectStorageBoundary(rootDir);
|
|
593
|
+
if (input.action === 'inspect') {
|
|
594
|
+
try {
|
|
595
|
+
return taskLedger.inspectTaskLedger(db, { rootDir });
|
|
596
|
+
} catch (error) {
|
|
597
|
+
return {
|
|
598
|
+
status: 'migration_required',
|
|
599
|
+
migration: {
|
|
600
|
+
required: true,
|
|
601
|
+
code: errorCode(error),
|
|
602
|
+
action: 'ultra.sync migrate',
|
|
603
|
+
},
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (input.action === 'import') return taskLedger.importTaskLedger(db, { rootDir });
|
|
608
|
+
if (input.action === 'migrate') {
|
|
609
|
+
const published = taskLedger.publishTaskLedger(db, {
|
|
610
|
+
rootDir,
|
|
611
|
+
reason: 'legacy_authority_migrated',
|
|
612
|
+
});
|
|
613
|
+
return {
|
|
614
|
+
migrated: Boolean(published.migrated_legacy_projection),
|
|
615
|
+
...published,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
if (input.action === 'publish') {
|
|
619
|
+
const cached = priorResult(db, input.idempotency_key, 'sync:publish');
|
|
620
|
+
if (cached) return { ...cached, idempotent: true };
|
|
621
|
+
const result = taskLedger.publishTaskLedger(db, {
|
|
622
|
+
rootDir,
|
|
623
|
+
reason: input.reason || 'manual_checkpoint',
|
|
624
|
+
});
|
|
625
|
+
rememberResult(db, {
|
|
626
|
+
idempotencyKey: input.idempotency_key,
|
|
627
|
+
operation: 'sync:publish',
|
|
628
|
+
result,
|
|
629
|
+
});
|
|
630
|
+
return result;
|
|
631
|
+
}
|
|
632
|
+
throw Object.assign(new Error(`unsupported sync action: ${input.action}`), {
|
|
633
|
+
code: 'VALIDATION_ERROR',
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function sessionDiagnostic(error) {
|
|
638
|
+
if (isHardError(error)) throw error;
|
|
639
|
+
return {
|
|
640
|
+
accepted: false,
|
|
641
|
+
mutable: true,
|
|
642
|
+
can_acquire: false,
|
|
643
|
+
diagnostics: [diagnostic(error)],
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
async function session(db, input = {}, {
|
|
648
|
+
rootDir = process.cwd(),
|
|
649
|
+
runtime = 'unknown',
|
|
650
|
+
sessionId = null,
|
|
651
|
+
} = {}) {
|
|
652
|
+
const scope = input.scope || {};
|
|
653
|
+
const payload = input.payload || {};
|
|
654
|
+
if (input.action === 'get') {
|
|
655
|
+
return { session: ops.readSession(db, scope.sid) };
|
|
656
|
+
}
|
|
657
|
+
if (input.action === 'list') {
|
|
658
|
+
const sessions = ops.listActiveSessions(db, { task_id: scope.task_id });
|
|
659
|
+
return { sessions, count: sessions.length };
|
|
660
|
+
}
|
|
661
|
+
if (input.action === 'heartbeat') return ops.heartbeatSession(db, scope.sid);
|
|
662
|
+
if (input.action === 'release') {
|
|
663
|
+
if (sessionId) {
|
|
664
|
+
return sessionDiagnostic(Object.assign(
|
|
665
|
+
new Error('a worker session is settled by its parent host'),
|
|
666
|
+
{ code: 'WORKER_SESSION_PARENT_OWNED' },
|
|
667
|
+
));
|
|
668
|
+
}
|
|
669
|
+
return sessionRunner.closeSession(
|
|
670
|
+
{ db, repoRoot: rootDir, sid: scope.sid },
|
|
671
|
+
{
|
|
672
|
+
status: payload.status || 'completed',
|
|
673
|
+
remove_worktree: payload.remove_worktree === true,
|
|
674
|
+
},
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
const taskId = scope.task_id;
|
|
678
|
+
if (input.action === 'admission') {
|
|
679
|
+
try {
|
|
680
|
+
const task = ops.readTask(db, taskId);
|
|
681
|
+
if (!task) throw Object.assign(new Error(`task ${taskId} not found`), { code: 'TASK_NOT_FOUND' });
|
|
682
|
+
const attention = ops.taskContractBlockers(task).map((code) => ({
|
|
683
|
+
code,
|
|
684
|
+
severity: 'needs_attention',
|
|
685
|
+
}));
|
|
686
|
+
const lease = ops.admissionCheck(db, taskId);
|
|
687
|
+
return {
|
|
688
|
+
accepted: attention.length === 0 && lease.can_spawn,
|
|
689
|
+
mutable: true,
|
|
690
|
+
can_acquire: attention.length === 0 && lease.can_spawn,
|
|
691
|
+
diagnostics: [
|
|
692
|
+
...attention,
|
|
693
|
+
...(lease.can_spawn ? [] : [{
|
|
694
|
+
code: 'ACTIVE_SESSION_LEASE_CONFLICT',
|
|
695
|
+
severity: 'hard_conflict',
|
|
696
|
+
details: lease.conflict,
|
|
697
|
+
}]),
|
|
698
|
+
],
|
|
699
|
+
lease,
|
|
700
|
+
};
|
|
701
|
+
} catch (error) {
|
|
702
|
+
return sessionDiagnostic(error);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (input.action !== 'acquire') {
|
|
706
|
+
throw Object.assign(new Error(`unsupported session action: ${input.action}`), {
|
|
707
|
+
code: 'VALIDATION_ERROR',
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
let packet = null;
|
|
711
|
+
try {
|
|
712
|
+
packet = workerPacket.createWorkerPacket(db, {
|
|
713
|
+
role: payload.role || 'implement',
|
|
714
|
+
task_id: taskId,
|
|
715
|
+
runtime: payload.runtime || runtime,
|
|
716
|
+
output_path: payload.output_path
|
|
717
|
+
|| `.ultra/changes/active/${ops.readTask(db, taskId)?.change_id}/delivery/${taskId}-outcome.json`,
|
|
718
|
+
output_schema: payload.output_schema,
|
|
719
|
+
evidence_refs: payload.evidence_refs,
|
|
720
|
+
diff_range: payload.diff_range,
|
|
721
|
+
changed_files: payload.changed_files,
|
|
722
|
+
}, { rootDir });
|
|
723
|
+
const handle = sessionRunner.spawnSession({
|
|
724
|
+
db,
|
|
725
|
+
repoRoot: rootDir,
|
|
726
|
+
task_id: taskId,
|
|
727
|
+
runtime: payload.runtime || runtime,
|
|
728
|
+
takeover: payload.takeover === true,
|
|
729
|
+
worktree_base: payload.worktree_base,
|
|
730
|
+
kernel_mode: true,
|
|
731
|
+
mark_task_started: true,
|
|
732
|
+
packet_digest: packet.packet_digest,
|
|
733
|
+
});
|
|
734
|
+
workerPacket.markWorkerPacketAssigned(db, packet.id);
|
|
735
|
+
return {
|
|
736
|
+
accepted: true,
|
|
737
|
+
sid: handle.sid,
|
|
738
|
+
worktree_path: handle.worktree_path,
|
|
739
|
+
artifact_dir: handle.artifact_dir,
|
|
740
|
+
lease_expires_at: handle.lease_expires_at,
|
|
741
|
+
packet,
|
|
742
|
+
};
|
|
743
|
+
} catch (error) {
|
|
744
|
+
if (packet?.id) {
|
|
745
|
+
workerPacket.abandonWorkerPacket(db, packet.id, error.code || error.message);
|
|
746
|
+
}
|
|
747
|
+
return sessionDiagnostic(error);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
async function archive(db, input = {}, {
|
|
752
|
+
rootDir = process.cwd(),
|
|
753
|
+
} = {}) {
|
|
754
|
+
const cached = priorResult(db, input.idempotency_key, 'archive');
|
|
755
|
+
if (cached) return { ...cached, idempotent: true };
|
|
756
|
+
const change = changes.readChange(db, input.change_id);
|
|
757
|
+
if (!change) {
|
|
758
|
+
return {
|
|
759
|
+
accepted: false,
|
|
760
|
+
mutable: true,
|
|
761
|
+
blockers: ['CHANGE_NOT_FOUND'],
|
|
762
|
+
diagnostics: [{
|
|
763
|
+
code: 'CHANGE_NOT_FOUND',
|
|
764
|
+
severity: 'needs_attention',
|
|
765
|
+
message: `change ${input.change_id} not found`,
|
|
766
|
+
}],
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
try {
|
|
770
|
+
const result = changes.archiveChange(
|
|
771
|
+
db,
|
|
772
|
+
{ id: input.change_id, ...(input.payload || {}) },
|
|
773
|
+
{ rootDir, kernelMode: true },
|
|
774
|
+
);
|
|
775
|
+
const output = {
|
|
776
|
+
accepted: true,
|
|
777
|
+
mutable: false,
|
|
778
|
+
blockers: [],
|
|
779
|
+
diagnostics: [],
|
|
780
|
+
result,
|
|
781
|
+
team_checkpoint: taskLedger.publishTaskLedger(db, {
|
|
782
|
+
rootDir,
|
|
783
|
+
reason: 'change_archived',
|
|
784
|
+
}),
|
|
785
|
+
};
|
|
786
|
+
rememberResult(db, {
|
|
787
|
+
idempotencyKey: input.idempotency_key,
|
|
788
|
+
operation: 'archive',
|
|
789
|
+
result: output,
|
|
790
|
+
changeId: input.change_id,
|
|
791
|
+
});
|
|
792
|
+
return output;
|
|
793
|
+
} catch (error) {
|
|
794
|
+
if (isHardError(error)) throw error;
|
|
795
|
+
return {
|
|
796
|
+
accepted: false,
|
|
797
|
+
mutable: true,
|
|
798
|
+
blockers: [errorCode(error)],
|
|
799
|
+
diagnostics: [diagnostic(error)],
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
async function dispatch(name, input, db, context = {}) {
|
|
805
|
+
if (name === 'ultra.context') return readContext(db, input, context);
|
|
806
|
+
if (name === 'ultra.record') return record(db, input, context);
|
|
807
|
+
if (name === 'ultra.checkpoint') return checkpoint(db, input, context);
|
|
808
|
+
if (name === 'ultra.sync') return sync(db, input, context);
|
|
809
|
+
if (name === 'ultra.session') return session(db, input, context);
|
|
810
|
+
if (name === 'ultra.archive') return archive(db, input, context);
|
|
811
|
+
if (name === 'ultra.doctor') {
|
|
812
|
+
return doctor.runDoctor(db, {
|
|
813
|
+
rootDir: context.rootDir || process.cwd(),
|
|
814
|
+
repair: input.repair === true,
|
|
815
|
+
project: context.projector,
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
const error = new Error(`unhandled public tool ${name}`);
|
|
819
|
+
error.code = 'UNKNOWN_TOOL';
|
|
820
|
+
throw error;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
module.exports = {
|
|
824
|
+
PUBLIC_TOOLS,
|
|
825
|
+
RECORD_KINDS,
|
|
826
|
+
dispatch,
|
|
827
|
+
readContext,
|
|
828
|
+
record,
|
|
829
|
+
checkpoint,
|
|
830
|
+
sync,
|
|
831
|
+
session,
|
|
832
|
+
archive,
|
|
833
|
+
isHardError,
|
|
834
|
+
};
|