ultra-builder-pro-cli 0.23.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ultra-template/docs/research/README.md +4 -4
- package/CHANGELOG.md +45 -0
- package/README.md +21 -18
- package/adapters/_shared/codex-assets.cjs +121 -65
- package/adapters/_shared/interaction-contract.cjs +7 -1
- package/adapters/_shared/provenance.cjs +7 -2
- package/adapters/_shared/runtime-assets.cjs +3 -2
- package/adapters/_shared/runtime-native.cjs +132 -0
- package/adapters/claude.js +60 -30
- package/adapters/codex.js +178 -81
- package/adapters/grok.js +372 -0
- package/adapters/kimi.js +7 -3
- package/adapters/opencode.js +210 -32
- package/agents/code-reviewer.md +4 -2
- package/agents/debugger.md +4 -1
- package/agents/review-code.md +7 -3
- package/agents/review-comments.md +7 -4
- package/agents/review-coordinator.md +11 -7
- package/agents/review-design.md +8 -4
- package/agents/review-errors.md +8 -4
- package/agents/review-spec.md +8 -5
- package/agents/review-tests.md +7 -4
- package/agents/tdd-runner.md +5 -2
- package/bin/install.js +9 -4
- package/bin/orchestrator.js +4 -13
- package/docs/AGENT-CONTEXT.md +141 -229
- package/docs/ARCHITECTURE.md +203 -494
- package/docs/ARTIFACT-AUTHORITY.md +26 -22
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +12 -9
- package/docs/ROADMAP.md +85 -110
- package/docs/RUNTIME-COMPAT-MATRIX.md +139 -175
- package/docs/SKILL-AUTHORING.md +3 -3
- package/docs/STATE-DB-ACCESS-POLICY.md +13 -8
- package/docs/WORKFLOW-LIFECYCLE.md +174 -448
- package/hooks/README.md +11 -8
- package/hooks/active_task_context.py +16 -16
- package/hooks/adapters/grok.py +134 -0
- package/hooks/{context_spine.py → context_envelope.py} +26 -22
- package/hooks/health_check.py +19 -29
- package/hooks/pre_stop_check.py +22 -12
- package/hooks/subagent_tracker.py +1 -1
- package/hooks/workflow_checkpoint.py +14 -13
- package/hooks/workflow_context.py +11 -12
- package/hooks/workflow_resume.py +14 -13
- package/mcp-server/{breadcrumb.cjs → hook-context.cjs} +7 -9
- package/mcp-server/lib/baseline-workflow.cjs +36 -38
- package/mcp-server/lib/canonical-json.cjs +19 -0
- package/mcp-server/lib/change-workflow.cjs +596 -44
- package/mcp-server/lib/context-envelope.cjs +508 -0
- package/mcp-server/lib/decision-records.cjs +337 -0
- package/mcp-server/lib/doctor.cjs +91 -33
- package/mcp-server/lib/init-project.cjs +78 -39
- package/mcp-server/lib/managed-file-write.cjs +116 -0
- package/mcp-server/lib/plan-checkpoint.cjs +177 -0
- package/mcp-server/lib/project-context.cjs +178 -0
- package/mcp-server/lib/stage-checkpoints.cjs +309 -0
- package/mcp-server/lib/state-db.cjs +155 -24
- package/mcp-server/lib/task-ledger.cjs +522 -13
- package/mcp-server/lib/ultra-facade.cjs +630 -442
- package/mcp-server/lib/worker-packet.cjs +358 -0
- package/mcp-server/server.cjs +200 -1103
- package/orchestrator/daemon.cjs +27 -9
- package/orchestrator/parallel-orchestrator.cjs +111 -76
- package/orchestrator/session-runner.cjs +135 -34
- package/package.json +10 -3
- package/skills/ultra-change/SKILL.md +9 -8
- package/skills/ultra-deliver/SKILL.md +3 -3
- package/skills/ultra-dev/SKILL.md +18 -11
- package/skills/ultra-doctor/SKILL.md +2 -2
- package/skills/ultra-init/SKILL.md +6 -5
- package/skills/ultra-init/references/brownfield-adoption.md +11 -8
- package/skills/ultra-plan/SKILL.md +10 -8
- package/skills/ultra-research/SKILL.md +12 -7
- package/skills/ultra-review/SKILL.md +9 -6
- package/skills/ultra-review/references/unified-schema.md +2 -0
- package/skills/ultra-review/scripts/review_wait.py +8 -0
- package/skills/ultra-status/SKILL.md +2 -2
- package/skills/ultra-test/SKILL.md +4 -2
- package/skills/ultra-think/SKILL.md +7 -7
- package/skills/ultra-think/references/interaction-boundary.md +56 -0
- package/spec/cli-protocol.md +80 -199
- package/spec/mcp-tools.yaml +37 -2804
- package/spec/schemas/state-db.sql +107 -4
- package/spec/schemas/{task-ledger.v1.schema.json → task-ledger.v2.schema.json} +70 -6
- package/ultra-tools/commands/status.cjs +369 -243
- package/mcp-server/lib/context-spine.cjs +0 -1208
- package/mcp-server/lib/decision-dialogue.cjs +0 -1128
- package/mcp-server/lib/project-breadcrumb.cjs +0 -173
- package/mcp-server/lib/spec-learning.cjs +0 -284
- package/mcp-server/lib/workflow-state.cjs +0 -3129
- package/skills/ultra-think/references/decision-dialogue.md +0 -155
|
@@ -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,80 @@ 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
|
+
'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',
|
|
67
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',
|
|
68
71
|
]);
|
|
69
72
|
|
|
70
|
-
function parseJson(value, fallback = null) {
|
|
71
|
-
try { return value == null ? fallback : JSON.parse(value); }
|
|
72
|
-
catch { return fallback; }
|
|
73
|
-
}
|
|
74
|
-
|
|
75
73
|
function errorCode(error) {
|
|
76
|
-
return String(error?.code || error?.name || '
|
|
74
|
+
return String(error?.code || error?.name || 'NEEDS_ATTENTION');
|
|
77
75
|
}
|
|
78
76
|
|
|
79
77
|
function isHardError(error) {
|
|
80
78
|
const code = errorCode(error);
|
|
81
79
|
return HARD_ERROR_CODES.has(code)
|
|
82
80
|
|| code.startsWith('PATH_')
|
|
81
|
+
|| code.startsWith('SQLITE_')
|
|
83
82
|
|| code.startsWith('STATE_CORRUPT')
|
|
84
|
-
|| code.startsWith('
|
|
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
|
+
);
|
|
85
88
|
}
|
|
86
89
|
|
|
87
|
-
function
|
|
88
|
-
|
|
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;
|
|
89
97
|
const rows = db.prepare(
|
|
90
98
|
`SELECT payload_json FROM events
|
|
91
|
-
WHERE type = '
|
|
99
|
+
WHERE type = 'ultra_kernel_call'
|
|
92
100
|
ORDER BY id DESC LIMIT 2000`,
|
|
93
101
|
).all();
|
|
94
102
|
for (const row of rows) {
|
|
95
103
|
const payload = parseJson(row.payload_json, {});
|
|
96
104
|
if (payload.idempotency_key === idempotencyKey
|
|
97
|
-
&&
|
|
105
|
+
&& payload.operation === operation
|
|
98
106
|
&& payload.accepted === true) {
|
|
99
107
|
return payload.result;
|
|
100
108
|
}
|
|
@@ -102,15 +110,16 @@ function priorFacadeResult(db, idempotencyKey, operation = null) {
|
|
|
102
110
|
return null;
|
|
103
111
|
}
|
|
104
112
|
|
|
105
|
-
function
|
|
113
|
+
function rememberResult(db, {
|
|
106
114
|
idempotencyKey,
|
|
107
115
|
operation,
|
|
108
116
|
result,
|
|
109
117
|
changeId = null,
|
|
110
118
|
taskId = null,
|
|
111
119
|
}) {
|
|
120
|
+
if (!db || !idempotencyKey) return;
|
|
112
121
|
ops.appendEvent(db, {
|
|
113
|
-
type: '
|
|
122
|
+
type: 'ultra_kernel_call',
|
|
114
123
|
change_id: changeId,
|
|
115
124
|
task_id: taskId,
|
|
116
125
|
payload: {
|
|
@@ -122,197 +131,260 @@ function rememberFacadeResult(db, {
|
|
|
122
131
|
});
|
|
123
132
|
}
|
|
124
133
|
|
|
125
|
-
function
|
|
126
|
-
return
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
+
};
|
|
131
141
|
}
|
|
132
142
|
|
|
133
|
-
function
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
task_id: scope.task_id
|
|
138
|
-
|
|
139
|
-
|
|
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' };
|
|
140
156
|
}
|
|
141
157
|
|
|
142
|
-
function
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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 }) };
|
|
159
179
|
}
|
|
160
|
-
|
|
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
|
+
});
|
|
161
198
|
}
|
|
162
199
|
|
|
163
|
-
function
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
+
});
|
|
168
227
|
}
|
|
169
228
|
|
|
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 });
|
|
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
|
+
});
|
|
181
236
|
}
|
|
182
237
|
|
|
183
|
-
function
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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(
|
|
194
264
|
db,
|
|
195
|
-
|
|
265
|
+
packet.id,
|
|
196
266
|
{ 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, {}),
|
|
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
|
+
);
|
|
223
273
|
}
|
|
224
|
-
:
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
+
);
|
|
237
344
|
}
|
|
238
345
|
|
|
239
346
|
async function record(db, input = {}, {
|
|
240
347
|
rootDir = process.cwd(),
|
|
241
|
-
callLegacy,
|
|
242
348
|
} = {}) {
|
|
243
349
|
const results = [];
|
|
244
350
|
for (const entry of input.entries || []) {
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
: null;
|
|
351
|
+
const operation = `${entry.kind}:${entry.action}`;
|
|
352
|
+
const cached = priorResult(db, entry.idempotency_key, operation);
|
|
248
353
|
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
|
-
}
|
|
354
|
+
results.push({
|
|
355
|
+
kind: entry.kind,
|
|
356
|
+
action: entry.action,
|
|
357
|
+
accepted: true,
|
|
358
|
+
idempotent: true,
|
|
359
|
+
result: cached,
|
|
360
|
+
});
|
|
273
361
|
continue;
|
|
274
362
|
}
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
|
|
363
|
+
try {
|
|
364
|
+
const result = applyRecord(db, entry, { rootDir });
|
|
365
|
+
rememberResult(db, {
|
|
278
366
|
idempotencyKey: entry.idempotency_key,
|
|
279
|
-
operation
|
|
280
|
-
result
|
|
281
|
-
changeId: entry.data
|
|
282
|
-
taskId: entry.
|
|
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,
|
|
283
371
|
});
|
|
284
|
-
results.push({ operation: entry.operation, accepted: true, idempotent: false, result: event });
|
|
285
|
-
continue;
|
|
286
|
-
}
|
|
287
|
-
if (!RECORD_OPERATIONS.has(entry.operation)) {
|
|
288
372
|
results.push({
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
373
|
+
kind: entry.kind,
|
|
374
|
+
action: entry.action,
|
|
375
|
+
accepted: true,
|
|
376
|
+
idempotent: false,
|
|
377
|
+
result,
|
|
378
|
+
diagnostics: result?.diagnostics || [],
|
|
293
379
|
});
|
|
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
380
|
} catch (error) {
|
|
309
381
|
if (isHardError(error)) throw error;
|
|
310
382
|
results.push({
|
|
311
|
-
|
|
383
|
+
kind: entry.kind,
|
|
384
|
+
action: entry.action,
|
|
312
385
|
accepted: false,
|
|
313
386
|
mutable: true,
|
|
314
|
-
|
|
315
|
-
diagnostic: { code: errorCode(error), message: error.message, details: error.details },
|
|
387
|
+
diagnostics: [diagnostic(error)],
|
|
316
388
|
});
|
|
317
389
|
}
|
|
318
390
|
}
|
|
@@ -323,273 +395,395 @@ async function record(db, input = {}, {
|
|
|
323
395
|
};
|
|
324
396
|
}
|
|
325
397
|
|
|
326
|
-
function
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
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
|
+
}));
|
|
330
424
|
}
|
|
331
425
|
|
|
332
|
-
function
|
|
333
|
-
if (output.step_id) return output.step_id;
|
|
334
|
-
if (stage === 'test') return 'write-report';
|
|
335
|
-
if (stage === 'deliver') return 'verify-delivery';
|
|
336
|
-
if (stage === 'review') {
|
|
337
|
-
if (output.role === 'spec_review') return 'review-specification';
|
|
338
|
-
if (output.role === 'engineering_review') return 'review-engineering';
|
|
339
|
-
return 'coordinate-findings';
|
|
340
|
-
}
|
|
341
|
-
return null;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
async function checkpoint(db, input = {}, {
|
|
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 || {};
|
|
426
|
+
function stageReadinessDiagnostics(db, input, scope) {
|
|
352
427
|
const payload = input.payload || {};
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
? payload.steps.map((step) => ({ ...step }))
|
|
369
|
-
: [];
|
|
370
|
-
const compiled = STAGE_CONTEXT[stage]
|
|
371
|
-
? compileStageContext(db, stage, scope, payload, rootDir)
|
|
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)
|
|
372
443
|
: null;
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
contextOutput(compiled, rootDir),
|
|
378
|
-
);
|
|
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.`));
|
|
379
448
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
kind: 'execution-plan',
|
|
386
|
-
});
|
|
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
|
+
));
|
|
387
454
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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.'));
|
|
393
462
|
}
|
|
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
|
-
};
|
|
463
|
+
if (evidence.length === 0) {
|
|
464
|
+
diagnostics.push(needs('TEST_EVIDENCE_REQUIRED', 'Test checkpoint requires evidence.'));
|
|
406
465
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
team_checkpoint: taskLedger.publishTaskLedger(db, {
|
|
415
|
-
rootDir,
|
|
416
|
-
reason: 'plan_accepted',
|
|
417
|
-
}),
|
|
418
|
-
};
|
|
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.'));
|
|
419
473
|
}
|
|
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 });
|
|
474
|
+
if (evidence.length === 0) {
|
|
475
|
+
diagnostics.push(needs('REVIEW_EVIDENCE_REQUIRED', 'Review checkpoint requires evidence.'));
|
|
449
476
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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,
|
|
455
484
|
stage,
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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')) {
|
|
461
557
|
return {
|
|
462
558
|
accepted: false,
|
|
463
559
|
mutable: true,
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
+
}),
|
|
467
578
|
};
|
|
468
579
|
}
|
|
580
|
+
return {
|
|
581
|
+
accepted: true,
|
|
582
|
+
mutable: false,
|
|
583
|
+
diagnostics,
|
|
584
|
+
blockers: [],
|
|
585
|
+
checkpoint: accepted,
|
|
586
|
+
context,
|
|
587
|
+
result,
|
|
588
|
+
};
|
|
469
589
|
}
|
|
470
590
|
|
|
471
591
|
function sync(db, input = {}, { rootDir = process.cwd() } = {}) {
|
|
472
|
-
|
|
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
|
+
}
|
|
473
607
|
if (input.action === 'import') return taskLedger.importTaskLedger(db, { rootDir });
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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, {
|
|
482
626
|
idempotencyKey: input.idempotency_key,
|
|
483
627
|
operation: 'sync:publish',
|
|
484
628
|
result,
|
|
485
629
|
});
|
|
630
|
+
return result;
|
|
486
631
|
}
|
|
487
|
-
|
|
632
|
+
throw Object.assign(new Error(`unsupported sync action: ${input.action}`), {
|
|
633
|
+
code: 'VALIDATION_ERROR',
|
|
634
|
+
});
|
|
488
635
|
}
|
|
489
636
|
|
|
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 } : {}),
|
|
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)],
|
|
505
644
|
};
|
|
506
|
-
if (input.action === 'release' && args.status === undefined) args.status = 'completed';
|
|
507
|
-
return callLegacy(operation, args);
|
|
508
645
|
}
|
|
509
646
|
|
|
510
|
-
async function
|
|
647
|
+
async function session(db, input = {}, {
|
|
511
648
|
rootDir = process.cwd(),
|
|
512
|
-
|
|
649
|
+
runtime = 'unknown',
|
|
650
|
+
sessionId = null,
|
|
513
651
|
} = {}) {
|
|
514
|
-
const
|
|
515
|
-
if (cached) return { ...cached, idempotent: true };
|
|
652
|
+
const scope = input.scope || {};
|
|
516
653
|
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);
|
|
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
|
+
));
|
|
544
668
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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);
|
|
551
687
|
return {
|
|
552
|
-
accepted:
|
|
688
|
+
accepted: attention.length === 0 && lease.can_spawn,
|
|
553
689
|
mutable: true,
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
return {
|
|
565
|
-
accepted: false,
|
|
566
|
-
mutable: true,
|
|
567
|
-
blockers: convergence.blockers || ['CHANGE_NOT_READY'],
|
|
568
|
-
convergence,
|
|
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,
|
|
569
700
|
};
|
|
701
|
+
} catch (error) {
|
|
702
|
+
return sessionDiagnostic(error);
|
|
570
703
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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,
|
|
582
733
|
});
|
|
583
|
-
|
|
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
|
+
);
|
|
584
775
|
const output = {
|
|
585
776
|
accepted: true,
|
|
586
777
|
mutable: false,
|
|
587
778
|
blockers: [],
|
|
588
|
-
|
|
779
|
+
diagnostics: [],
|
|
589
780
|
result,
|
|
590
|
-
|
|
781
|
+
team_checkpoint: taskLedger.publishTaskLedger(db, {
|
|
782
|
+
rootDir,
|
|
783
|
+
reason: 'change_archived',
|
|
784
|
+
}),
|
|
591
785
|
};
|
|
592
|
-
|
|
786
|
+
rememberResult(db, {
|
|
593
787
|
idempotencyKey: input.idempotency_key,
|
|
594
788
|
operation: 'archive',
|
|
595
789
|
result: output,
|
|
@@ -598,17 +792,11 @@ async function archive(db, input = {}, {
|
|
|
598
792
|
return output;
|
|
599
793
|
} catch (error) {
|
|
600
794
|
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
795
|
return {
|
|
608
796
|
accepted: false,
|
|
609
797
|
mutable: true,
|
|
610
798
|
blockers: [errorCode(error)],
|
|
611
|
-
|
|
799
|
+
diagnostics: [diagnostic(error)],
|
|
612
800
|
};
|
|
613
801
|
}
|
|
614
802
|
}
|
|
@@ -634,7 +822,7 @@ async function dispatch(name, input, db, context = {}) {
|
|
|
634
822
|
|
|
635
823
|
module.exports = {
|
|
636
824
|
PUBLIC_TOOLS,
|
|
637
|
-
|
|
825
|
+
RECORD_KINDS,
|
|
638
826
|
dispatch,
|
|
639
827
|
readContext,
|
|
640
828
|
record,
|