ultra-builder-pro-cli 0.23.0 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ultra-template/docs/research/README.md +4 -4
- package/.ultra-template/tasks/tasks.json +4 -2
- package/CHANGELOG.md +72 -0
- package/README.md +55 -39
- package/adapters/_shared/codex-assets.cjs +121 -65
- package/adapters/_shared/interaction-contract.cjs +7 -1
- package/adapters/_shared/provenance.cjs +7 -2
- package/adapters/_shared/runtime-assets.cjs +3 -2
- package/adapters/_shared/runtime-native.cjs +132 -0
- package/adapters/claude.js +60 -30
- package/adapters/codex.js +178 -81
- package/adapters/grok.js +372 -0
- package/adapters/kimi.js +7 -3
- package/adapters/opencode.js +210 -32
- package/agents/code-reviewer.md +4 -2
- package/agents/debugger.md +4 -1
- package/agents/review-code.md +7 -3
- package/agents/review-comments.md +7 -4
- package/agents/review-coordinator.md +11 -7
- package/agents/review-design.md +8 -4
- package/agents/review-errors.md +8 -4
- package/agents/review-spec.md +8 -5
- package/agents/review-tests.md +7 -4
- package/agents/tdd-runner.md +5 -2
- package/bin/install.js +9 -4
- package/bin/orchestrator.js +4 -13
- package/docs/AGENT-CONTEXT.md +143 -229
- package/docs/ARCHITECTURE.md +205 -494
- package/docs/ARTIFACT-AUTHORITY.md +35 -28
- package/docs/LEGACY-CLI-CROSSWALK.md +1 -1
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +12 -9
- package/docs/ROADMAP.md +85 -110
- package/docs/RUNTIME-COMPAT-MATRIX.md +139 -175
- package/docs/SKILL-AUTHORING.md +3 -3
- package/docs/STATE-DB-ACCESS-POLICY.md +13 -8
- package/docs/WORKFLOW-LIFECYCLE.md +189 -448
- package/hooks/README.md +11 -8
- package/hooks/active_task_context.py +16 -16
- package/hooks/adapters/grok.py +134 -0
- package/hooks/{context_spine.py → context_envelope.py} +26 -22
- package/hooks/health_check.py +29 -34
- package/hooks/pre_stop_check.py +22 -12
- package/hooks/subagent_tracker.py +1 -1
- package/hooks/workflow_checkpoint.py +14 -13
- package/hooks/workflow_context.py +11 -12
- package/hooks/workflow_resume.py +14 -13
- package/mcp-server/{breadcrumb.cjs → hook-context.cjs} +7 -9
- package/mcp-server/lib/baseline-workflow.cjs +108 -82
- package/mcp-server/lib/canonical-json.cjs +19 -0
- package/mcp-server/lib/change-authority.cjs +26 -0
- package/mcp-server/lib/change-workflow.cjs +630 -45
- 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/state-ops.cjs +21 -3
- package/mcp-server/lib/task-ledger.cjs +522 -13
- package/mcp-server/lib/ultra-facade.cjs +757 -442
- package/mcp-server/lib/worker-packet.cjs +358 -0
- package/mcp-server/server.cjs +200 -1103
- package/orchestrator/daemon.cjs +27 -9
- package/orchestrator/parallel-orchestrator.cjs +111 -76
- package/orchestrator/session-runner.cjs +135 -34
- package/package.json +10 -3
- package/skills/ultra-change/SKILL.md +10 -8
- package/skills/ultra-deliver/SKILL.md +17 -19
- package/skills/ultra-dev/SKILL.md +20 -11
- package/skills/ultra-doctor/SKILL.md +4 -4
- package/skills/ultra-init/SKILL.md +6 -5
- package/skills/ultra-init/references/brownfield-adoption.md +20 -14
- package/skills/ultra-plan/SKILL.md +13 -9
- package/skills/ultra-research/SKILL.md +18 -9
- package/skills/ultra-review/SKILL.md +14 -9
- package/skills/ultra-review/references/unified-schema.md +2 -0
- package/skills/ultra-review/scripts/review_wait.py +8 -0
- package/skills/ultra-status/SKILL.md +2 -2
- package/skills/ultra-test/SKILL.md +10 -6
- package/skills/ultra-think/SKILL.md +8 -7
- package/skills/ultra-think/references/interaction-boundary.md +56 -0
- package/spec/cli-protocol.md +80 -199
- package/spec/mcp-tools.yaml +43 -2806
- package/spec/schemas/state-db.sql +107 -4
- package/spec/schemas/{task-ledger.v1.schema.json → task-ledger.v2.schema.json} +70 -6
- package/ultra-tools/commands/status.cjs +369 -243
- package/mcp-server/lib/context-spine.cjs +0 -1208
- package/mcp-server/lib/decision-dialogue.cjs +0 -1128
- package/mcp-server/lib/project-breadcrumb.cjs +0 -173
- package/mcp-server/lib/spec-learning.cjs +0 -284
- package/mcp-server/lib/workflow-state.cjs +0 -3129
- package/skills/ultra-think/references/decision-dialogue.md +0 -155
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const fs = require('node:fs');
|
|
3
4
|
const path = require('node:path');
|
|
4
5
|
|
|
5
|
-
const
|
|
6
|
+
const artifactRegistry = require('./artifact-registry.cjs');
|
|
6
7
|
const baselines = require('./baseline-workflow.cjs');
|
|
7
8
|
const changes = require('./change-workflow.cjs');
|
|
8
|
-
const
|
|
9
|
+
const contextEnvelope = require('./context-envelope.cjs');
|
|
10
|
+
const decisionRecords = require('./decision-records.cjs');
|
|
9
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');
|
|
10
18
|
const taskLedger = require('./task-ledger.cjs');
|
|
19
|
+
const workerPacket = require('./worker-packet.cjs');
|
|
20
|
+
const sessionRunner = require('../../orchestrator/session-runner.cjs');
|
|
11
21
|
|
|
12
22
|
const PUBLIC_TOOLS = Object.freeze([
|
|
13
23
|
'ultra.context',
|
|
@@ -19,82 +29,84 @@ const PUBLIC_TOOLS = Object.freeze([
|
|
|
19
29
|
'ultra.doctor',
|
|
20
30
|
]);
|
|
21
31
|
|
|
22
|
-
const
|
|
23
|
-
'
|
|
24
|
-
'
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'baseline.converge',
|
|
31
|
-
'change.create',
|
|
32
|
-
'change.update',
|
|
33
|
-
'change.delta',
|
|
34
|
-
'change.documentation_reconcile',
|
|
35
|
-
'change.context',
|
|
36
|
-
'change.learning_propose',
|
|
37
|
-
'change.learning_resolve',
|
|
38
|
-
'decision.thread_start',
|
|
39
|
-
'decision.open',
|
|
40
|
-
'decision.resolve',
|
|
41
|
-
'decision.delegate',
|
|
42
|
-
'decision.defer',
|
|
43
|
-
'decision.supersede',
|
|
44
|
-
'decision.complete',
|
|
45
|
-
'decision.checkpoint',
|
|
46
|
-
'artifact.record',
|
|
32
|
+
const RECORD_KINDS = Object.freeze([
|
|
33
|
+
'baseline',
|
|
34
|
+
'change_contract',
|
|
35
|
+
'decision',
|
|
36
|
+
'task_contract',
|
|
37
|
+
'task_outcome',
|
|
38
|
+
'artifact',
|
|
39
|
+
'event',
|
|
47
40
|
]);
|
|
48
41
|
|
|
49
|
-
const STAGE_CONTEXT = Object.freeze({
|
|
50
|
-
plan: { role: 'plan', gate: 'planning' },
|
|
51
|
-
dev: { role: 'implement', gate: 'implementation' },
|
|
52
|
-
test: { role: 'check', gate: 'verification' },
|
|
53
|
-
review: { role: 'review', gate: 'review' },
|
|
54
|
-
deliver: { role: 'check', gate: 'convergence' },
|
|
55
|
-
});
|
|
56
|
-
|
|
57
42
|
const HARD_ERROR_CODES = new Set([
|
|
58
|
-
'STATE_CORRUPT',
|
|
59
|
-
'STATE_DB_ERROR',
|
|
60
|
-
'STATE_DB_MISSING',
|
|
61
|
-
'SCHEMA_VERSION_MISMATCH',
|
|
62
|
-
'PATH_AUTHORITY_VIOLATION',
|
|
63
43
|
'ARCHIVE_PATH_UNSAFE',
|
|
44
|
+
'ARCHIVE_RECOVERY_REQUIRED',
|
|
64
45
|
'ARCHIVE_RUNTIME_UNAVAILABLE',
|
|
65
|
-
'PLAN_RECOVERY_REQUIRED',
|
|
66
46
|
'BACKUP_FAILED',
|
|
47
|
+
'CHECKPOINT_DIGEST_MISMATCH',
|
|
48
|
+
'CHECKPOINT_EVIDENCE_AUTHORITY_MISSING',
|
|
49
|
+
'CHECKPOINT_EVIDENCE_DIGEST_MISMATCH',
|
|
50
|
+
'CHECKPOINT_SCOPE_MISMATCH',
|
|
51
|
+
'CHECKPOINT_SCOPE_NOT_FOUND',
|
|
52
|
+
'CONTEXT_ENVELOPE_LIMIT_EXCEEDED',
|
|
53
|
+
'CONTEXT_ENVELOPE_DIGEST_MISMATCH',
|
|
54
|
+
'CONTEXT_ENVELOPE_FILE_DRIFT',
|
|
55
|
+
'DECISION_DIGEST_MISMATCH',
|
|
56
|
+
'DECISION_FILE_DRIFT',
|
|
57
|
+
'DECISION_ID_CONFLICT',
|
|
58
|
+
'DECISION_SCOPE_CONFLICT',
|
|
67
59
|
'OUTPUT_SCHEMA_DRIFT',
|
|
60
|
+
'PATH_AUTHORITY_VIOLATION',
|
|
61
|
+
'PLAN_RECOVERY_REQUIRED',
|
|
62
|
+
'RUNTIME_ABI_MISMATCH',
|
|
63
|
+
'RUNTIME_NATIVE_MISSING',
|
|
64
|
+
'SCHEMA_VERSION_MISMATCH',
|
|
65
|
+
'SESSION_STATUS_CONFLICT',
|
|
66
|
+
'STATE_CORRUPT',
|
|
67
|
+
'STATE_DB_ERROR',
|
|
68
|
+
'STATE_DB_MISSING',
|
|
69
|
+
'TASK_LEDGER_BASELINE_CONFLICT',
|
|
70
|
+
'TASK_LEDGER_CHANGE_CONFLICT',
|
|
71
|
+
'TASK_LEDGER_HISTORY_CONFLICT',
|
|
72
|
+
'TASK_LEDGER_TASK_CONFLICT',
|
|
73
|
+
'WORKER_PACKET_DIGEST_MISMATCH',
|
|
74
|
+
'WORKER_PACKET_FILE_DRIFT',
|
|
68
75
|
]);
|
|
69
76
|
|
|
70
|
-
function parseJson(value, fallback = null) {
|
|
71
|
-
try { return value == null ? fallback : JSON.parse(value); }
|
|
72
|
-
catch { return fallback; }
|
|
73
|
-
}
|
|
74
|
-
|
|
75
77
|
function errorCode(error) {
|
|
76
|
-
return String(error?.code || error?.name || '
|
|
78
|
+
return String(error?.code || error?.name || 'NEEDS_ATTENTION');
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
function isHardError(error) {
|
|
80
82
|
const code = errorCode(error);
|
|
81
83
|
return HARD_ERROR_CODES.has(code)
|
|
82
84
|
|| code.startsWith('PATH_')
|
|
85
|
+
|| code.startsWith('SQLITE_')
|
|
83
86
|
|| code.startsWith('STATE_CORRUPT')
|
|
84
|
-
|| code.startsWith('
|
|
87
|
+
|| code.startsWith('TASK_LEDGER_') && code.endsWith('_CONFLICT')
|
|
88
|
+
|| code.startsWith('SESSION_CLOSE_') && code.endsWith('_CONFLICT')
|
|
89
|
+
|| code.startsWith('WORKTREE_') && (
|
|
90
|
+
code.endsWith('_UNSAFE') || code.endsWith('_CONFLICT')
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function parseJson(value, fallback = null) {
|
|
95
|
+
try { return value == null ? fallback : JSON.parse(value); }
|
|
96
|
+
catch { return fallback; }
|
|
85
97
|
}
|
|
86
98
|
|
|
87
|
-
function
|
|
88
|
-
if (!idempotencyKey) return null;
|
|
99
|
+
function priorResult(db, idempotencyKey, operation) {
|
|
100
|
+
if (!db || !idempotencyKey) return null;
|
|
89
101
|
const rows = db.prepare(
|
|
90
102
|
`SELECT payload_json FROM events
|
|
91
|
-
WHERE type = '
|
|
103
|
+
WHERE type = 'ultra_kernel_call'
|
|
92
104
|
ORDER BY id DESC LIMIT 2000`,
|
|
93
105
|
).all();
|
|
94
106
|
for (const row of rows) {
|
|
95
107
|
const payload = parseJson(row.payload_json, {});
|
|
96
108
|
if (payload.idempotency_key === idempotencyKey
|
|
97
|
-
&&
|
|
109
|
+
&& payload.operation === operation
|
|
98
110
|
&& payload.accepted === true) {
|
|
99
111
|
return payload.result;
|
|
100
112
|
}
|
|
@@ -102,15 +114,16 @@ function priorFacadeResult(db, idempotencyKey, operation = null) {
|
|
|
102
114
|
return null;
|
|
103
115
|
}
|
|
104
116
|
|
|
105
|
-
function
|
|
117
|
+
function rememberResult(db, {
|
|
106
118
|
idempotencyKey,
|
|
107
119
|
operation,
|
|
108
120
|
result,
|
|
109
121
|
changeId = null,
|
|
110
122
|
taskId = null,
|
|
111
123
|
}) {
|
|
124
|
+
if (!db || !idempotencyKey) return;
|
|
112
125
|
ops.appendEvent(db, {
|
|
113
|
-
type: '
|
|
126
|
+
type: 'ultra_kernel_call',
|
|
114
127
|
change_id: changeId,
|
|
115
128
|
task_id: taskId,
|
|
116
129
|
payload: {
|
|
@@ -122,197 +135,298 @@ function rememberFacadeResult(db, {
|
|
|
122
135
|
});
|
|
123
136
|
}
|
|
124
137
|
|
|
125
|
-
function
|
|
126
|
-
return
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
138
|
+
function diagnostic(error, severity = 'needs_attention') {
|
|
139
|
+
return {
|
|
140
|
+
code: errorCode(error),
|
|
141
|
+
severity,
|
|
142
|
+
message: error.message,
|
|
143
|
+
...(error.details === undefined ? {} : { details: error.details }),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function selectedScope(db, input = {}) {
|
|
148
|
+
const scope = input.scope || {};
|
|
149
|
+
const task = scope.task_id ? ops.readTask(db, scope.task_id) : null;
|
|
150
|
+
if (scope.task_id && !task) {
|
|
151
|
+
throw Object.assign(
|
|
152
|
+
new Error(`checkpoint task scope does not exist: ${scope.task_id}`),
|
|
153
|
+
{
|
|
154
|
+
code: 'CHECKPOINT_SCOPE_NOT_FOUND',
|
|
155
|
+
details: { scope_type: 'task', scope_id: scope.task_id },
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
const changeId = scope.change_id || task?.change_id || null;
|
|
160
|
+
const change = changeId ? changes.readChange(db, changeId) : null;
|
|
161
|
+
if (changeId && !change) {
|
|
162
|
+
throw Object.assign(
|
|
163
|
+
new Error(`checkpoint Change scope does not exist: ${changeId}`),
|
|
164
|
+
{
|
|
165
|
+
code: 'CHECKPOINT_SCOPE_NOT_FOUND',
|
|
166
|
+
details: { scope_type: 'change', scope_id: changeId },
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
if (task && scope.change_id && task.change_id !== scope.change_id) {
|
|
171
|
+
throw Object.assign(
|
|
172
|
+
new Error(
|
|
173
|
+
`checkpoint task ${task.id} belongs to Change ${task.change_id || '(none)'}, not ${scope.change_id}`,
|
|
174
|
+
),
|
|
175
|
+
{
|
|
176
|
+
code: 'CHECKPOINT_SCOPE_MISMATCH',
|
|
177
|
+
details: {
|
|
178
|
+
task_id: task.id,
|
|
179
|
+
task_change_id: task.change_id || null,
|
|
180
|
+
requested_change_id: scope.change_id,
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
if (task) {
|
|
186
|
+
return {
|
|
187
|
+
...(changeId ? { change_id: changeId } : {}),
|
|
188
|
+
task_id: task.id,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
if (change) return { change_id: change.id };
|
|
192
|
+
const baseline = baselines.readBaseline(db);
|
|
193
|
+
return baseline ? { baseline_id: baseline.id } : { project_id: 'project' };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function readContext(db, input = {}, {
|
|
197
|
+
rootDir = process.cwd(),
|
|
198
|
+
runtime = 'unknown',
|
|
199
|
+
} = {}) {
|
|
200
|
+
return contextEnvelope.buildEnvelope(db, input, { rootDir, runtime });
|
|
131
201
|
}
|
|
132
202
|
|
|
133
|
-
function
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
203
|
+
function initializeProject(data = {}) {
|
|
204
|
+
const targetDir = path.resolve(data.target_dir || process.cwd());
|
|
205
|
+
const alreadyInitialized = fs.existsSync(path.join(targetDir, '.ultra'));
|
|
206
|
+
return initProject({
|
|
207
|
+
...data,
|
|
208
|
+
target_dir: targetDir,
|
|
209
|
+
resume: alreadyInitialized || data.resume === true,
|
|
210
|
+
});
|
|
140
211
|
}
|
|
141
212
|
|
|
142
|
-
function
|
|
143
|
-
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
: `Record the ${stage} stage for ${scope.task_id || scope.change_id || 'this project'}.`,
|
|
150
|
-
baseline_id: currentBaselineId(db),
|
|
151
|
-
change_id: scope.change_id || undefined,
|
|
152
|
-
task_id: scope.task_id || undefined,
|
|
153
|
-
metadata: { public_facade: true },
|
|
154
|
-
};
|
|
155
|
-
if (stage === 'research') {
|
|
156
|
-
input.mode = payload.mode || 'custom';
|
|
157
|
-
if (payload.coverage !== undefined) input.coverage = payload.coverage;
|
|
158
|
-
if (payload.selected_steps !== undefined) input.selected_steps = payload.selected_steps;
|
|
213
|
+
function baselineRecord(db, action, data, rootDir) {
|
|
214
|
+
if (action === 'initialize') return initializeProject(data);
|
|
215
|
+
if (action === 'start') {
|
|
216
|
+
return { baseline: baselines.startBaseline(db, data, { rootDir }) };
|
|
217
|
+
}
|
|
218
|
+
if (action === 'observe' || action === 'revise') {
|
|
219
|
+
return { baseline: baselines.recordBaseline(db, data, { rootDir }) };
|
|
159
220
|
}
|
|
160
|
-
|
|
221
|
+
if (action === 'accept') {
|
|
222
|
+
const result = baselines.convergeBaseline(db, data, { rootDir });
|
|
223
|
+
return {
|
|
224
|
+
...result,
|
|
225
|
+
diagnostics: [
|
|
226
|
+
...(result.blockers || []).map((code) => ({
|
|
227
|
+
code,
|
|
228
|
+
severity: 'needs_attention',
|
|
229
|
+
})),
|
|
230
|
+
...(result.warnings || []).map((code) => ({
|
|
231
|
+
code,
|
|
232
|
+
severity: 'warning',
|
|
233
|
+
})),
|
|
234
|
+
],
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
throw Object.assign(new Error(`unsupported baseline action: ${action}`), {
|
|
238
|
+
code: 'VALIDATION_ERROR',
|
|
239
|
+
});
|
|
161
240
|
}
|
|
162
241
|
|
|
163
|
-
function
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
242
|
+
function changeContractRecord(db, action, data, rootDir) {
|
|
243
|
+
if (action === 'open') {
|
|
244
|
+
return changes.createChange(db, data, { rootDir, kernelMode: true });
|
|
245
|
+
}
|
|
246
|
+
if (action === 'revise') {
|
|
247
|
+
return {
|
|
248
|
+
change: changes.updateChange(
|
|
249
|
+
db,
|
|
250
|
+
data.id,
|
|
251
|
+
data.patch || {},
|
|
252
|
+
{ rootDir, kernelMode: true },
|
|
253
|
+
),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
if (action === 'cancel') {
|
|
257
|
+
return {
|
|
258
|
+
change: changes.updateChange(
|
|
259
|
+
db,
|
|
260
|
+
data.id,
|
|
261
|
+
{ status: 'cancelled' },
|
|
262
|
+
{ rootDir, kernelMode: true },
|
|
263
|
+
),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
throw Object.assign(new Error(`unsupported change_contract action: ${action}`), {
|
|
267
|
+
code: 'VALIDATION_ERROR',
|
|
268
|
+
});
|
|
168
269
|
}
|
|
169
270
|
|
|
170
|
-
function
|
|
171
|
-
|
|
172
|
-
if (
|
|
173
|
-
return
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
gate: expectation.gate,
|
|
178
|
-
...(Array.isArray(input.context_refs) ? { context_refs: input.context_refs } : {}),
|
|
179
|
-
...(input.budget && typeof input.budget === 'object' ? { budget: input.budget } : {}),
|
|
180
|
-
}, { rootDir });
|
|
271
|
+
function taskContractRecord(db, action, data) {
|
|
272
|
+
if (action === 'define') return { task: ops.createTask(db, data) };
|
|
273
|
+
if (action === 'revise') return { task: ops.patchTask(db, data.id, data.patch || {}) };
|
|
274
|
+
if (action === 'remove') return ops.deleteTask(db, data.id, { force: data.force === true });
|
|
275
|
+
throw Object.assign(new Error(`unsupported task_contract action: ${action}`), {
|
|
276
|
+
code: 'VALIDATION_ERROR',
|
|
277
|
+
});
|
|
181
278
|
}
|
|
182
279
|
|
|
183
|
-
function
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
280
|
+
function taskOutcomeRecord(db, action, data, rootDir) {
|
|
281
|
+
const status = {
|
|
282
|
+
start: 'in_progress',
|
|
283
|
+
complete: 'completed',
|
|
284
|
+
block: 'blocked',
|
|
285
|
+
reopen: 'in_progress',
|
|
286
|
+
}[action];
|
|
287
|
+
if (!status) {
|
|
288
|
+
throw Object.assign(new Error(`unsupported task_outcome action: ${action}`), {
|
|
289
|
+
code: 'VALIDATION_ERROR',
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
if (action === 'complete') {
|
|
293
|
+
const packet = db.prepare(
|
|
294
|
+
`SELECT id, packet_digest, packet_path, output_path
|
|
295
|
+
FROM worker_packets
|
|
296
|
+
WHERE scope_type = 'task' AND scope_id = ? AND status = 'assigned'
|
|
297
|
+
ORDER BY created_at DESC, rowid DESC LIMIT 1`,
|
|
298
|
+
).get(data.id);
|
|
299
|
+
if (!packet) {
|
|
300
|
+
throw Object.assign(
|
|
301
|
+
new Error(`task ${data.id} has no assigned Worker Packet`),
|
|
302
|
+
{ code: 'WORKER_PACKET_REQUIRED' },
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
const packetDocument = workerPacket.readWorkerPacket(
|
|
194
306
|
db,
|
|
195
|
-
|
|
307
|
+
packet.id,
|
|
196
308
|
{ rootDir },
|
|
197
|
-
)
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
`SELECT * FROM context_snapshots
|
|
204
|
-
WHERE change_id = ? AND (? IS NULL OR task_id = ?)
|
|
205
|
-
AND role = ? AND gate = ?
|
|
206
|
-
ORDER BY created_at DESC, rowid DESC LIMIT 1`,
|
|
207
|
-
).get(
|
|
208
|
-
scope.change_id,
|
|
209
|
-
scope.task_id || null,
|
|
210
|
-
scope.task_id || null,
|
|
211
|
-
expectation.role,
|
|
212
|
-
expectation.gate,
|
|
213
|
-
)
|
|
214
|
-
: null;
|
|
215
|
-
const context = snapshot
|
|
216
|
-
? {
|
|
217
|
-
snapshot_id: snapshot.id,
|
|
218
|
-
manifest_path: snapshot.manifest_path,
|
|
219
|
-
manifest_hash: snapshot.manifest_hash,
|
|
220
|
-
readiness: snapshot.readiness,
|
|
221
|
-
blockers: parseJson(snapshot.blockers_json, []),
|
|
222
|
-
spine: parseJson(snapshot.context_json, {}),
|
|
309
|
+
);
|
|
310
|
+
if (data.packet_digest !== packet.packet_digest) {
|
|
311
|
+
throw Object.assign(
|
|
312
|
+
new Error('task outcome does not reference the exact assigned Worker Packet'),
|
|
313
|
+
{ code: 'WORKER_PACKET_DIGEST_MISMATCH' },
|
|
314
|
+
);
|
|
223
315
|
}
|
|
224
|
-
:
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
316
|
+
const output = readStableProjectFile(rootDir, packet.output_path, { encoding: 'utf8' });
|
|
317
|
+
let result;
|
|
318
|
+
try {
|
|
319
|
+
result = JSON.parse(output.text);
|
|
320
|
+
} catch (cause) {
|
|
321
|
+
throw Object.assign(
|
|
322
|
+
new Error(`worker output is not valid JSON: ${packet.output_path}`),
|
|
323
|
+
{ code: 'WORKER_OUTPUT_INVALID', cause },
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
workerPacket.verifyWorkerResult(packetDocument, result);
|
|
327
|
+
artifactRegistry.recordArtifact(db, {
|
|
328
|
+
id: `artifact-worker-outcome-${packet.id}`,
|
|
329
|
+
owner_type: 'task',
|
|
330
|
+
owner_id: data.id,
|
|
331
|
+
task_id: data.id,
|
|
332
|
+
change_id: ops.readTask(db, data.id)?.change_id || null,
|
|
333
|
+
kind: 'task_outcome',
|
|
334
|
+
path: packet.output_path,
|
|
335
|
+
content_digest: output.digest,
|
|
336
|
+
source_refs: [{
|
|
337
|
+
type: 'external',
|
|
338
|
+
id: `worker-packet:${packet.id}`,
|
|
339
|
+
relation: 'produced_from_packet',
|
|
340
|
+
}],
|
|
341
|
+
consumer_refs: [{
|
|
342
|
+
type: 'external',
|
|
343
|
+
id: 'ultra-test',
|
|
344
|
+
relation: 'consumed_by',
|
|
345
|
+
}],
|
|
346
|
+
provenance: {
|
|
347
|
+
writer: 'worker-result',
|
|
348
|
+
packet_digest: packet.packet_digest,
|
|
349
|
+
},
|
|
350
|
+
metadata: {
|
|
351
|
+
worker_packet_id: packet.id,
|
|
352
|
+
packet_path: packet.packet_path,
|
|
353
|
+
},
|
|
354
|
+
}, { rootDir });
|
|
355
|
+
}
|
|
356
|
+
if (data.patch && Object.keys(data.patch).length > 0) {
|
|
357
|
+
ops.patchTask(db, data.id, data.patch);
|
|
358
|
+
}
|
|
359
|
+
return { task: ops.updateTaskStatus(db, data.id, status) };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function applyRecord(db, entry, { rootDir }) {
|
|
363
|
+
const { kind, action, data = {} } = entry;
|
|
364
|
+
if (!RECORD_KINDS.includes(kind)) {
|
|
365
|
+
throw Object.assign(new Error(`unsupported record kind: ${kind}`), {
|
|
366
|
+
code: 'VALIDATION_ERROR',
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
if (kind === 'baseline') return baselineRecord(db, action, data, rootDir);
|
|
370
|
+
if (kind === 'change_contract') return changeContractRecord(db, action, data, rootDir);
|
|
371
|
+
if (kind === 'decision' && action === 'accept') {
|
|
372
|
+
return { decision: decisionRecords.acceptDecision(db, data, { rootDir }) };
|
|
373
|
+
}
|
|
374
|
+
if (kind === 'task_contract') return taskContractRecord(db, action, data);
|
|
375
|
+
if (kind === 'task_outcome') return taskOutcomeRecord(db, action, data, rootDir);
|
|
376
|
+
if (kind === 'artifact' && action === 'bind') {
|
|
377
|
+
return artifactRegistry.recordArtifact(db, data, { rootDir });
|
|
378
|
+
}
|
|
379
|
+
if (kind === 'event' && action === 'append') {
|
|
380
|
+
return ops.appendEvent(db, data);
|
|
381
|
+
}
|
|
382
|
+
throw Object.assign(
|
|
383
|
+
new Error(`unsupported ${kind} action: ${action}`),
|
|
384
|
+
{ code: 'VALIDATION_ERROR' },
|
|
385
|
+
);
|
|
237
386
|
}
|
|
238
387
|
|
|
239
388
|
async function record(db, input = {}, {
|
|
240
389
|
rootDir = process.cwd(),
|
|
241
|
-
callLegacy,
|
|
242
390
|
} = {}) {
|
|
243
391
|
const results = [];
|
|
244
392
|
for (const entry of input.entries || []) {
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
: null;
|
|
393
|
+
const operation = `${entry.kind}:${entry.action}`;
|
|
394
|
+
const cached = priorResult(db, entry.idempotency_key, operation);
|
|
248
395
|
if (cached) {
|
|
249
|
-
results.push({
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
idempotencyKey: entry.idempotency_key,
|
|
257
|
-
operation: entry.operation,
|
|
258
|
-
result,
|
|
259
|
-
changeId: result.change_id,
|
|
260
|
-
taskId: result.task_id,
|
|
261
|
-
});
|
|
262
|
-
results.push({ operation: entry.operation, accepted: true, idempotent: false, result });
|
|
263
|
-
} catch (error) {
|
|
264
|
-
if (isHardError(error)) throw error;
|
|
265
|
-
results.push({
|
|
266
|
-
operation: entry.operation,
|
|
267
|
-
accepted: false,
|
|
268
|
-
mutable: true,
|
|
269
|
-
blockers: [errorCode(error)],
|
|
270
|
-
diagnostic: { code: errorCode(error), message: error.message },
|
|
271
|
-
});
|
|
272
|
-
}
|
|
396
|
+
results.push({
|
|
397
|
+
kind: entry.kind,
|
|
398
|
+
action: entry.action,
|
|
399
|
+
accepted: true,
|
|
400
|
+
idempotent: true,
|
|
401
|
+
result: cached,
|
|
402
|
+
});
|
|
273
403
|
continue;
|
|
274
404
|
}
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
|
|
405
|
+
try {
|
|
406
|
+
const result = applyRecord(db, entry, { rootDir });
|
|
407
|
+
rememberResult(db, {
|
|
278
408
|
idempotencyKey: entry.idempotency_key,
|
|
279
|
-
operation
|
|
280
|
-
result
|
|
281
|
-
changeId: entry.data
|
|
282
|
-
taskId: entry.
|
|
409
|
+
operation,
|
|
410
|
+
result,
|
|
411
|
+
changeId: entry.data?.change_id || entry.data?.id || null,
|
|
412
|
+
taskId: entry.kind.startsWith('task_') ? entry.data?.id || null : null,
|
|
283
413
|
});
|
|
284
|
-
results.push({ operation: entry.operation, accepted: true, idempotent: false, result: event });
|
|
285
|
-
continue;
|
|
286
|
-
}
|
|
287
|
-
if (!RECORD_OPERATIONS.has(entry.operation)) {
|
|
288
414
|
results.push({
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
415
|
+
kind: entry.kind,
|
|
416
|
+
action: entry.action,
|
|
417
|
+
accepted: true,
|
|
418
|
+
idempotent: false,
|
|
419
|
+
result,
|
|
420
|
+
diagnostics: result?.diagnostics || [],
|
|
293
421
|
});
|
|
294
|
-
continue;
|
|
295
|
-
}
|
|
296
|
-
try {
|
|
297
|
-
const result = await callLegacy(entry.operation, entry.data);
|
|
298
|
-
if (db) {
|
|
299
|
-
rememberFacadeResult(db, {
|
|
300
|
-
idempotencyKey: entry.idempotency_key,
|
|
301
|
-
operation: entry.operation,
|
|
302
|
-
result,
|
|
303
|
-
changeId: entry.data.change_id || entry.data.id || null,
|
|
304
|
-
taskId: entry.data.task_id || null,
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
results.push({ operation: entry.operation, accepted: true, idempotent: false, result });
|
|
308
422
|
} catch (error) {
|
|
309
423
|
if (isHardError(error)) throw error;
|
|
310
424
|
results.push({
|
|
311
|
-
|
|
425
|
+
kind: entry.kind,
|
|
426
|
+
action: entry.action,
|
|
312
427
|
accepted: false,
|
|
313
428
|
mutable: true,
|
|
314
|
-
|
|
315
|
-
diagnostic: { code: errorCode(error), message: error.message, details: error.details },
|
|
429
|
+
diagnostics: [diagnostic(error)],
|
|
316
430
|
});
|
|
317
431
|
}
|
|
318
432
|
}
|
|
@@ -323,273 +437,480 @@ async function record(db, input = {}, {
|
|
|
323
437
|
};
|
|
324
438
|
}
|
|
325
439
|
|
|
326
|
-
function
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
440
|
+
function checkpointDiagnostics(input, context) {
|
|
441
|
+
const supplied = Array.isArray(input.payload?.diagnostics)
|
|
442
|
+
? input.payload.diagnostics
|
|
443
|
+
: [];
|
|
444
|
+
const derived = [
|
|
445
|
+
...(context.envelope?.diagnostics?.warnings || []).map((item) => ({
|
|
446
|
+
...item,
|
|
447
|
+
severity: 'warning',
|
|
448
|
+
})),
|
|
449
|
+
...(context.envelope?.diagnostics?.needs_attention || []).map((item) => ({
|
|
450
|
+
...item,
|
|
451
|
+
severity: 'warning',
|
|
452
|
+
})),
|
|
453
|
+
...(context.envelope?.diagnostics?.hard_conflicts || []).map((item) => ({
|
|
454
|
+
...item,
|
|
455
|
+
severity: 'hard_conflict',
|
|
456
|
+
})),
|
|
457
|
+
];
|
|
458
|
+
return [...supplied, ...derived]
|
|
459
|
+
.filter((item) => item && typeof item === 'object' && item.code)
|
|
460
|
+
.map((item) => ({
|
|
461
|
+
code: String(item.code),
|
|
462
|
+
severity: item.severity || 'needs_attention',
|
|
463
|
+
...(item.message ? { message: String(item.message) } : {}),
|
|
464
|
+
...(item.details === undefined ? {} : { details: item.details }),
|
|
465
|
+
}));
|
|
342
466
|
}
|
|
343
467
|
|
|
344
|
-
|
|
345
|
-
rootDir = process.cwd(),
|
|
346
|
-
callLegacy,
|
|
347
|
-
} = {}) {
|
|
348
|
-
const cached = priorFacadeResult(db, input.idempotency_key, `checkpoint:${input.stage}`);
|
|
349
|
-
if (cached) return { ...cached, idempotent: true };
|
|
350
|
-
const stage = input.stage;
|
|
351
|
-
const scope = input.scope || {};
|
|
468
|
+
function stageAdvisoryDiagnostics(db, input, scope) {
|
|
352
469
|
const payload = input.payload || {};
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
? compileStageContext(db, stage, scope, payload, rootDir)
|
|
470
|
+
const evidence = Array.isArray(payload.evidence) ? payload.evidence : [];
|
|
471
|
+
const warning = (code, message) => ({ code, severity: 'warning', message });
|
|
472
|
+
if (input.stage === 'research') {
|
|
473
|
+
return evidence.length > 0
|
|
474
|
+
? []
|
|
475
|
+
: [warning(
|
|
476
|
+
'RESEARCH_EVIDENCE_MISSING',
|
|
477
|
+
'No research evidence was declared; the caller remains responsible for semantic sufficiency.',
|
|
478
|
+
)];
|
|
479
|
+
}
|
|
480
|
+
if (input.stage === 'dev') {
|
|
481
|
+
const task = scope.task_id ? ops.readTask(db, scope.task_id) : null;
|
|
482
|
+
const outcome = task
|
|
483
|
+
? db.prepare(
|
|
484
|
+
`SELECT id FROM artifacts
|
|
485
|
+
WHERE task_id = ? AND kind = 'task_outcome' AND status = 'current'
|
|
486
|
+
ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
|
|
487
|
+
).get(task.id)
|
|
372
488
|
: null;
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
contextOutput(compiled, rootDir),
|
|
378
|
-
);
|
|
489
|
+
const diagnostics = [];
|
|
490
|
+
if (!task) diagnostics.push(warning('TASK_NOT_FOUND', 'No task scope was found for this Dev checkpoint.'));
|
|
491
|
+
else if (task.status !== 'completed') {
|
|
492
|
+
diagnostics.push(warning('TASK_OUTCOME_INCOMPLETE', `Task ${task.id} is not completed.`));
|
|
379
493
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
kind: 'execution-plan',
|
|
386
|
-
});
|
|
494
|
+
if (task && !outcome) {
|
|
495
|
+
diagnostics.push(warning(
|
|
496
|
+
'WORKER_OUTCOME_MISSING',
|
|
497
|
+
`Task ${task.id} has no current Worker Packet outcome artifact.`,
|
|
498
|
+
));
|
|
387
499
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
500
|
+
return diagnostics;
|
|
501
|
+
}
|
|
502
|
+
if (input.stage === 'test') {
|
|
503
|
+
const result = String(payload.result || '').toLowerCase();
|
|
504
|
+
const diagnostics = [];
|
|
505
|
+
if (!result) {
|
|
506
|
+
diagnostics.push(warning('TEST_RESULT_MISSING', 'No test result was declared.'));
|
|
507
|
+
} else if (!['pass', 'known_red'].includes(result)) {
|
|
508
|
+
diagnostics.push(warning(
|
|
509
|
+
'TEST_RESULT_RECORDED',
|
|
510
|
+
`The caller recorded test result ${result}; MCP does not reinterpret it.`,
|
|
511
|
+
));
|
|
393
512
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
steps,
|
|
397
|
-
evidence: payload.evidence,
|
|
398
|
-
}, { rootDir });
|
|
399
|
-
if (!prepared.ready) {
|
|
400
|
-
return {
|
|
401
|
-
accepted: false,
|
|
402
|
-
mutable: true,
|
|
403
|
-
blockers: prepared.blockers,
|
|
404
|
-
workflow: prepared.workflow,
|
|
405
|
-
};
|
|
513
|
+
if (evidence.length === 0) {
|
|
514
|
+
diagnostics.push(warning('TEST_EVIDENCE_MISSING', 'No test evidence was declared.'));
|
|
406
515
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
}
|
|
418
|
-
|
|
516
|
+
return diagnostics;
|
|
517
|
+
}
|
|
518
|
+
if (input.stage === 'review') {
|
|
519
|
+
const verdict = String(payload.verdict || '').toLowerCase();
|
|
520
|
+
const diagnostics = [];
|
|
521
|
+
if (!verdict) {
|
|
522
|
+
diagnostics.push(warning('REVIEW_VERDICT_MISSING', 'No review verdict was declared.'));
|
|
523
|
+
} else if (!['approve', 'pass'].includes(verdict)) {
|
|
524
|
+
diagnostics.push(warning(
|
|
525
|
+
'REVIEW_VERDICT_RECORDED',
|
|
526
|
+
`The caller recorded review verdict ${verdict}; MCP does not reinterpret it.`,
|
|
527
|
+
));
|
|
419
528
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
mutable: false,
|
|
423
|
-
blockers: [],
|
|
424
|
-
workflow: completed,
|
|
425
|
-
result: stageResult,
|
|
426
|
-
idempotent: false,
|
|
427
|
-
};
|
|
428
|
-
rememberFacadeResult(db, {
|
|
429
|
-
idempotencyKey: input.idempotency_key,
|
|
430
|
-
operation: `checkpoint:${stage}`,
|
|
431
|
-
result,
|
|
432
|
-
changeId: scope.change_id || null,
|
|
433
|
-
taskId: scope.task_id || null,
|
|
434
|
-
});
|
|
435
|
-
ops.appendEvent(db, {
|
|
436
|
-
type: 'ultra_checkpoint_accepted',
|
|
437
|
-
change_id: scope.change_id || null,
|
|
438
|
-
task_id: scope.task_id || null,
|
|
439
|
-
payload: { stage, workflow_id: completed.id, idempotency_key: input.idempotency_key },
|
|
440
|
-
});
|
|
441
|
-
return result;
|
|
442
|
-
} catch (error) {
|
|
443
|
-
if (isHardError(error)) throw error;
|
|
444
|
-
if (run?.id) {
|
|
445
|
-
workflows.reopenWorkflowDraft(db, {
|
|
446
|
-
id: run.id,
|
|
447
|
-
blocker: errorCode(error),
|
|
448
|
-
}, { rootDir });
|
|
529
|
+
if (evidence.length === 0) {
|
|
530
|
+
diagnostics.push(warning('REVIEW_EVIDENCE_MISSING', 'No review evidence was declared.'));
|
|
449
531
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
532
|
+
return diagnostics;
|
|
533
|
+
}
|
|
534
|
+
if (input.stage === 'deliver') {
|
|
535
|
+
const diagnostics = [];
|
|
536
|
+
for (const stage of ['test', 'review']) {
|
|
537
|
+
if (!stageCheckpoints.currentCheckpoint(
|
|
538
|
+
db,
|
|
455
539
|
stage,
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
540
|
+
{ change_id: scope.change_id },
|
|
541
|
+
{ includeDraft: false },
|
|
542
|
+
)) {
|
|
543
|
+
diagnostics.push(warning(
|
|
544
|
+
`${stage.toUpperCase()}_CHECKPOINT_MISSING`,
|
|
545
|
+
`No accepted ${stage} checkpoint was found; the caller owns the delivery decision.`,
|
|
546
|
+
));
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (String(payload.summary || '').trim().length < 3) {
|
|
550
|
+
diagnostics.push(warning('DELIVERY_SUMMARY_MISSING', 'No durable delivery summary was declared.'));
|
|
551
|
+
}
|
|
552
|
+
if (evidence.length === 0) {
|
|
553
|
+
diagnostics.push(warning('DELIVERY_EVIDENCE_MISSING', 'No delivery evidence was declared.'));
|
|
554
|
+
}
|
|
555
|
+
return diagnostics;
|
|
556
|
+
}
|
|
557
|
+
return [];
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function normalizeCheckpointEvidence(db, input, rootDir) {
|
|
561
|
+
const evidence = Array.isArray(input.payload?.evidence) ? input.payload.evidence : [];
|
|
562
|
+
return evidence.map((item, index) => {
|
|
563
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
564
|
+
const error = new Error(`checkpoint evidence[${index}] must be an object`);
|
|
565
|
+
error.code = 'VALIDATION_ERROR';
|
|
566
|
+
throw error;
|
|
567
|
+
}
|
|
568
|
+
const normalized = { ...item };
|
|
569
|
+
let relative = typeof item.ref === 'string' ? item.ref.trim() : '';
|
|
570
|
+
let artifact = null;
|
|
571
|
+
if (item.artifact_id) {
|
|
572
|
+
try {
|
|
573
|
+
artifact = artifactRegistry.getArtifact(db, { id: String(item.artifact_id) });
|
|
574
|
+
} catch (error) {
|
|
575
|
+
if (error?.code !== 'ARTIFACT_NOT_FOUND') throw error;
|
|
576
|
+
}
|
|
577
|
+
if (!artifact || artifact.status !== 'current') {
|
|
578
|
+
const error = new Error(`checkpoint evidence artifact is not current: ${item.artifact_id}`);
|
|
579
|
+
error.code = 'CHECKPOINT_EVIDENCE_AUTHORITY_MISSING';
|
|
580
|
+
throw error;
|
|
581
|
+
}
|
|
582
|
+
if (relative && artifact.path !== relative) {
|
|
583
|
+
const error = new Error(`checkpoint evidence path does not match artifact ${item.artifact_id}`);
|
|
584
|
+
error.code = 'CHECKPOINT_EVIDENCE_AUTHORITY_MISSING';
|
|
585
|
+
throw error;
|
|
586
|
+
}
|
|
587
|
+
relative = artifact.path;
|
|
588
|
+
normalized.artifact_id = artifact.id;
|
|
589
|
+
if (item.digest && String(item.digest) !== artifact.digest) {
|
|
590
|
+
const error = new Error(
|
|
591
|
+
`checkpoint evidence digest contradicts artifact ${item.artifact_id}`,
|
|
592
|
+
);
|
|
593
|
+
error.code = 'CHECKPOINT_EVIDENCE_DIGEST_MISMATCH';
|
|
594
|
+
error.details = {
|
|
595
|
+
path: relative,
|
|
596
|
+
expected: artifact.digest,
|
|
597
|
+
actual: String(item.digest),
|
|
598
|
+
};
|
|
599
|
+
throw error;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (!item.digest && !item.artifact_id) return normalized;
|
|
603
|
+
if (!relative) {
|
|
604
|
+
const error = new Error(`checkpoint evidence[${index}] requires ref when digest is supplied`);
|
|
605
|
+
error.code = 'VALIDATION_ERROR';
|
|
606
|
+
throw error;
|
|
607
|
+
}
|
|
608
|
+
const read = readStableProjectFile(rootDir, relative);
|
|
609
|
+
const expected = item.artifact_id
|
|
610
|
+
? artifact.digest
|
|
611
|
+
: String(item.digest);
|
|
612
|
+
if (read.digest !== expected) {
|
|
613
|
+
const error = new Error(`checkpoint evidence digest mismatch: ${relative}`);
|
|
614
|
+
error.code = 'CHECKPOINT_EVIDENCE_DIGEST_MISMATCH';
|
|
615
|
+
error.details = { path: relative, expected, actual: read.digest };
|
|
616
|
+
throw error;
|
|
617
|
+
}
|
|
618
|
+
normalized.ref = relative;
|
|
619
|
+
normalized.digest = read.digest;
|
|
620
|
+
return normalized;
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
async function checkpoint(db, input = {}, {
|
|
625
|
+
rootDir = process.cwd(),
|
|
626
|
+
runtime = 'unknown',
|
|
627
|
+
markPlanRecoveryRequired = null,
|
|
628
|
+
} = {}) {
|
|
629
|
+
const scope = selectedScope(db, input);
|
|
630
|
+
const context = contextEnvelope.persistEnvelope(db, {
|
|
631
|
+
stage: input.stage,
|
|
632
|
+
scope,
|
|
633
|
+
}, { rootDir, runtime });
|
|
634
|
+
const view = contextEnvelope.buildEnvelope(db, {
|
|
635
|
+
stage: input.stage,
|
|
636
|
+
scope,
|
|
637
|
+
detail: 'summary',
|
|
638
|
+
}, { rootDir, runtime });
|
|
639
|
+
const evidence = normalizeCheckpointEvidence(db, input, rootDir);
|
|
640
|
+
const diagnostics = [
|
|
641
|
+
...checkpointDiagnostics(input, view),
|
|
642
|
+
...stageAdvisoryDiagnostics(db, input, scope),
|
|
643
|
+
...evidence
|
|
644
|
+
.filter((item) => !item.digest && !item.artifact_id)
|
|
645
|
+
.map((item, index) => ({
|
|
646
|
+
code: 'CHECKPOINT_EVIDENCE_UNBOUND',
|
|
647
|
+
severity: 'warning',
|
|
648
|
+
message: `Evidence declaration ${item.ref || item.kind || index} has no current digest or managed artifact binding.`,
|
|
649
|
+
})),
|
|
650
|
+
];
|
|
651
|
+
let result = null;
|
|
652
|
+
|
|
653
|
+
if (input.stage === 'plan'
|
|
654
|
+
&& !diagnostics.some((item) => item.severity === 'hard_conflict')) {
|
|
655
|
+
try {
|
|
656
|
+
result = planCheckpoint.publishPlan(db, {
|
|
657
|
+
change_id: scope.change_id,
|
|
658
|
+
context,
|
|
659
|
+
}, { rootDir, markRecoveryRequired: markPlanRecoveryRequired });
|
|
660
|
+
} catch (error) {
|
|
661
|
+
if (isHardError(error)) throw error;
|
|
662
|
+
diagnostics.push(diagnostic(error, 'hard_conflict'));
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const draft = stageCheckpoints.saveDraft(db, {
|
|
667
|
+
stage: input.stage,
|
|
668
|
+
scope: scope.task_id
|
|
669
|
+
? { task_id: scope.task_id }
|
|
670
|
+
: scope.change_id
|
|
671
|
+
? { change_id: scope.change_id }
|
|
672
|
+
: scope.baseline_id
|
|
673
|
+
? { baseline_id: scope.baseline_id }
|
|
674
|
+
: { project_id: 'project' },
|
|
675
|
+
payload: input.stage === 'plan' && result
|
|
676
|
+
? { ...(input.payload || {}), plan: result }
|
|
677
|
+
: (input.payload || {}),
|
|
678
|
+
evidence,
|
|
679
|
+
diagnostics,
|
|
680
|
+
context_envelope_id: context.id,
|
|
681
|
+
idempotency_key: `${input.idempotency_key}:draft`,
|
|
682
|
+
});
|
|
683
|
+
if (diagnostics.some((item) => item.severity === 'hard_conflict')) {
|
|
461
684
|
return {
|
|
462
685
|
accepted: false,
|
|
463
686
|
mutable: true,
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
687
|
+
diagnostics,
|
|
688
|
+
blockers: diagnostics.map((item) => item.code),
|
|
689
|
+
checkpoint: draft,
|
|
690
|
+
context,
|
|
691
|
+
result,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
const accepted = stageCheckpoints.acceptDraft(db, {
|
|
695
|
+
id: draft.id,
|
|
696
|
+
idempotency_key: `${input.idempotency_key}:accept`,
|
|
697
|
+
});
|
|
698
|
+
if (input.stage === 'plan') {
|
|
699
|
+
result = {
|
|
700
|
+
...result,
|
|
701
|
+
team_checkpoint: taskLedger.publishTaskLedger(db, {
|
|
702
|
+
rootDir,
|
|
703
|
+
reason: 'plan_checkpoint_accepted',
|
|
704
|
+
}),
|
|
467
705
|
};
|
|
468
706
|
}
|
|
707
|
+
return {
|
|
708
|
+
accepted: true,
|
|
709
|
+
mutable: false,
|
|
710
|
+
diagnostics,
|
|
711
|
+
blockers: [],
|
|
712
|
+
checkpoint: accepted,
|
|
713
|
+
context,
|
|
714
|
+
result,
|
|
715
|
+
};
|
|
469
716
|
}
|
|
470
717
|
|
|
471
718
|
function sync(db, input = {}, { rootDir = process.cwd() } = {}) {
|
|
472
|
-
|
|
719
|
+
gitBootstrap.ensureExistingProjectStorageBoundary(rootDir);
|
|
720
|
+
if (input.action === 'inspect') {
|
|
721
|
+
try {
|
|
722
|
+
return taskLedger.inspectTaskLedger(db, { rootDir });
|
|
723
|
+
} catch (error) {
|
|
724
|
+
return {
|
|
725
|
+
status: 'migration_required',
|
|
726
|
+
migration: {
|
|
727
|
+
required: true,
|
|
728
|
+
code: errorCode(error),
|
|
729
|
+
action: 'ultra.sync migrate',
|
|
730
|
+
},
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
}
|
|
473
734
|
if (input.action === 'import') return taskLedger.importTaskLedger(db, { rootDir });
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
735
|
+
if (input.action === 'migrate') {
|
|
736
|
+
const published = taskLedger.publishTaskLedger(db, {
|
|
737
|
+
rootDir,
|
|
738
|
+
reason: 'legacy_authority_migrated',
|
|
739
|
+
});
|
|
740
|
+
return {
|
|
741
|
+
migrated: Boolean(published.migrated_legacy_projection),
|
|
742
|
+
...published,
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
if (input.action === 'publish') {
|
|
746
|
+
const cached = priorResult(db, input.idempotency_key, 'sync:publish');
|
|
747
|
+
if (cached) return { ...cached, idempotent: true };
|
|
748
|
+
const result = taskLedger.publishTaskLedger(db, {
|
|
749
|
+
rootDir,
|
|
750
|
+
reason: input.reason || 'manual_checkpoint',
|
|
751
|
+
});
|
|
752
|
+
rememberResult(db, {
|
|
482
753
|
idempotencyKey: input.idempotency_key,
|
|
483
754
|
operation: 'sync:publish',
|
|
484
755
|
result,
|
|
485
756
|
});
|
|
757
|
+
return result;
|
|
486
758
|
}
|
|
487
|
-
|
|
759
|
+
throw Object.assign(new Error(`unsupported sync action: ${input.action}`), {
|
|
760
|
+
code: 'VALIDATION_ERROR',
|
|
761
|
+
});
|
|
488
762
|
}
|
|
489
763
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
list: 'session.list',
|
|
498
|
-
heartbeat: 'session.heartbeat',
|
|
499
|
-
release: 'session.close',
|
|
500
|
-
}[input.action];
|
|
501
|
-
const args = {
|
|
502
|
-
...payload,
|
|
503
|
-
...(scope.task_id ? { task_id: scope.task_id } : {}),
|
|
504
|
-
...(scope.sid ? { sid: scope.sid } : {}),
|
|
764
|
+
function sessionDiagnostic(error) {
|
|
765
|
+
if (isHardError(error)) throw error;
|
|
766
|
+
return {
|
|
767
|
+
accepted: false,
|
|
768
|
+
mutable: true,
|
|
769
|
+
can_acquire: false,
|
|
770
|
+
diagnostics: [diagnostic(error)],
|
|
505
771
|
};
|
|
506
|
-
if (input.action === 'release' && args.status === undefined) args.status = 'completed';
|
|
507
|
-
return callLegacy(operation, args);
|
|
508
772
|
}
|
|
509
773
|
|
|
510
|
-
async function
|
|
774
|
+
async function session(db, input = {}, {
|
|
511
775
|
rootDir = process.cwd(),
|
|
512
|
-
|
|
776
|
+
runtime = 'unknown',
|
|
777
|
+
sessionId = null,
|
|
513
778
|
} = {}) {
|
|
514
|
-
const
|
|
515
|
-
if (cached) return { ...cached, idempotent: true };
|
|
779
|
+
const scope = input.scope || {};
|
|
516
780
|
const payload = input.payload || {};
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
'deliver',
|
|
532
|
-
{ change_id: input.change_id },
|
|
533
|
-
payload,
|
|
534
|
-
rootDir,
|
|
535
|
-
);
|
|
536
|
-
if (compiled) {
|
|
537
|
-
addStepOutput(steps, 'verify-candidate', contextOutput(compiled, rootDir));
|
|
538
|
-
}
|
|
539
|
-
for (const output of Array.isArray(payload.outputs) ? payload.outputs : []) {
|
|
540
|
-
const stepId = checkpointOutputStep('deliver', output);
|
|
541
|
-
if (!stepId) continue;
|
|
542
|
-
const { step_id: _stepId, role: _role, ...normalized } = output;
|
|
543
|
-
addStepOutput(steps, stepId, normalized);
|
|
544
|
-
}
|
|
545
|
-
const prepared = workflows.prepareWorkflowCheckpoint(db, {
|
|
546
|
-
id: run.id,
|
|
547
|
-
steps,
|
|
548
|
-
evidence: payload.evidence,
|
|
549
|
-
}, { rootDir });
|
|
550
|
-
if (!prepared.ready) {
|
|
551
|
-
return {
|
|
552
|
-
accepted: false,
|
|
553
|
-
mutable: true,
|
|
554
|
-
blockers: prepared.blockers,
|
|
555
|
-
workflow: prepared.workflow,
|
|
556
|
-
};
|
|
781
|
+
if (input.action === 'get') {
|
|
782
|
+
return { session: ops.readSession(db, scope.sid) };
|
|
783
|
+
}
|
|
784
|
+
if (input.action === 'list') {
|
|
785
|
+
const sessions = ops.listActiveSessions(db, { task_id: scope.task_id });
|
|
786
|
+
return { sessions, count: sessions.length };
|
|
787
|
+
}
|
|
788
|
+
if (input.action === 'heartbeat') return ops.heartbeatSession(db, scope.sid);
|
|
789
|
+
if (input.action === 'release') {
|
|
790
|
+
if (sessionId) {
|
|
791
|
+
return sessionDiagnostic(Object.assign(
|
|
792
|
+
new Error('a worker session is settled by its parent host'),
|
|
793
|
+
{ code: 'WORKER_SESSION_PARENT_OWNED' },
|
|
794
|
+
));
|
|
557
795
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
},
|
|
796
|
+
return sessionRunner.closeSession(
|
|
797
|
+
{ db, repoRoot: rootDir, sid: scope.sid },
|
|
798
|
+
{
|
|
799
|
+
status: payload.status || 'completed',
|
|
800
|
+
remove_worktree: payload.remove_worktree === true,
|
|
801
|
+
},
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
const taskId = scope.task_id;
|
|
805
|
+
if (input.action === 'admission') {
|
|
806
|
+
try {
|
|
807
|
+
const task = ops.readTask(db, taskId);
|
|
808
|
+
if (!task) throw Object.assign(new Error(`task ${taskId} not found`), { code: 'TASK_NOT_FOUND' });
|
|
809
|
+
const attention = ops.taskContractBlockers(task).map((code) => ({
|
|
810
|
+
code,
|
|
811
|
+
severity: 'needs_attention',
|
|
812
|
+
}));
|
|
813
|
+
const lease = ops.admissionCheck(db, taskId);
|
|
564
814
|
return {
|
|
565
|
-
accepted:
|
|
815
|
+
accepted: attention.length === 0 && lease.can_spawn,
|
|
566
816
|
mutable: true,
|
|
567
|
-
|
|
568
|
-
|
|
817
|
+
can_acquire: attention.length === 0 && lease.can_spawn,
|
|
818
|
+
diagnostics: [
|
|
819
|
+
...attention,
|
|
820
|
+
...(lease.can_spawn ? [] : [{
|
|
821
|
+
code: 'ACTIVE_SESSION_LEASE_CONFLICT',
|
|
822
|
+
severity: 'hard_conflict',
|
|
823
|
+
details: lease.conflict,
|
|
824
|
+
}]),
|
|
825
|
+
],
|
|
826
|
+
lease,
|
|
569
827
|
};
|
|
828
|
+
} catch (error) {
|
|
829
|
+
return sessionDiagnostic(error);
|
|
570
830
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
831
|
+
}
|
|
832
|
+
if (input.action !== 'acquire') {
|
|
833
|
+
throw Object.assign(new Error(`unsupported session action: ${input.action}`), {
|
|
834
|
+
code: 'VALIDATION_ERROR',
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
let packet = null;
|
|
838
|
+
try {
|
|
839
|
+
packet = workerPacket.createWorkerPacket(db, {
|
|
840
|
+
role: payload.role || 'implement',
|
|
841
|
+
task_id: taskId,
|
|
842
|
+
runtime: payload.runtime || runtime,
|
|
843
|
+
output_path: payload.output_path
|
|
844
|
+
|| `.ultra/changes/active/${ops.readTask(db, taskId)?.change_id}/delivery/${taskId}-outcome.json`,
|
|
845
|
+
output_schema: payload.output_schema,
|
|
846
|
+
evidence_refs: payload.evidence_refs,
|
|
847
|
+
diff_range: payload.diff_range,
|
|
848
|
+
changed_files: payload.changed_files,
|
|
849
|
+
}, { rootDir });
|
|
850
|
+
const handle = sessionRunner.spawnSession({
|
|
851
|
+
db,
|
|
852
|
+
repoRoot: rootDir,
|
|
853
|
+
task_id: taskId,
|
|
854
|
+
runtime: payload.runtime || runtime,
|
|
855
|
+
takeover: payload.takeover === true,
|
|
856
|
+
worktree_base: payload.worktree_base,
|
|
857
|
+
kernel_mode: true,
|
|
858
|
+
mark_task_started: true,
|
|
859
|
+
packet_digest: packet.packet_digest,
|
|
582
860
|
});
|
|
583
|
-
|
|
861
|
+
workerPacket.markWorkerPacketAssigned(db, packet.id);
|
|
862
|
+
return {
|
|
863
|
+
accepted: true,
|
|
864
|
+
sid: handle.sid,
|
|
865
|
+
worktree_path: handle.worktree_path,
|
|
866
|
+
artifact_dir: handle.artifact_dir,
|
|
867
|
+
lease_expires_at: handle.lease_expires_at,
|
|
868
|
+
packet,
|
|
869
|
+
};
|
|
870
|
+
} catch (error) {
|
|
871
|
+
if (packet?.id) {
|
|
872
|
+
workerPacket.abandonWorkerPacket(db, packet.id, error.code || error.message);
|
|
873
|
+
}
|
|
874
|
+
return sessionDiagnostic(error);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
async function archive(db, input = {}, {
|
|
879
|
+
rootDir = process.cwd(),
|
|
880
|
+
} = {}) {
|
|
881
|
+
const cached = priorResult(db, input.idempotency_key, 'archive');
|
|
882
|
+
if (cached) return { ...cached, idempotent: true };
|
|
883
|
+
const change = changes.readChange(db, input.change_id);
|
|
884
|
+
if (!change) {
|
|
885
|
+
return {
|
|
886
|
+
accepted: false,
|
|
887
|
+
mutable: true,
|
|
888
|
+
blockers: ['CHANGE_NOT_FOUND'],
|
|
889
|
+
diagnostics: [{
|
|
890
|
+
code: 'CHANGE_NOT_FOUND',
|
|
891
|
+
severity: 'needs_attention',
|
|
892
|
+
message: `change ${input.change_id} not found`,
|
|
893
|
+
}],
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
try {
|
|
897
|
+
const result = changes.archiveChange(
|
|
898
|
+
db,
|
|
899
|
+
{ id: input.change_id, ...(input.payload || {}) },
|
|
900
|
+
{ rootDir, kernelMode: true },
|
|
901
|
+
);
|
|
584
902
|
const output = {
|
|
585
903
|
accepted: true,
|
|
586
904
|
mutable: false,
|
|
587
905
|
blockers: [],
|
|
588
|
-
|
|
906
|
+
diagnostics: [],
|
|
589
907
|
result,
|
|
590
|
-
|
|
908
|
+
team_checkpoint: taskLedger.publishTaskLedger(db, {
|
|
909
|
+
rootDir,
|
|
910
|
+
reason: 'change_archived',
|
|
911
|
+
}),
|
|
591
912
|
};
|
|
592
|
-
|
|
913
|
+
rememberResult(db, {
|
|
593
914
|
idempotencyKey: input.idempotency_key,
|
|
594
915
|
operation: 'archive',
|
|
595
916
|
result: output,
|
|
@@ -598,17 +919,11 @@ async function archive(db, input = {}, {
|
|
|
598
919
|
return output;
|
|
599
920
|
} catch (error) {
|
|
600
921
|
if (isHardError(error)) throw error;
|
|
601
|
-
if (run?.id) {
|
|
602
|
-
workflows.reopenWorkflowDraft(db, {
|
|
603
|
-
id: run.id,
|
|
604
|
-
blocker: errorCode(error),
|
|
605
|
-
}, { rootDir });
|
|
606
|
-
}
|
|
607
922
|
return {
|
|
608
923
|
accepted: false,
|
|
609
924
|
mutable: true,
|
|
610
925
|
blockers: [errorCode(error)],
|
|
611
|
-
|
|
926
|
+
diagnostics: [diagnostic(error)],
|
|
612
927
|
};
|
|
613
928
|
}
|
|
614
929
|
}
|
|
@@ -634,7 +949,7 @@ async function dispatch(name, input, db, context = {}) {
|
|
|
634
949
|
|
|
635
950
|
module.exports = {
|
|
636
951
|
PUBLIC_TOOLS,
|
|
637
|
-
|
|
952
|
+
RECORD_KINDS,
|
|
638
953
|
dispatch,
|
|
639
954
|
readContext,
|
|
640
955
|
record,
|