ultra-builder-pro-cli 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ultra-template/docs/research/README.md +4 -4
- package/CHANGELOG.md +83 -0
- package/README.md +68 -35
- package/adapters/_shared/codex-assets.cjs +121 -60
- package/adapters/_shared/interaction-contract.cjs +20 -7
- package/adapters/_shared/provenance.cjs +7 -2
- package/adapters/_shared/runtime-assets.cjs +4 -3
- package/adapters/_shared/runtime-native.cjs +132 -0
- package/adapters/claude.js +61 -31
- package/adapters/codex.js +178 -81
- package/adapters/grok.js +372 -0
- package/adapters/kimi.js +8 -4
- package/adapters/opencode.js +211 -33
- package/agents/code-reviewer.md +4 -2
- package/agents/debugger.md +4 -1
- package/agents/review-code.md +7 -3
- package/agents/review-comments.md +7 -4
- package/agents/review-coordinator.md +11 -7
- package/agents/review-design.md +8 -4
- package/agents/review-errors.md +8 -4
- package/agents/review-spec.md +8 -5
- package/agents/review-tests.md +7 -4
- package/agents/tdd-runner.md +5 -2
- package/bin/install.js +9 -4
- package/bin/orchestrator.js +4 -13
- package/docs/AGENT-CONTEXT.md +141 -231
- package/docs/ARCHITECTURE.md +203 -498
- package/docs/ARTIFACT-AUTHORITY.md +38 -35
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +12 -9
- package/docs/ROADMAP.md +85 -110
- package/docs/RUNTIME-COMPAT-MATRIX.md +139 -174
- package/docs/SKILL-AUTHORING.md +3 -3
- package/docs/STATE-DB-ACCESS-POLICY.md +25 -20
- package/docs/WORKFLOW-LIFECYCLE.md +174 -442
- package/hooks/README.md +11 -8
- package/hooks/active_task_context.py +16 -16
- package/hooks/adapters/grok.py +134 -0
- package/hooks/{context_spine.py → context_envelope.py} +26 -22
- package/hooks/health_check.py +19 -29
- package/hooks/pre_stop_check.py +22 -12
- package/hooks/subagent_tracker.py +1 -1
- package/hooks/workflow_checkpoint.py +14 -13
- package/hooks/workflow_context.py +11 -12
- package/hooks/workflow_resume.py +14 -13
- package/mcp-server/{breadcrumb.cjs → hook-context.cjs} +7 -9
- package/mcp-server/lib/artifact-registry.cjs +25 -1
- package/mcp-server/lib/baseline-workflow.cjs +36 -38
- package/mcp-server/lib/canonical-json.cjs +19 -0
- package/mcp-server/lib/change-workflow.cjs +637 -49
- package/mcp-server/lib/context-envelope.cjs +508 -0
- package/mcp-server/lib/decision-records.cjs +337 -0
- package/mcp-server/lib/doctor.cjs +91 -33
- package/mcp-server/lib/init-project.cjs +78 -39
- package/mcp-server/lib/managed-file-write.cjs +116 -0
- package/mcp-server/lib/plan-checkpoint.cjs +177 -0
- package/mcp-server/lib/project-context.cjs +178 -0
- package/mcp-server/lib/stage-checkpoints.cjs +309 -0
- package/mcp-server/lib/state-db.cjs +155 -24
- package/mcp-server/lib/task-ledger.cjs +522 -13
- package/mcp-server/lib/ultra-facade.cjs +834 -0
- package/mcp-server/lib/worker-packet.cjs +358 -0
- package/mcp-server/server.cjs +208 -1059
- package/orchestrator/daemon.cjs +27 -9
- package/orchestrator/parallel-orchestrator.cjs +111 -76
- package/orchestrator/session-runner.cjs +135 -34
- package/package.json +10 -3
- package/skills/ultra-change/SKILL.md +39 -98
- package/skills/ultra-deliver/SKILL.md +42 -65
- package/skills/ultra-dev/SKILL.md +52 -99
- package/skills/ultra-doctor/SKILL.md +35 -57
- package/skills/ultra-init/SKILL.md +48 -88
- package/skills/ultra-init/references/brownfield-adoption.md +11 -8
- package/skills/ultra-plan/SKILL.md +54 -132
- package/skills/ultra-research/SKILL.md +58 -112
- package/skills/ultra-review/SKILL.md +28 -46
- package/skills/ultra-review/references/unified-schema.md +2 -0
- package/skills/ultra-review/scripts/review_wait.py +8 -0
- package/skills/ultra-status/SKILL.md +22 -45
- package/skills/ultra-test/SKILL.md +27 -61
- package/skills/ultra-think/SKILL.md +29 -48
- package/skills/ultra-think/references/interaction-boundary.md +56 -0
- package/spec/cli-protocol.md +80 -187
- package/spec/mcp-tools.yaml +130 -2738
- package/spec/schemas/mcp-tools.schema.json +1 -1
- package/spec/schemas/state-db.sql +107 -4
- package/spec/schemas/{task-ledger.v1.schema.json → task-ledger.v2.schema.json} +70 -6
- package/ultra-tools/commands/status.cjs +369 -243
- package/mcp-server/lib/context-spine.cjs +0 -1208
- package/mcp-server/lib/decision-dialogue.cjs +0 -1128
- package/mcp-server/lib/project-breadcrumb.cjs +0 -173
- package/mcp-server/lib/spec-learning.cjs +0 -284
- package/mcp-server/lib/workflow-state.cjs +0 -2965
- package/skills/ultra-think/references/decision-dialogue.md +0 -153
|
@@ -1,1208 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const crypto = require('node:crypto');
|
|
4
|
-
const fs = require('node:fs');
|
|
5
|
-
const path = require('node:path');
|
|
6
|
-
const { spawnSync } = require('node:child_process');
|
|
7
|
-
const baselines = require('./baseline-workflow.cjs');
|
|
8
|
-
const workflows = require('./workflow-state.cjs');
|
|
9
|
-
const decisions = require('./decision-dialogue.cjs');
|
|
10
|
-
const ops = require('./state-ops.cjs');
|
|
11
|
-
|
|
12
|
-
const ROLES = new Set(['plan', 'implement', 'check', 'review']);
|
|
13
|
-
const GATES = new Set([
|
|
14
|
-
'alignment', 'planning', 'implementation', 'verification', 'review', 'convergence', 'recovery',
|
|
15
|
-
]);
|
|
16
|
-
const CONTEXT_KINDS = new Set(['spec', 'source', 'test', 'docs', 'external']);
|
|
17
|
-
const SLICE_KINDS = new Set(['tracer_bullet', 'expand_contract', 'integration_checkpoint']);
|
|
18
|
-
const DEFAULT_BUDGET = Object.freeze({ max_tokens: 12_000, max_files: 12 });
|
|
19
|
-
const MAX_CONTEXT_REFS = 64;
|
|
20
|
-
const MAX_REF_LENGTH = 2_048;
|
|
21
|
-
const MAX_REASON_LENGTH = 2_048;
|
|
22
|
-
const MAX_LOCATOR_LENGTH = 512;
|
|
23
|
-
const SPEC_REF_FIELDS = new Set([
|
|
24
|
-
'ref', 'reason', 'required', 'expected_digest', 'anchor', 'scope', 'freshness_policy',
|
|
25
|
-
]);
|
|
26
|
-
const ADVISORY_CONTEXT_CODES = new Set([
|
|
27
|
-
'CONTEXT_FILE_BUDGET_EXCEEDED',
|
|
28
|
-
'CONTEXT_TOKEN_BUDGET_EXCEEDED',
|
|
29
|
-
'EXECUTION_CONTEXT_BUDGET_EXCEEDED',
|
|
30
|
-
'EXECUTION_CONTEXT_BUDGET_ADVISORY',
|
|
31
|
-
]);
|
|
32
|
-
const ACTIVE_CHANGE_BASELINE_DRIFT = Object.freeze([
|
|
33
|
-
/^BASELINE_HEAD_STALE$/,
|
|
34
|
-
/^BASELINE_BRANCH_STALE$/,
|
|
35
|
-
/^BASELINE_WORKSPACE_STALE$/,
|
|
36
|
-
/^BASELINE_WORKTREE_STALE$/,
|
|
37
|
-
/^BASELINE_WORKTREE_DIRTY$/,
|
|
38
|
-
/^BASELINE_SPEC_STALE:/,
|
|
39
|
-
/^BASELINE_EVIDENCE_STALE:/,
|
|
40
|
-
]);
|
|
41
|
-
|
|
42
|
-
class ContextSpineError extends Error {
|
|
43
|
-
constructor(code, message) {
|
|
44
|
-
super(message);
|
|
45
|
-
this.name = 'ContextSpineError';
|
|
46
|
-
this.code = code;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function currentHead(rootDir) {
|
|
51
|
-
const result = spawnSync('git', ['rev-parse', 'HEAD'], {
|
|
52
|
-
cwd: rootDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
|
53
|
-
});
|
|
54
|
-
return result.status === 0 ? result.stdout.trim() : null;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function parseJson(value, fallback = {}) {
|
|
58
|
-
if (typeof value !== 'string' || !value.trim()) return fallback;
|
|
59
|
-
try { return JSON.parse(value); } catch { return fallback; }
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function uniqueStrings(values) {
|
|
63
|
-
return [...new Set(values.filter((value) => typeof value === 'string' && value.length > 0))];
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function hasIncidentBreakGlass(change) {
|
|
67
|
-
const bypass = change?.baseline_bypass
|
|
68
|
-
|| parseJson(change?.baseline_bypass_json, null);
|
|
69
|
-
return change?.kind === 'incident'
|
|
70
|
-
&& bypass?.mode === 'incident_break_glass'
|
|
71
|
-
&& Boolean(String(bypass.reason || '').trim())
|
|
72
|
-
&& Boolean(String(bypass.approved_by || '').trim());
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function expectedActiveChangeDrift(code) {
|
|
76
|
-
return ACTIVE_CHANGE_BASELINE_DRIFT.some((pattern) => pattern.test(code));
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function baselineGateForChange(db, change, baselineHealth) {
|
|
80
|
-
const baseWarnings = Array.isArray(baselineHealth.warnings) ? baselineHealth.warnings : [];
|
|
81
|
-
if (baselineHealth.status === 'pass') {
|
|
82
|
-
return { blockers: [], warnings: [...new Set(baseWarnings)], mode: 'healthy' };
|
|
83
|
-
}
|
|
84
|
-
const healthBlockers = Array.isArray(baselineHealth.blockers)
|
|
85
|
-
? baselineHealth.blockers : ['BASELINE_STATE_UNAVAILABLE'];
|
|
86
|
-
if (hasIncidentBreakGlass(change)) {
|
|
87
|
-
return {
|
|
88
|
-
blockers: [], warnings: [...new Set([...healthBlockers, ...baseWarnings])],
|
|
89
|
-
mode: 'incident_break_glass',
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
const baseline = baselineHealth.baseline;
|
|
93
|
-
if (!baseline || baseline.status !== 'ready' || baseline.mode === 'migrated') {
|
|
94
|
-
return { blockers: healthBlockers, warnings: baseWarnings, mode: 'baseline_required' };
|
|
95
|
-
}
|
|
96
|
-
const binding = db.prepare(
|
|
97
|
-
`SELECT wr.baseline_id, ws.status AS binding_status, ws.evidence_json
|
|
98
|
-
FROM workflow_runs wr
|
|
99
|
-
LEFT JOIN workflow_steps ws
|
|
100
|
-
ON ws.run_id = wr.id AND ws.step_id = 'bind-baseline'
|
|
101
|
-
WHERE wr.kind = 'change' AND wr.change_id = ?
|
|
102
|
-
ORDER BY wr.started_at DESC, wr.rowid DESC LIMIT 1`,
|
|
103
|
-
).get(change.id);
|
|
104
|
-
let evidence = [];
|
|
105
|
-
try { evidence = JSON.parse(binding?.evidence_json || '[]'); } catch { evidence = []; }
|
|
106
|
-
const boundFromReadyAuthority = binding?.baseline_id === baseline.id
|
|
107
|
-
&& binding.binding_status === 'completed'
|
|
108
|
-
&& evidence.some((item) => item?.ref === baseline.id);
|
|
109
|
-
if (!boundFromReadyAuthority) {
|
|
110
|
-
return { blockers: healthBlockers, warnings: baseWarnings, mode: 'baseline_binding_required' };
|
|
111
|
-
}
|
|
112
|
-
const blockers = healthBlockers.filter((code) => !expectedActiveChangeDrift(code));
|
|
113
|
-
const driftWarnings = healthBlockers.filter(expectedActiveChangeDrift);
|
|
114
|
-
return {
|
|
115
|
-
blockers,
|
|
116
|
-
warnings: [...new Set([...driftWarnings, ...baseWarnings])],
|
|
117
|
-
mode: blockers.length === 0 ? 'active_change_drift' : 'baseline_invalid',
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function taskStateDigest(task) {
|
|
122
|
-
if (!task) return null;
|
|
123
|
-
const payload = {
|
|
124
|
-
id: task.id,
|
|
125
|
-
change_id: task.change_id,
|
|
126
|
-
status: task.status,
|
|
127
|
-
stale: Boolean(task.stale),
|
|
128
|
-
deps: task.deps || [],
|
|
129
|
-
outcome: task.outcome,
|
|
130
|
-
slice_kind: task.slice_kind,
|
|
131
|
-
public_seam: task.public_seam,
|
|
132
|
-
verification_command: task.verification_command,
|
|
133
|
-
acceptance: task.acceptance || [],
|
|
134
|
-
context_refs: task.context_refs || [],
|
|
135
|
-
docs_impact: task.docs_impact || {},
|
|
136
|
-
ownership: task.ownership || {},
|
|
137
|
-
trace_to: task.trace_to,
|
|
138
|
-
};
|
|
139
|
-
return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex');
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function taskContractDigest(task) {
|
|
143
|
-
if (!task) return null;
|
|
144
|
-
const payload = {
|
|
145
|
-
id: task.id,
|
|
146
|
-
change_id: task.change_id,
|
|
147
|
-
deps: task.deps || [],
|
|
148
|
-
files_modified: task.files_modified || [],
|
|
149
|
-
outcome: task.outcome,
|
|
150
|
-
slice_kind: task.slice_kind,
|
|
151
|
-
public_seam: task.public_seam,
|
|
152
|
-
verification_command: task.verification_command,
|
|
153
|
-
acceptance: task.acceptance || [],
|
|
154
|
-
context_refs: task.context_refs || [],
|
|
155
|
-
docs_impact: task.docs_impact || {},
|
|
156
|
-
ownership: task.ownership || {},
|
|
157
|
-
trace_to: task.trace_to,
|
|
158
|
-
};
|
|
159
|
-
return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex');
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function changeStateDigest(change) {
|
|
163
|
-
if (!change) return null;
|
|
164
|
-
const payload = {
|
|
165
|
-
id: change.id,
|
|
166
|
-
kind: change.kind,
|
|
167
|
-
intent: change.intent,
|
|
168
|
-
docs_impact: change.docs_impact || parseJson(change.docs_impact_json, {}),
|
|
169
|
-
provider_refs: change.provider_refs || parseJson(change.provider_refs_json, {}),
|
|
170
|
-
contract: change.contract || parseJson(change.contract_json, {}),
|
|
171
|
-
classification: change.classification || parseJson(change.classification_json, {}),
|
|
172
|
-
research_disposition: change.research_disposition
|
|
173
|
-
|| parseJson(change.research_disposition_json, {}),
|
|
174
|
-
};
|
|
175
|
-
return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex');
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function nonEmpty(value, field) {
|
|
179
|
-
const text = typeof value === 'string' ? value.trim() : '';
|
|
180
|
-
if (!text) throw new ContextSpineError('VALIDATION_ERROR', `${field} must be a non-empty string`);
|
|
181
|
-
return text;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function boundedText(value, field, maxLength) {
|
|
185
|
-
const text = nonEmpty(value, field);
|
|
186
|
-
if (text.length > maxLength) {
|
|
187
|
-
throw new ContextSpineError(
|
|
188
|
-
'VALIDATION_ERROR', `${field} must be at most ${maxLength} characters`,
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
return text;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function localPath(rootDir, ref) {
|
|
195
|
-
const candidate = ref.split('#', 1)[0];
|
|
196
|
-
if (!candidate || path.isAbsolute(candidate)
|
|
197
|
-
|| /^[a-z][a-z0-9+.-]*:\/\//i.test(candidate)) {
|
|
198
|
-
throw new ContextSpineError('VALIDATION_ERROR', `context ref must be project-relative: ${ref}`);
|
|
199
|
-
}
|
|
200
|
-
const root = path.resolve(rootDir);
|
|
201
|
-
const resolved = path.resolve(root, candidate);
|
|
202
|
-
if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`)) {
|
|
203
|
-
throw new ContextSpineError('VALIDATION_ERROR', `context ref escapes project root: ${ref}`);
|
|
204
|
-
}
|
|
205
|
-
return resolved;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function contextPathUnsafe(ref, message) {
|
|
209
|
-
return new ContextSpineError(
|
|
210
|
-
'CONTEXT_REF_UNSAFE',
|
|
211
|
-
`${message}: ${ref}`,
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function lstatOrNull(candidate) {
|
|
216
|
-
try { return fs.lstatSync(candidate); } catch (error) {
|
|
217
|
-
if (error?.code === 'ENOENT') return null;
|
|
218
|
-
throw error;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function sameEntry(left, right) {
|
|
223
|
-
return Boolean(left && right
|
|
224
|
-
&& String(left.dev) === String(right.dev)
|
|
225
|
-
&& String(left.ino) === String(right.ino));
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function physicallyContained(root, candidate) {
|
|
229
|
-
return candidate === root || candidate.startsWith(`${root}${path.sep}`);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function readProjectFile(rootDir, ref) {
|
|
233
|
-
const file = localPath(rootDir, ref);
|
|
234
|
-
const root = path.resolve(rootDir);
|
|
235
|
-
const rootStat = lstatOrNull(root);
|
|
236
|
-
if (!rootStat || !rootStat.isDirectory() || rootStat.isSymbolicLink()) {
|
|
237
|
-
throw contextPathUnsafe(ref, 'project root must be a real directory');
|
|
238
|
-
}
|
|
239
|
-
const physicalRoot = fs.realpathSync(root);
|
|
240
|
-
const identities = [{ file: root, stat: rootStat, directory: true }];
|
|
241
|
-
let current = root;
|
|
242
|
-
const segments = path.relative(root, file).split(path.sep).filter(Boolean);
|
|
243
|
-
for (const [index, segment] of segments.entries()) {
|
|
244
|
-
current = path.join(current, segment);
|
|
245
|
-
const stat = lstatOrNull(current);
|
|
246
|
-
if (!stat) return null;
|
|
247
|
-
const final = index === segments.length - 1;
|
|
248
|
-
if (stat.isSymbolicLink()) {
|
|
249
|
-
throw contextPathUnsafe(ref, 'local Context refs cannot traverse symbolic links');
|
|
250
|
-
}
|
|
251
|
-
if ((!final && !stat.isDirectory()) || (final && !stat.isFile())) {
|
|
252
|
-
throw contextPathUnsafe(
|
|
253
|
-
ref,
|
|
254
|
-
final
|
|
255
|
-
? 'local Context ref must be a regular file'
|
|
256
|
-
: 'local Context ref ancestor must be a directory',
|
|
257
|
-
);
|
|
258
|
-
}
|
|
259
|
-
identities.push({ file: current, stat, directory: !final });
|
|
260
|
-
}
|
|
261
|
-
let physicalFile;
|
|
262
|
-
try { physicalFile = fs.realpathSync(file); } catch (error) {
|
|
263
|
-
if (error?.code === 'ENOENT') return null;
|
|
264
|
-
throw error;
|
|
265
|
-
}
|
|
266
|
-
if (!physicallyContained(physicalRoot, physicalFile)) {
|
|
267
|
-
throw contextPathUnsafe(ref, 'local Context ref escapes the physical project root');
|
|
268
|
-
}
|
|
269
|
-
const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0);
|
|
270
|
-
let fd;
|
|
271
|
-
try {
|
|
272
|
-
try { fd = fs.openSync(file, flags); } catch (error) {
|
|
273
|
-
if (error?.code === 'ENOENT') return null;
|
|
274
|
-
if (['ELOOP', 'EMLINK'].includes(error?.code)) {
|
|
275
|
-
throw contextPathUnsafe(ref, 'local Context ref changed to a symbolic link');
|
|
276
|
-
}
|
|
277
|
-
throw error;
|
|
278
|
-
}
|
|
279
|
-
const opened = fs.fstatSync(fd);
|
|
280
|
-
const finalIdentity = identities[identities.length - 1]?.stat;
|
|
281
|
-
if (!opened.isFile() || !sameEntry(opened, finalIdentity)) {
|
|
282
|
-
throw contextPathUnsafe(ref, 'local Context ref changed while opening');
|
|
283
|
-
}
|
|
284
|
-
for (const identity of identities) {
|
|
285
|
-
const currentStat = lstatOrNull(identity.file);
|
|
286
|
-
if (!currentStat || currentStat.isSymbolicLink()
|
|
287
|
-
|| !sameEntry(currentStat, identity.stat)
|
|
288
|
-
|| (identity.directory ? !currentStat.isDirectory() : !currentStat.isFile())) {
|
|
289
|
-
throw contextPathUnsafe(ref, 'local Context ref path changed while opening');
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
const currentPhysicalRoot = fs.realpathSync(root);
|
|
293
|
-
const currentPhysicalFile = fs.realpathSync(file);
|
|
294
|
-
if (currentPhysicalRoot !== physicalRoot
|
|
295
|
-
|| !physicallyContained(physicalRoot, currentPhysicalFile)) {
|
|
296
|
-
throw contextPathUnsafe(ref, 'local Context ref physical path changed while opening');
|
|
297
|
-
}
|
|
298
|
-
return { file, bytes: fs.readFileSync(fd) };
|
|
299
|
-
} finally {
|
|
300
|
-
if (fd !== undefined) fs.closeSync(fd);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function normalizeBudget(value, inherited) {
|
|
305
|
-
const source = value || inherited || DEFAULT_BUDGET;
|
|
306
|
-
const maxTokens = Number(source.max_tokens ?? DEFAULT_BUDGET.max_tokens);
|
|
307
|
-
const maxFiles = Number(source.max_files ?? DEFAULT_BUDGET.max_files);
|
|
308
|
-
if (!Number.isInteger(maxTokens) || maxTokens < 1 || !Number.isInteger(maxFiles) || maxFiles < 1) {
|
|
309
|
-
throw new ContextSpineError('VALIDATION_ERROR', 'context budget must use positive integer max_tokens/max_files');
|
|
310
|
-
}
|
|
311
|
-
return { max_tokens: maxTokens, max_files: maxFiles };
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function specRefToContextRef(value, role) {
|
|
315
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
316
|
-
throw new ContextSpineError('VALIDATION_ERROR', 'spec_refs entries must be objects');
|
|
317
|
-
}
|
|
318
|
-
const unknown = Object.keys(value).filter((field) => !SPEC_REF_FIELDS.has(field));
|
|
319
|
-
if (unknown.length > 0) {
|
|
320
|
-
throw new ContextSpineError(
|
|
321
|
-
'VALIDATION_ERROR', `spec_refs entry has unsupported fields: ${unknown.join(', ')}`,
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
|
-
if (value.required !== undefined && typeof value.required !== 'boolean') {
|
|
325
|
-
throw new ContextSpineError(
|
|
326
|
-
'VALIDATION_ERROR', 'spec_refs[].required must be a boolean',
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
const ref = boundedText(value.ref, 'spec_refs[].ref', MAX_REF_LENGTH);
|
|
330
|
-
return {
|
|
331
|
-
ref,
|
|
332
|
-
kind: 'spec',
|
|
333
|
-
reason: value.reason === undefined
|
|
334
|
-
? `Specification evidence for ${role}`
|
|
335
|
-
: boundedText(value.reason, 'spec_refs[].reason', MAX_REASON_LENGTH),
|
|
336
|
-
required: value.required !== false,
|
|
337
|
-
expected_digest: value.expected_digest,
|
|
338
|
-
anchor: value.anchor,
|
|
339
|
-
scope: value.scope,
|
|
340
|
-
freshness_policy: value.freshness_policy,
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function normalizeRefs(input, role, inherited) {
|
|
345
|
-
if (input.context_refs !== undefined && input.spec_refs !== undefined) {
|
|
346
|
-
throw new ContextSpineError(
|
|
347
|
-
'VALIDATION_ERROR',
|
|
348
|
-
'context_refs and spec_refs are mutually exclusive; provide exactly one reference format',
|
|
349
|
-
);
|
|
350
|
-
}
|
|
351
|
-
let refs;
|
|
352
|
-
if (input.context_refs !== undefined) refs = input.context_refs;
|
|
353
|
-
else if (input.spec_refs !== undefined) {
|
|
354
|
-
if (!Array.isArray(input.spec_refs)) {
|
|
355
|
-
throw new ContextSpineError('VALIDATION_ERROR', 'spec_refs must be an array');
|
|
356
|
-
}
|
|
357
|
-
if (input.spec_refs.length > MAX_CONTEXT_REFS) {
|
|
358
|
-
throw new ContextSpineError(
|
|
359
|
-
'VALIDATION_ERROR', `spec_refs must contain at most ${MAX_CONTEXT_REFS} entries`,
|
|
360
|
-
);
|
|
361
|
-
}
|
|
362
|
-
refs = input.spec_refs.map((value) => specRefToContextRef(value, role));
|
|
363
|
-
}
|
|
364
|
-
else refs = inherited || [];
|
|
365
|
-
if (!Array.isArray(refs)) {
|
|
366
|
-
throw new ContextSpineError('VALIDATION_ERROR', 'context_refs must be an array');
|
|
367
|
-
}
|
|
368
|
-
if (refs.length > MAX_CONTEXT_REFS) {
|
|
369
|
-
throw new ContextSpineError(
|
|
370
|
-
'VALIDATION_ERROR', `context_refs must contain at most ${MAX_CONTEXT_REFS} entries`,
|
|
371
|
-
);
|
|
372
|
-
}
|
|
373
|
-
return refs.map((value, index) => {
|
|
374
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
375
|
-
throw new ContextSpineError('VALIDATION_ERROR', `context_refs[${index}] must be an object`);
|
|
376
|
-
}
|
|
377
|
-
const ref = boundedText(
|
|
378
|
-
value.ref || value.path || value.reference,
|
|
379
|
-
`context_refs[${index}].ref`,
|
|
380
|
-
MAX_REF_LENGTH,
|
|
381
|
-
);
|
|
382
|
-
const kind = value.kind || 'source';
|
|
383
|
-
if (!CONTEXT_KINDS.has(kind)) {
|
|
384
|
-
throw new ContextSpineError('VALIDATION_ERROR', `unsupported context kind: ${kind}`);
|
|
385
|
-
}
|
|
386
|
-
const reason = boundedText(
|
|
387
|
-
value.reason, `context_refs[${index}].reason`, MAX_REASON_LENGTH,
|
|
388
|
-
);
|
|
389
|
-
const expectedDigest = value.expected_digest ?? value.digest ?? null;
|
|
390
|
-
if (expectedDigest !== null && !/^[0-9a-f]{64}$/.test(expectedDigest)) {
|
|
391
|
-
throw new ContextSpineError(
|
|
392
|
-
'VALIDATION_ERROR', `context_refs[${index}].expected_digest must be sha256`,
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
const freshnessPolicy = value.freshness_policy
|
|
396
|
-
|| (expectedDigest ? 'digest' : 'existence');
|
|
397
|
-
if (!['digest', 'existence', 'advisory'].includes(freshnessPolicy)) {
|
|
398
|
-
throw new ContextSpineError(
|
|
399
|
-
'VALIDATION_ERROR',
|
|
400
|
-
`context_refs[${index}].freshness_policy must be digest, existence, or advisory`,
|
|
401
|
-
);
|
|
402
|
-
}
|
|
403
|
-
const normalized = {
|
|
404
|
-
ref, kind, reason, required: value.required !== false,
|
|
405
|
-
expected_digest: expectedDigest,
|
|
406
|
-
freshness_policy: freshnessPolicy,
|
|
407
|
-
};
|
|
408
|
-
if (value.anchor !== undefined) {
|
|
409
|
-
normalized.anchor = boundedText(
|
|
410
|
-
value.anchor, `context_refs[${index}].anchor`, MAX_LOCATOR_LENGTH,
|
|
411
|
-
);
|
|
412
|
-
}
|
|
413
|
-
if (value.scope !== undefined) {
|
|
414
|
-
normalized.scope = boundedText(
|
|
415
|
-
value.scope, `context_refs[${index}].scope`, MAX_LOCATOR_LENGTH,
|
|
416
|
-
);
|
|
417
|
-
}
|
|
418
|
-
return normalized;
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
function authoritativeTaskRefs(input, role, task) {
|
|
423
|
-
if (!task) return null;
|
|
424
|
-
const dbRefs = normalizeRefs({ context_refs: task.context_refs || [] }, role, []);
|
|
425
|
-
if (input.context_refs === undefined && input.spec_refs === undefined) return dbRefs;
|
|
426
|
-
const supplied = normalizeRefs(input, role, []);
|
|
427
|
-
const comparable = (items) => items.map((item) => ({
|
|
428
|
-
ref: item.ref,
|
|
429
|
-
kind: item.kind,
|
|
430
|
-
reason: item.reason,
|
|
431
|
-
required: item.required,
|
|
432
|
-
expected_digest: item.expected_digest,
|
|
433
|
-
anchor: item.anchor,
|
|
434
|
-
scope: item.scope,
|
|
435
|
-
freshness_policy: item.freshness_policy,
|
|
436
|
-
}));
|
|
437
|
-
if (JSON.stringify(comparable(supplied)) !== JSON.stringify(comparable(dbRefs))) {
|
|
438
|
-
throw new ContextSpineError(
|
|
439
|
-
'EXECUTION_CONTEXT_REFS_CONFLICT',
|
|
440
|
-
`context_refs conflict with task ${task.id} authority; update the task contract first`,
|
|
441
|
-
);
|
|
442
|
-
}
|
|
443
|
-
return dbRefs;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
function inspectRef(rootDir, role, value) {
|
|
447
|
-
if (value.kind === 'external') {
|
|
448
|
-
return { ...value, role, status: 'external', digest: value.expected_digest, estimated_tokens: 0 };
|
|
449
|
-
}
|
|
450
|
-
const read = readProjectFile(rootDir, value.ref);
|
|
451
|
-
if (!read) {
|
|
452
|
-
return { ...value, role, status: 'missing', digest: null, estimated_tokens: 0 };
|
|
453
|
-
}
|
|
454
|
-
const content = read.bytes;
|
|
455
|
-
const digest = crypto.createHash('sha256').update(content).digest('hex');
|
|
456
|
-
const status = value.freshness_policy !== 'existence'
|
|
457
|
-
&& value.expected_digest && value.expected_digest !== digest ? 'stale' : 'current';
|
|
458
|
-
return {
|
|
459
|
-
...value, role, status, digest,
|
|
460
|
-
estimated_tokens: Math.max(1, Math.ceil(content.byteLength / 4)),
|
|
461
|
-
};
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
function taskNeighborhood(tasks, task, role) {
|
|
465
|
-
if (!task || role !== 'implement') return tasks;
|
|
466
|
-
const selected = new Set([task.id, ...(task.deps || [])]);
|
|
467
|
-
for (const candidate of tasks) {
|
|
468
|
-
const dependencies = candidate.deps || [];
|
|
469
|
-
if (dependencies.includes(task.id)) selected.add(candidate.id);
|
|
470
|
-
if (candidate.slice_kind === 'integration_checkpoint'
|
|
471
|
-
&& dependencies.some((dependency) => selected.has(dependency))) {
|
|
472
|
-
selected.add(candidate.id);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
return tasks.filter((candidate) => selected.has(candidate.id));
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
function taskContextContract(change, task) {
|
|
479
|
-
if (!task) return null;
|
|
480
|
-
const contract = change.contract || parseJson(change.contract_json, {});
|
|
481
|
-
return {
|
|
482
|
-
purpose: task.outcome,
|
|
483
|
-
why: task.trace_to,
|
|
484
|
-
constraints: contract.constraints || [],
|
|
485
|
-
non_goals: contract.non_goals || [],
|
|
486
|
-
target: {
|
|
487
|
-
public_seams: [task.public_seam].filter(Boolean),
|
|
488
|
-
files: task.files_modified || [],
|
|
489
|
-
},
|
|
490
|
-
pattern_refs: (task.context_refs || []).map((item) => ({
|
|
491
|
-
ref: item.ref,
|
|
492
|
-
reason: item.reason,
|
|
493
|
-
anchor: item.anchor || null,
|
|
494
|
-
scope: item.scope || null,
|
|
495
|
-
})),
|
|
496
|
-
acceptance: task.acceptance || [],
|
|
497
|
-
documentation_impact: task.docs_impact || {},
|
|
498
|
-
recovery: contract.recovery || null,
|
|
499
|
-
definition_of_drift: {
|
|
500
|
-
change_state_digest: changeStateDigest(change),
|
|
501
|
-
task_contract_digest: taskContractDigest(task),
|
|
502
|
-
required_context_freshness: (task.context_refs || []).map((item) => ({
|
|
503
|
-
ref: item.ref,
|
|
504
|
-
expected_digest: item.expected_digest || item.digest || null,
|
|
505
|
-
freshness_policy: item.freshness_policy
|
|
506
|
-
|| ((item.expected_digest || item.digest) ? 'digest' : 'existence'),
|
|
507
|
-
})),
|
|
508
|
-
},
|
|
509
|
-
};
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
function estimateInlineTokens(value) {
|
|
513
|
-
return Math.max(1, Math.ceil(Buffer.byteLength(JSON.stringify(value), 'utf8') / 4));
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
function normalizeExecutionContract(input, inherited, { task }) {
|
|
517
|
-
const taskContract = task
|
|
518
|
-
&& task.slice_kind && task.public_seam && task.verification_command ? {
|
|
519
|
-
slice_kind: task.slice_kind,
|
|
520
|
-
public_seam: task.public_seam,
|
|
521
|
-
verification_command: task.verification_command,
|
|
522
|
-
context_budget_percent: input.execution_contract?.context_budget_percent
|
|
523
|
-
?? inherited?.context_budget_percent
|
|
524
|
-
?? 40,
|
|
525
|
-
} : null;
|
|
526
|
-
if (input.execution_contract) {
|
|
527
|
-
for (const field of ['slice_kind', 'public_seam', 'verification_command']) {
|
|
528
|
-
if (input.execution_contract[field] !== undefined) {
|
|
529
|
-
throw new ContextSpineError(
|
|
530
|
-
'EXECUTION_CONTRACT_CONFLICT',
|
|
531
|
-
`execution_contract.${field} is owned by the task row and cannot be supplied by Prompt input`,
|
|
532
|
-
);
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
if (!task && input.execution_contract !== undefined) {
|
|
537
|
-
throw new ContextSpineError(
|
|
538
|
-
'EXECUTION_CONTRACT_CONFLICT',
|
|
539
|
-
'execution_contract is valid only for a DB-backed task context',
|
|
540
|
-
);
|
|
541
|
-
}
|
|
542
|
-
const source = taskContract || input.execution_contract || inherited || null;
|
|
543
|
-
const taskId = task?.id || null;
|
|
544
|
-
if (!taskId) return null;
|
|
545
|
-
if (!source || typeof source !== 'object' || Array.isArray(source)) return null;
|
|
546
|
-
const sliceKind = source.slice_kind || 'tracer_bullet';
|
|
547
|
-
if (!SLICE_KINDS.has(sliceKind)) {
|
|
548
|
-
throw new ContextSpineError('VALIDATION_ERROR', `unsupported slice kind: ${sliceKind}`);
|
|
549
|
-
}
|
|
550
|
-
const percent = Number(source.context_budget_percent ?? 40);
|
|
551
|
-
if (!Number.isInteger(percent) || percent < 1 || percent > 100) {
|
|
552
|
-
throw new ContextSpineError('VALIDATION_ERROR', 'context_budget_percent must be an integer from 1 to 100');
|
|
553
|
-
}
|
|
554
|
-
return {
|
|
555
|
-
slice_kind: sliceKind,
|
|
556
|
-
public_seam: typeof source.public_seam === 'string' ? source.public_seam.trim() : '',
|
|
557
|
-
verification_command: typeof source.verification_command === 'string'
|
|
558
|
-
? source.verification_command.trim()
|
|
559
|
-
: '',
|
|
560
|
-
context_budget_percent: percent,
|
|
561
|
-
};
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
function defaultGate(role) {
|
|
565
|
-
return {
|
|
566
|
-
plan: 'planning', implement: 'implementation', check: 'verification', review: 'review',
|
|
567
|
-
}[role];
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
function deriveTransitions({
|
|
571
|
-
change,
|
|
572
|
-
tasks = [],
|
|
573
|
-
task,
|
|
574
|
-
role,
|
|
575
|
-
readiness,
|
|
576
|
-
blockers = [],
|
|
577
|
-
activeWorkflow = null,
|
|
578
|
-
deliveryReady = false,
|
|
579
|
-
}) {
|
|
580
|
-
const unique = (values) => [...new Set(values.filter(Boolean))];
|
|
581
|
-
const has = (prefix) => blockers.some((blocker) => blocker === prefix || blocker.startsWith(`${prefix}:`));
|
|
582
|
-
if (has('STATE_DB_UNREADABLE') || has('STATE_SCHEMA_INCOMPLETE') || has('STATE_AUTHORITY_CONFLICT')) {
|
|
583
|
-
return {
|
|
584
|
-
allowed_transitions: ['ultra-doctor', 'ultra-status'],
|
|
585
|
-
required_transition: 'ultra-doctor',
|
|
586
|
-
};
|
|
587
|
-
}
|
|
588
|
-
if (has('STATE_DB_MISSING') || has('BASELINE_MISSING')) {
|
|
589
|
-
return {
|
|
590
|
-
allowed_transitions: ['ultra-init', 'ultra-status'],
|
|
591
|
-
required_transition: 'ultra-init',
|
|
592
|
-
};
|
|
593
|
-
}
|
|
594
|
-
if (blockers.some((blocker) => blocker.startsWith('DECISION_'))) {
|
|
595
|
-
return {
|
|
596
|
-
allowed_transitions: ['ultra-think', 'ultra-status'],
|
|
597
|
-
required_transition: 'ultra-think',
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
if (blockers.some((blocker) => blocker.startsWith('BASELINE_SCHEMA_')
|
|
601
|
-
|| blocker.startsWith('STATE_SCHEMA_MIGRATION_'))) {
|
|
602
|
-
return {
|
|
603
|
-
allowed_transitions: ['ultra-doctor', 'ultra-init', 'ultra-status'],
|
|
604
|
-
required_transition: 'ultra-doctor',
|
|
605
|
-
};
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
const transitions = [];
|
|
609
|
-
if (activeWorkflow) transitions.push(`ultra-${activeWorkflow.kind}`);
|
|
610
|
-
if (!change) {
|
|
611
|
-
transitions.push('ultra-research', 'ultra-change', 'ultra-think', 'ultra-status', 'ultra-doctor');
|
|
612
|
-
return { allowed_transitions: unique(transitions), required_transition: null };
|
|
613
|
-
}
|
|
614
|
-
if (readiness === 'blocked') {
|
|
615
|
-
if (blockers.some((blocker) => blocker.startsWith('CONTEXT_')
|
|
616
|
-
|| blocker.startsWith('EXECUTION_'))) {
|
|
617
|
-
transitions.push('change.context');
|
|
618
|
-
}
|
|
619
|
-
if (blockers.some((blocker) => blocker.startsWith('BASELINE_'))) {
|
|
620
|
-
transitions.push('ultra-research', 'ultra-doctor');
|
|
621
|
-
}
|
|
622
|
-
transitions.push('ultra-change', 'ultra-think', 'ultra-status');
|
|
623
|
-
return { allowed_transitions: unique(transitions), required_transition: null };
|
|
624
|
-
}
|
|
625
|
-
if (change.status === 'ready') {
|
|
626
|
-
transitions.push('ultra-deliver', 'ultra-review', 'ultra-test');
|
|
627
|
-
} else if (change.status === 'blocked') {
|
|
628
|
-
transitions.push('ultra-think', 'ultra-change');
|
|
629
|
-
} else if (!task && tasks.length === 0) {
|
|
630
|
-
transitions.push('ultra-plan', 'ultra-research', 'ultra-change');
|
|
631
|
-
} else if (task?.status === 'pending' || task?.status === 'in_progress') {
|
|
632
|
-
transitions.push('ultra-dev', 'ultra-plan');
|
|
633
|
-
if (role === 'review') transitions.push('ultra-review');
|
|
634
|
-
} else if (tasks.length > 0 && tasks.every((row) => ['completed', 'expanded'].includes(row.status))) {
|
|
635
|
-
transitions.push('ultra-test', 'ultra-review');
|
|
636
|
-
if (deliveryReady) transitions.push('ultra-deliver');
|
|
637
|
-
} else {
|
|
638
|
-
transitions.push('ultra-plan', 'ultra-change');
|
|
639
|
-
}
|
|
640
|
-
transitions.push('ultra-think', 'ultra-status');
|
|
641
|
-
return { allowed_transitions: unique(transitions), required_transition: null };
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
function normalizeRecommendation(value) {
|
|
645
|
-
if (value === undefined || value === null) return null;
|
|
646
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
647
|
-
throw new ContextSpineError('VALIDATION_ERROR', 'recommendation must be an object');
|
|
648
|
-
}
|
|
649
|
-
const workflow = String(value.workflow || '').trim();
|
|
650
|
-
const rationale = String(value.rationale || '').trim();
|
|
651
|
-
if (!workflow || rationale.length < 3) {
|
|
652
|
-
throw new ContextSpineError(
|
|
653
|
-
'VALIDATION_ERROR', 'recommendation requires workflow and rationale',
|
|
654
|
-
);
|
|
655
|
-
}
|
|
656
|
-
return { workflow, rationale, authoritative: false };
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
function deliveryCapabilityReady(db, change, tasks, rootDir) {
|
|
660
|
-
if (!change || !Array.isArray(tasks) || tasks.length === 0
|
|
661
|
-
|| !tasks.every((row) => ['completed', 'expanded'].includes(row.status))) {
|
|
662
|
-
return false;
|
|
663
|
-
}
|
|
664
|
-
try {
|
|
665
|
-
workflows.validateDeliveryPrerequisites(db, { change_id: change.id }, rootDir);
|
|
666
|
-
return true;
|
|
667
|
-
} catch {
|
|
668
|
-
return false;
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
function latestContextSnapshot(db, {
|
|
673
|
-
change_id: changeId,
|
|
674
|
-
task_id: taskId = null,
|
|
675
|
-
role,
|
|
676
|
-
gate,
|
|
677
|
-
} = {}) {
|
|
678
|
-
if (!changeId || !role || !gate) return null;
|
|
679
|
-
return db.prepare(
|
|
680
|
-
`SELECT * FROM context_snapshots
|
|
681
|
-
WHERE change_id = ? AND task_id IS ? AND role = ? AND gate = ?
|
|
682
|
-
ORDER BY created_at DESC, rowid DESC LIMIT 1`,
|
|
683
|
-
).get(changeId, taskId, role, gate) || null;
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
function latestContext(db, binding) {
|
|
687
|
-
const row = latestContextSnapshot(db, binding);
|
|
688
|
-
return parseJson(row?.context_json, {});
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
function validateContextSnapshot(db, binding, {
|
|
692
|
-
rootDir = process.cwd(),
|
|
693
|
-
require_current_checkout = false,
|
|
694
|
-
} = {}) {
|
|
695
|
-
const snapshot = latestContextSnapshot(db, binding);
|
|
696
|
-
if (!snapshot) {
|
|
697
|
-
return {
|
|
698
|
-
snapshot: null,
|
|
699
|
-
manifest: null,
|
|
700
|
-
blockers: ['CONTEXT_SNAPSHOT_MISSING'],
|
|
701
|
-
warnings: [],
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
const blockers = [];
|
|
705
|
-
const warnings = [];
|
|
706
|
-
let manifest = null;
|
|
707
|
-
let manifestFile = null;
|
|
708
|
-
try {
|
|
709
|
-
const read = readProjectFile(rootDir, snapshot.manifest_path);
|
|
710
|
-
manifestFile = read?.file || localPath(rootDir, snapshot.manifest_path);
|
|
711
|
-
if (!read) {
|
|
712
|
-
blockers.push('CONTEXT_MANIFEST_MISSING');
|
|
713
|
-
} else {
|
|
714
|
-
const bytes = read.bytes;
|
|
715
|
-
const digest = crypto.createHash('sha256').update(bytes).digest('hex');
|
|
716
|
-
if (digest !== snapshot.manifest_hash) blockers.push('CONTEXT_MANIFEST_STALE');
|
|
717
|
-
manifest = JSON.parse(bytes.toString('utf8'));
|
|
718
|
-
}
|
|
719
|
-
} catch {
|
|
720
|
-
blockers.push('CONTEXT_MANIFEST_INVALID');
|
|
721
|
-
}
|
|
722
|
-
if (!manifest) {
|
|
723
|
-
return {
|
|
724
|
-
snapshot,
|
|
725
|
-
manifest: null,
|
|
726
|
-
blockers: [...new Set(blockers)],
|
|
727
|
-
warnings: [],
|
|
728
|
-
};
|
|
729
|
-
}
|
|
730
|
-
if (manifest.snapshot_id !== snapshot.id
|
|
731
|
-
|| manifest.change?.id !== binding.change_id
|
|
732
|
-
|| (manifest.resume?.task_id || null) !== (binding.task_id || null)
|
|
733
|
-
|| manifest.role !== binding.role
|
|
734
|
-
|| manifest.gate !== binding.gate
|
|
735
|
-
|| snapshot.role !== binding.role
|
|
736
|
-
|| snapshot.gate !== binding.gate) {
|
|
737
|
-
blockers.push('CONTEXT_BINDING_MISMATCH');
|
|
738
|
-
}
|
|
739
|
-
if (snapshot.readiness !== 'ready' || manifest.readiness?.status !== 'ready') {
|
|
740
|
-
blockers.push('CONTEXT_NOT_READY');
|
|
741
|
-
}
|
|
742
|
-
const change = db.prepare('SELECT * FROM changes WHERE id = ?').get(binding.change_id);
|
|
743
|
-
if (!change || manifest.resume?.change_state_digest !== changeStateDigest(change)) {
|
|
744
|
-
blockers.push('CONTEXT_CHANGE_CONTRACT_STALE');
|
|
745
|
-
}
|
|
746
|
-
if (binding.task_id) {
|
|
747
|
-
const task = ops.readTask(db, binding.task_id);
|
|
748
|
-
if (!task || manifest.resume?.task_contract_digest !== taskContractDigest(task)) {
|
|
749
|
-
blockers.push('CONTEXT_TASK_CONTRACT_STALE');
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
for (const item of manifest.context?.items || []) {
|
|
753
|
-
if (item.kind === 'external') continue;
|
|
754
|
-
let read;
|
|
755
|
-
try { read = readProjectFile(rootDir, item.ref); }
|
|
756
|
-
catch (error) {
|
|
757
|
-
blockers.push(
|
|
758
|
-
error?.code === 'CONTEXT_REF_UNSAFE'
|
|
759
|
-
? `CONTEXT_REF_UNSAFE:${item.ref}`
|
|
760
|
-
: `CONTEXT_REF_INVALID:${item.ref}`,
|
|
761
|
-
);
|
|
762
|
-
continue;
|
|
763
|
-
}
|
|
764
|
-
if (!read) {
|
|
765
|
-
item.freshness_status = 'missing';
|
|
766
|
-
item.current_digest = null;
|
|
767
|
-
if (item.required) blockers.push(`CONTEXT_REQUIRED_REF_MISSING:${item.ref}`);
|
|
768
|
-
else warnings.push(`CONTEXT_OPTIONAL_REF_MISSING:${item.ref}`);
|
|
769
|
-
continue;
|
|
770
|
-
}
|
|
771
|
-
if (['digest', 'advisory'].includes(item.freshness_policy)) {
|
|
772
|
-
const currentDigest = crypto.createHash('sha256').update(read.bytes).digest('hex');
|
|
773
|
-
const expectedDigest = item.expected_digest || item.digest;
|
|
774
|
-
item.current_digest = currentDigest;
|
|
775
|
-
item.freshness_status = currentDigest === expectedDigest ? 'current' : 'stale';
|
|
776
|
-
if (currentDigest !== expectedDigest) {
|
|
777
|
-
if (item.freshness_policy === 'advisory') {
|
|
778
|
-
warnings.push(`CONTEXT_REF_STALE_ADVISORY:${item.ref}`);
|
|
779
|
-
} else if (item.required) {
|
|
780
|
-
blockers.push(`CONTEXT_REQUIRED_REF_STALE:${item.ref}`);
|
|
781
|
-
} else {
|
|
782
|
-
warnings.push(`CONTEXT_OPTIONAL_REF_STALE:${item.ref}`);
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
} else {
|
|
786
|
-
item.current_digest = item.digest || null;
|
|
787
|
-
item.freshness_status = 'current';
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
if (require_current_checkout) {
|
|
791
|
-
const checkout = baselines.gitWorktreeSnapshot(rootDir, ['.']);
|
|
792
|
-
if (checkout.head && manifest.git?.head !== checkout.head) blockers.push('CONTEXT_HEAD_STALE');
|
|
793
|
-
if (checkout.digest && manifest.git?.worktree_digest !== checkout.digest) {
|
|
794
|
-
blockers.push('CONTEXT_WORKTREE_STALE');
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
return {
|
|
798
|
-
snapshot,
|
|
799
|
-
manifest,
|
|
800
|
-
blockers: [...new Set(blockers)],
|
|
801
|
-
warnings: [...new Set(warnings)],
|
|
802
|
-
};
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
function compileRoleContext(db, { input, change, tasks, rootDir }) {
|
|
806
|
-
if (input.next_action !== undefined) {
|
|
807
|
-
throw new ContextSpineError(
|
|
808
|
-
'CONTEXT_LEGACY_NEXT_ACTION_UNSUPPORTED',
|
|
809
|
-
'next_action is not an authority field; provide a non-authoritative recommendation instead',
|
|
810
|
-
);
|
|
811
|
-
}
|
|
812
|
-
const task = input.task_id ? tasks.find((row) => row.id === input.task_id) : null;
|
|
813
|
-
if (input.task_id && !task) {
|
|
814
|
-
throw new ContextSpineError('TASK_NOT_FOUND', `task ${input.task_id} is not linked to change ${change.id}`);
|
|
815
|
-
}
|
|
816
|
-
const role = input.role || (input.task_id ? 'implement' : 'plan');
|
|
817
|
-
if (!ROLES.has(role)) throw new ContextSpineError('VALIDATION_ERROR', `unsupported context role: ${role}`);
|
|
818
|
-
const gate = input.gate || defaultGate(role);
|
|
819
|
-
if (!GATES.has(gate)) throw new ContextSpineError('VALIDATION_ERROR', `unsupported workflow gate: ${gate}`);
|
|
820
|
-
const inherited = latestContext(db, {
|
|
821
|
-
change_id: change.id,
|
|
822
|
-
task_id: input.task_id || null,
|
|
823
|
-
role,
|
|
824
|
-
gate,
|
|
825
|
-
});
|
|
826
|
-
|
|
827
|
-
const refs = authoritativeTaskRefs(input, role, task)
|
|
828
|
-
|| normalizeRefs(input, role, inherited.context?.items || []);
|
|
829
|
-
const items = refs.map((value) => inspectRef(rootDir, role, value));
|
|
830
|
-
const budget = normalizeBudget(input.budget, inherited.context?.budget);
|
|
831
|
-
const scopedTasks = taskNeighborhood(tasks, task, role);
|
|
832
|
-
const executionContract = normalizeExecutionContract(
|
|
833
|
-
input, inherited.execution_contract, { task },
|
|
834
|
-
);
|
|
835
|
-
const taskContract = taskContextContract(change, task);
|
|
836
|
-
const fileTokenEstimate = items.reduce((sum, item) => sum + item.estimated_tokens, 0);
|
|
837
|
-
const inlineTokenEstimate = estimateInlineTokens({
|
|
838
|
-
change: {
|
|
839
|
-
id: change.id,
|
|
840
|
-
kind: change.kind,
|
|
841
|
-
intent: change.intent,
|
|
842
|
-
contract: change.contract || parseJson(change.contract_json, {}),
|
|
843
|
-
docs_impact: change.docs_impact || parseJson(change.docs_impact_json, {}),
|
|
844
|
-
},
|
|
845
|
-
tasks: scopedTasks,
|
|
846
|
-
execution_contract: executionContract,
|
|
847
|
-
task_context_contract: taskContract,
|
|
848
|
-
context_refs: items.map((item) => ({
|
|
849
|
-
ref: item.ref,
|
|
850
|
-
kind: item.kind,
|
|
851
|
-
reason: item.reason,
|
|
852
|
-
required: item.required,
|
|
853
|
-
expected_digest: item.expected_digest,
|
|
854
|
-
anchor: item.anchor,
|
|
855
|
-
scope: item.scope,
|
|
856
|
-
freshness_policy: item.freshness_policy,
|
|
857
|
-
})),
|
|
858
|
-
});
|
|
859
|
-
const tokenEstimate = fileTokenEstimate + inlineTokenEstimate;
|
|
860
|
-
const fileCount = items.filter((item) => item.status !== 'external').length;
|
|
861
|
-
const blockers = [];
|
|
862
|
-
const warnings = [];
|
|
863
|
-
const baselineHealth = baselines.inspectBaseline(db, { rootDir });
|
|
864
|
-
const checkout = baselines.gitWorktreeSnapshot(rootDir, ['.']);
|
|
865
|
-
const baselineGate = baselineGateForChange(db, change, baselineHealth);
|
|
866
|
-
blockers.push(...baselineGate.blockers);
|
|
867
|
-
warnings.push(...baselineGate.warnings);
|
|
868
|
-
for (const item of items) {
|
|
869
|
-
if (item.required && item.status === 'missing') blockers.push(`CONTEXT_REQUIRED_REF_MISSING:${item.ref}`);
|
|
870
|
-
if (!item.required && item.status === 'missing') {
|
|
871
|
-
warnings.push(`CONTEXT_OPTIONAL_REF_MISSING:${item.ref}`);
|
|
872
|
-
}
|
|
873
|
-
if (item.status === 'stale') {
|
|
874
|
-
if (item.freshness_policy === 'advisory') {
|
|
875
|
-
warnings.push(`CONTEXT_REF_STALE_ADVISORY:${item.ref}`);
|
|
876
|
-
} else if (item.required) {
|
|
877
|
-
blockers.push(`CONTEXT_REQUIRED_REF_STALE:${item.ref}`);
|
|
878
|
-
} else {
|
|
879
|
-
warnings.push(`CONTEXT_OPTIONAL_REF_STALE:${item.ref}`);
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
if (fileCount > budget.max_files) warnings.push('CONTEXT_FILE_BUDGET_EXCEEDED');
|
|
884
|
-
if (tokenEstimate > budget.max_tokens) warnings.push('CONTEXT_TOKEN_BUDGET_EXCEEDED');
|
|
885
|
-
if (input.task_id) {
|
|
886
|
-
if (!executionContract) blockers.push('EXECUTION_CONTRACT_MISSING');
|
|
887
|
-
else {
|
|
888
|
-
if (!executionContract.public_seam) blockers.push('EXECUTION_PUBLIC_SEAM_MISSING');
|
|
889
|
-
if (!executionContract.verification_command) blockers.push('EXECUTION_VERIFICATION_COMMAND_MISSING');
|
|
890
|
-
if (executionContract.slice_kind !== 'expand_contract'
|
|
891
|
-
&& executionContract.context_budget_percent > 40) {
|
|
892
|
-
warnings.push('EXECUTION_CONTEXT_BUDGET_ADVISORY');
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
const readiness = blockers.length === 0 ? 'ready' : 'blocked';
|
|
897
|
-
const control = deriveTransitions({
|
|
898
|
-
change,
|
|
899
|
-
tasks,
|
|
900
|
-
task,
|
|
901
|
-
role,
|
|
902
|
-
readiness,
|
|
903
|
-
blockers,
|
|
904
|
-
deliveryReady: deliveryCapabilityReady(db, change, tasks, rootDir),
|
|
905
|
-
});
|
|
906
|
-
return {
|
|
907
|
-
role,
|
|
908
|
-
gate,
|
|
909
|
-
control,
|
|
910
|
-
recommendation: normalizeRecommendation(input.recommendation),
|
|
911
|
-
readiness: { status: readiness, blockers, warnings: [...new Set(warnings)] },
|
|
912
|
-
context: {
|
|
913
|
-
items,
|
|
914
|
-
budget,
|
|
915
|
-
token_estimate: tokenEstimate,
|
|
916
|
-
file_token_estimate: fileTokenEstimate,
|
|
917
|
-
inline_token_estimate: inlineTokenEstimate,
|
|
918
|
-
file_count: fileCount,
|
|
919
|
-
lazy_file_refs: true,
|
|
920
|
-
},
|
|
921
|
-
execution_contract: executionContract,
|
|
922
|
-
task_context_contract: taskContract,
|
|
923
|
-
task_scope: scopedTasks,
|
|
924
|
-
baseline: baselineHealth.baseline ? {
|
|
925
|
-
id: baselineHealth.baseline.id,
|
|
926
|
-
mode: baselineHealth.baseline.mode,
|
|
927
|
-
status: baselineHealth.baseline.status,
|
|
928
|
-
repository_revision: baselineHealth.baseline.repository_revision,
|
|
929
|
-
health: baselineHealth.status,
|
|
930
|
-
warnings: [...new Set([...baselineHealth.blockers, ...baselineHealth.warnings])],
|
|
931
|
-
} : null,
|
|
932
|
-
selected_task: task,
|
|
933
|
-
resume: {
|
|
934
|
-
change_id: change.id,
|
|
935
|
-
change_state_digest: changeStateDigest(change),
|
|
936
|
-
task_id: task?.id || null,
|
|
937
|
-
task_status: task?.status || null,
|
|
938
|
-
task_state_digest: taskStateDigest(task),
|
|
939
|
-
task_contract_digest: taskContractDigest(task),
|
|
940
|
-
session_id: task?.session_id || null,
|
|
941
|
-
role,
|
|
942
|
-
gate,
|
|
943
|
-
git_head: checkout.head,
|
|
944
|
-
worktree_state: checkout.state,
|
|
945
|
-
worktree_digest: checkout.digest,
|
|
946
|
-
},
|
|
947
|
-
};
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
function actionableBaselineWorkflow(db, baselineId, rootDir) {
|
|
951
|
-
if (!baselineId) return null;
|
|
952
|
-
const candidates = workflows.listWorkflows(db, { baseline_id: baselineId, limit: 100 }, { rootDir })
|
|
953
|
-
.filter((run) => ['active', 'blocked', 'ready'].includes(run.status));
|
|
954
|
-
return candidates.find((run) => run.kind === 'research')
|
|
955
|
-
|| candidates.find((run) => run.kind === 'init')
|
|
956
|
-
|| candidates[0]
|
|
957
|
-
|| null;
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
function actionableChangeWorkflow(db, changeId, rootDir) {
|
|
961
|
-
const candidates = workflows.listWorkflows(db, { change_id: changeId, limit: 100 }, { rootDir })
|
|
962
|
-
.filter((run) => ['active', 'blocked', 'ready'].includes(run.status));
|
|
963
|
-
return candidates.find((run) => run.status === 'blocked') || candidates[0] || null;
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
function summarizeWorkflow(run) {
|
|
967
|
-
if (!run) return null;
|
|
968
|
-
return {
|
|
969
|
-
id: run.id, kind: run.kind, mode: run.mode, status: run.status,
|
|
970
|
-
current_step: run.current_step, blockers: run.blockers,
|
|
971
|
-
};
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
function summarizeDecision(gate) {
|
|
975
|
-
if (!gate?.thread) return null;
|
|
976
|
-
const current = gate.current_decision;
|
|
977
|
-
return {
|
|
978
|
-
thread_id: gate.thread.id,
|
|
979
|
-
status: gate.thread.status,
|
|
980
|
-
mode: gate.thread.mode,
|
|
981
|
-
current: current ? {
|
|
982
|
-
id: current.id,
|
|
983
|
-
phase: current.phase,
|
|
984
|
-
question: current.question,
|
|
985
|
-
why_now: current.why_now,
|
|
986
|
-
recommendation: current.recommendation,
|
|
987
|
-
options: current.options,
|
|
988
|
-
effects: current.effects,
|
|
989
|
-
} : null,
|
|
990
|
-
};
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
994
|
-
const baselineHealth = baselines.inspectBaseline(db, { rootDir });
|
|
995
|
-
const change = id
|
|
996
|
-
? db.prepare('SELECT * FROM changes WHERE id = ?').get(id)
|
|
997
|
-
: db.prepare(
|
|
998
|
-
"SELECT * FROM changes WHERE status IN ('active', 'blocked', 'ready') ORDER BY updated_at DESC LIMIT 1",
|
|
999
|
-
).get();
|
|
1000
|
-
if (id && !change) {
|
|
1001
|
-
throw new ContextSpineError('CHANGE_NOT_FOUND', `change ${id} not found`);
|
|
1002
|
-
}
|
|
1003
|
-
if (!change) {
|
|
1004
|
-
const baseline = baselineHealth.baseline;
|
|
1005
|
-
const activeWorkflow = actionableBaselineWorkflow(db, baseline?.id, rootDir);
|
|
1006
|
-
const decisionGate = decisions.decisionGate(
|
|
1007
|
-
db, {
|
|
1008
|
-
baseline_id: baseline?.id || null,
|
|
1009
|
-
workflow_run_id: activeWorkflow?.id || null,
|
|
1010
|
-
}, { rootDir },
|
|
1011
|
-
);
|
|
1012
|
-
const acceptedIntent = decisions.acceptedIntent(db, {
|
|
1013
|
-
baseline_id: baseline?.id || null,
|
|
1014
|
-
workflow_run_id: activeWorkflow?.id || null,
|
|
1015
|
-
});
|
|
1016
|
-
if (!decisionGate.ready) {
|
|
1017
|
-
return {
|
|
1018
|
-
change_id: null, task_id: null, role: 'plan', gate: 'alignment', readiness: 'blocked',
|
|
1019
|
-
blockers: [...new Set([...baselineHealth.blockers, ...decisionGate.blockers])],
|
|
1020
|
-
warnings: baselineHealth.warnings,
|
|
1021
|
-
allowed_transitions: ['ultra-think', 'ultra-status'], required_transition: 'ultra-think',
|
|
1022
|
-
workflow: summarizeWorkflow(activeWorkflow),
|
|
1023
|
-
decision: summarizeDecision(decisionGate),
|
|
1024
|
-
accepted_intent: acceptedIntent,
|
|
1025
|
-
context_manifest_path: null, context_manifest_hash: null, git_head: currentHead(rootDir),
|
|
1026
|
-
baseline: baseline ? {
|
|
1027
|
-
id: baseline.id, mode: baseline.mode, status: baseline.status,
|
|
1028
|
-
repository_revision: baseline.repository_revision,
|
|
1029
|
-
} : null,
|
|
1030
|
-
};
|
|
1031
|
-
}
|
|
1032
|
-
if (baselineHealth.status !== 'pass') {
|
|
1033
|
-
const control = baseline
|
|
1034
|
-
? {
|
|
1035
|
-
allowed_transitions: uniqueStrings([
|
|
1036
|
-
activeWorkflow ? `ultra-${activeWorkflow.kind}` : 'ultra-research',
|
|
1037
|
-
'ultra-status',
|
|
1038
|
-
'ultra-doctor',
|
|
1039
|
-
]),
|
|
1040
|
-
required_transition: null,
|
|
1041
|
-
}
|
|
1042
|
-
: {
|
|
1043
|
-
allowed_transitions: ['ultra-init', 'ultra-status'],
|
|
1044
|
-
required_transition: 'ultra-init',
|
|
1045
|
-
};
|
|
1046
|
-
return {
|
|
1047
|
-
change_id: null, task_id: null, role: 'plan', gate: 'alignment', readiness: 'blocked',
|
|
1048
|
-
blockers: baselineHealth.blockers, warnings: baselineHealth.warnings,
|
|
1049
|
-
allowed_transitions: control.allowed_transitions,
|
|
1050
|
-
required_transition: control.required_transition,
|
|
1051
|
-
workflow: summarizeWorkflow(activeWorkflow),
|
|
1052
|
-
decision: null,
|
|
1053
|
-
accepted_intent: acceptedIntent,
|
|
1054
|
-
context_manifest_path: null,
|
|
1055
|
-
context_manifest_hash: null, git_head: currentHead(rootDir),
|
|
1056
|
-
baseline: baseline ? {
|
|
1057
|
-
id: baseline.id, mode: baseline.mode, status: baseline.status,
|
|
1058
|
-
repository_revision: baseline.repository_revision,
|
|
1059
|
-
} : null,
|
|
1060
|
-
};
|
|
1061
|
-
}
|
|
1062
|
-
return {
|
|
1063
|
-
change_id: null, task_id: null, role: 'plan', gate: 'alignment', readiness: 'ready',
|
|
1064
|
-
allowed_transitions: ['ultra-change', 'ultra-research', 'ultra-think', 'ultra-status'],
|
|
1065
|
-
required_transition: null,
|
|
1066
|
-
workflow: null,
|
|
1067
|
-
decision: null,
|
|
1068
|
-
accepted_intent: acceptedIntent,
|
|
1069
|
-
context_manifest_path: null, context_manifest_hash: null, git_head: currentHead(rootDir),
|
|
1070
|
-
blockers: [], warnings: baselineHealth.warnings,
|
|
1071
|
-
baseline: {
|
|
1072
|
-
id: baselineHealth.baseline.id, mode: baselineHealth.baseline.mode,
|
|
1073
|
-
status: baselineHealth.baseline.status,
|
|
1074
|
-
repository_revision: baselineHealth.baseline.repository_revision,
|
|
1075
|
-
},
|
|
1076
|
-
};
|
|
1077
|
-
}
|
|
1078
|
-
const tasks = db.prepare('SELECT id FROM tasks WHERE change_id = ? ORDER BY created_at ASC')
|
|
1079
|
-
.all(change.id).map((row) => ops.readTask(db, row.id));
|
|
1080
|
-
const activeWorkflow = actionableChangeWorkflow(db, change.id, rootDir);
|
|
1081
|
-
const taskId = activeWorkflow?.task_id || tasks.find((row) => row.status === 'in_progress')?.id
|
|
1082
|
-
|| tasks.find((row) => row.status === 'pending')?.id || null;
|
|
1083
|
-
const task = tasks.find((row) => row.id === taskId) || null;
|
|
1084
|
-
const expectedBinding = {
|
|
1085
|
-
plan: ['plan', 'planning'],
|
|
1086
|
-
dev: ['implement', 'implementation'],
|
|
1087
|
-
test: ['check', 'verification'],
|
|
1088
|
-
review: ['review', 'review'],
|
|
1089
|
-
deliver: ['check', 'convergence'],
|
|
1090
|
-
}[activeWorkflow?.kind] || (task ? ['implement', 'implementation'] : ['plan', 'planning']);
|
|
1091
|
-
const snapshot = latestContextSnapshot(db, {
|
|
1092
|
-
change_id: change.id,
|
|
1093
|
-
task_id: expectedBinding[0] === 'implement' ? taskId : null,
|
|
1094
|
-
role: expectedBinding[0],
|
|
1095
|
-
gate: expectedBinding[1],
|
|
1096
|
-
});
|
|
1097
|
-
const context = parseJson(snapshot?.context_json, {});
|
|
1098
|
-
const checkout = baselines.gitWorktreeSnapshot(rootDir, ['.']);
|
|
1099
|
-
const head = checkout.head;
|
|
1100
|
-
const snapshotMissing = !snapshot;
|
|
1101
|
-
const legacyContext = Boolean(
|
|
1102
|
-
snapshot && (
|
|
1103
|
-
!context.resume || !context.context || !context.readiness || !('baseline' in context)
|
|
1104
|
-
|| !Object.prototype.hasOwnProperty.call(context.resume, 'worktree_digest')
|
|
1105
|
-
|| !Object.prototype.hasOwnProperty.call(context.resume, 'task_state_digest')
|
|
1106
|
-
|| !Object.prototype.hasOwnProperty.call(context.resume, 'change_state_digest')
|
|
1107
|
-
),
|
|
1108
|
-
);
|
|
1109
|
-
const headStale = Boolean(snapshot?.git_head && head && snapshot.git_head !== head);
|
|
1110
|
-
const worktreeStale = Boolean(
|
|
1111
|
-
context.resume && context.resume.worktree_digest && checkout.digest
|
|
1112
|
-
&& context.resume.worktree_digest !== checkout.digest,
|
|
1113
|
-
);
|
|
1114
|
-
const stateChanged = context.resume?.task_status !== undefined
|
|
1115
|
-
&& context.resume.task_status !== (task?.status || null);
|
|
1116
|
-
const taskContractChanged = Boolean(
|
|
1117
|
-
context.resume && (
|
|
1118
|
-
context.resume.task_contract_digest
|
|
1119
|
-
? context.resume.task_contract_digest !== taskContractDigest(task)
|
|
1120
|
-
: context.resume.task_state_digest !== taskStateDigest(task)
|
|
1121
|
-
),
|
|
1122
|
-
);
|
|
1123
|
-
const changeContractChanged = Boolean(
|
|
1124
|
-
context.resume && context.resume.change_state_digest !== changeStateDigest(change),
|
|
1125
|
-
);
|
|
1126
|
-
const blockers = [];
|
|
1127
|
-
const baselineGate = baselineGateForChange(db, change, baselineHealth);
|
|
1128
|
-
const decisionGate = decisions.decisionGate(db, {
|
|
1129
|
-
baseline_id: baselineHealth.baseline?.id || null,
|
|
1130
|
-
change_id: change.id,
|
|
1131
|
-
workflow_run_id: activeWorkflow?.id || null,
|
|
1132
|
-
}, { rootDir });
|
|
1133
|
-
const acceptedIntent = decisions.acceptedIntent(db, {
|
|
1134
|
-
baseline_id: baselineHealth.baseline?.id || null,
|
|
1135
|
-
change_id: change.id,
|
|
1136
|
-
workflow_run_id: activeWorkflow?.id || null,
|
|
1137
|
-
});
|
|
1138
|
-
const warnings = [...baselineGate.warnings];
|
|
1139
|
-
blockers.push(...baselineGate.blockers);
|
|
1140
|
-
blockers.push(...decisionGate.blockers);
|
|
1141
|
-
if (snapshotMissing && tasks.length > 0) blockers.push('CONTEXT_NOT_COMPILED');
|
|
1142
|
-
else if (snapshotMissing) warnings.push('CONTEXT_NOT_COMPILED');
|
|
1143
|
-
else {
|
|
1144
|
-
if (legacyContext) blockers.push('CONTEXT_SNAPSHOT_UPGRADE_REQUIRED');
|
|
1145
|
-
if (headStale) blockers.push('CONTEXT_HEAD_STALE');
|
|
1146
|
-
if (worktreeStale) blockers.push('CONTEXT_WORKTREE_STALE');
|
|
1147
|
-
if (stateChanged) blockers.push('CONTEXT_TASK_STATE_STALE');
|
|
1148
|
-
if (taskContractChanged) blockers.push('CONTEXT_TASK_CONTRACT_STALE');
|
|
1149
|
-
if (changeContractChanged) blockers.push('CONTEXT_CHANGE_CONTRACT_STALE');
|
|
1150
|
-
for (const blocker of parseJson(snapshot.blockers_json, [])) {
|
|
1151
|
-
if (ADVISORY_CONTEXT_CODES.has(blocker)
|
|
1152
|
-
|| (blocker.startsWith('BASELINE_') && baselineGate.blockers.length === 0)) {
|
|
1153
|
-
if (!warnings.includes(blocker)) warnings.push(blocker);
|
|
1154
|
-
} else if (!blockers.includes(blocker)) blockers.push(blocker);
|
|
1155
|
-
}
|
|
1156
|
-
for (const warning of context.readiness?.warnings || []) {
|
|
1157
|
-
if (!warnings.includes(warning)) warnings.push(warning);
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
const readiness = blockers.length > 0 ? 'blocked' : (snapshot?.readiness || 'ready');
|
|
1161
|
-
const control = deriveTransitions({
|
|
1162
|
-
change,
|
|
1163
|
-
tasks,
|
|
1164
|
-
task,
|
|
1165
|
-
role: snapshot?.role || 'plan',
|
|
1166
|
-
readiness,
|
|
1167
|
-
blockers,
|
|
1168
|
-
activeWorkflow,
|
|
1169
|
-
deliveryReady: deliveryCapabilityReady(db, change, tasks, rootDir),
|
|
1170
|
-
});
|
|
1171
|
-
return {
|
|
1172
|
-
change_id: change.id,
|
|
1173
|
-
change_status: change.status,
|
|
1174
|
-
task_id: task?.id || null,
|
|
1175
|
-
task_status: task?.status || null,
|
|
1176
|
-
session_id: task?.session_id || null,
|
|
1177
|
-
role: snapshot?.role || 'plan',
|
|
1178
|
-
gate: decisionGate.ready ? (snapshot?.gate || 'alignment') : 'alignment',
|
|
1179
|
-
readiness,
|
|
1180
|
-
blockers,
|
|
1181
|
-
warnings: [...new Set(warnings)],
|
|
1182
|
-
allowed_transitions: control.allowed_transitions,
|
|
1183
|
-
required_transition: control.required_transition,
|
|
1184
|
-
context_manifest_path: snapshot?.manifest_path || null,
|
|
1185
|
-
context_manifest_hash: snapshot?.manifest_hash || null,
|
|
1186
|
-
git_head: head,
|
|
1187
|
-
baseline: baselineHealth.baseline ? {
|
|
1188
|
-
id: baselineHealth.baseline.id, mode: baselineHealth.baseline.mode,
|
|
1189
|
-
status: baselineHealth.baseline.status,
|
|
1190
|
-
repository_revision: baselineHealth.baseline.repository_revision,
|
|
1191
|
-
} : null,
|
|
1192
|
-
workflow: summarizeWorkflow(activeWorkflow),
|
|
1193
|
-
decision: summarizeDecision(decisionGate),
|
|
1194
|
-
accepted_intent: acceptedIntent,
|
|
1195
|
-
};
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
module.exports = {
|
|
1199
|
-
ContextSpineError,
|
|
1200
|
-
baselineGateForChange,
|
|
1201
|
-
changeStateDigest,
|
|
1202
|
-
compileRoleContext,
|
|
1203
|
-
deriveTransitions,
|
|
1204
|
-
latestContextSnapshot,
|
|
1205
|
-
readBreadcrumb,
|
|
1206
|
-
taskContractDigest,
|
|
1207
|
-
validateContextSnapshot,
|
|
1208
|
-
};
|