ultra-builder-pro-cli 0.20.0 → 0.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ultra-template/docs/research/README.md +26 -5
- package/.ultra-template/specs/discovery.md +1 -1
- package/.ultra-template/tasks/tasks.json +1 -1
- package/CHANGELOG.md +66 -0
- package/README.md +54 -24
- package/adapters/_shared/codex-assets.cjs +69 -11
- package/adapters/_shared/interaction-contract.cjs +36 -3
- package/adapters/_shared/runtime-assets.cjs +41 -1
- package/adapters/claude.js +7 -5
- package/adapters/codex.js +1 -1
- package/adapters/kimi.js +2 -8
- package/adapters/opencode.js +30 -28
- package/bin/orchestrator.js +64 -24
- package/docs/AGENT-CONTEXT.md +74 -43
- package/docs/ARCHITECTURE.md +63 -31
- package/docs/ARTIFACT-AUTHORITY.md +234 -0
- package/docs/DECISIONS.md +10 -4
- package/docs/LEGACY-CLI-CROSSWALK.md +6 -4
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +4 -3
- package/docs/ROADMAP.md +19 -5
- package/docs/RUNTIME-COMPAT-MATRIX.md +24 -13
- package/docs/STATE-DB-ACCESS-POLICY.md +11 -9
- package/docs/WORKFLOW-LIFECYCLE.md +70 -39
- package/hooks/README.md +6 -5
- package/hooks/active_task_context.py +97 -12
- package/hooks/context_spine.py +16 -4
- package/hooks/health_check.py +5 -4
- package/hooks/pre_stop_check.py +5 -2
- package/hooks/runtime_paths.py +396 -0
- package/hooks/subagent_tracker.py +5 -2
- package/hooks/workflow_checkpoint.py +16 -3
- package/hooks/workflow_context.py +11 -3
- package/hooks/workflow_resume.py +7 -4
- package/mcp-server/breadcrumb.cjs +20 -2
- package/mcp-server/lib/archive-journal.cjs +1026 -111
- package/mcp-server/lib/archive-mutation-worker.py +365 -0
- package/mcp-server/lib/artifact-registry.cjs +1126 -0
- package/mcp-server/lib/baseline-workflow.cjs +9 -4
- package/mcp-server/lib/change-packet.cjs +818 -0
- package/mcp-server/lib/change-workflow.cjs +932 -176
- package/mcp-server/lib/context-paths.cjs +111 -0
- package/mcp-server/lib/context-spine.cjs +511 -45
- package/mcp-server/lib/decision-dialogue.cjs +345 -3
- package/mcp-server/lib/delivery-transaction.cjs +471 -0
- package/mcp-server/lib/doctor-backup-worker.cjs +328 -0
- package/mcp-server/lib/doctor.cjs +373 -11
- package/mcp-server/lib/git-bootstrap.cjs +459 -47
- package/mcp-server/lib/init-project.cjs +190 -95
- package/mcp-server/lib/plan-publication-worker.cjs +830 -0
- package/mcp-server/lib/plan-store.cjs +700 -5
- package/mcp-server/lib/project-breadcrumb.cjs +23 -4
- package/mcp-server/lib/projector.cjs +174 -40
- package/mcp-server/lib/runtime-paths.cjs +1687 -0
- package/mcp-server/lib/safe-project-file.cjs +385 -0
- package/mcp-server/lib/spec-learning.cjs +28 -12
- package/mcp-server/lib/state-db.cjs +263 -9
- package/mcp-server/lib/state-ops.cjs +248 -49
- package/mcp-server/lib/telemetry.cjs +12 -3
- package/mcp-server/lib/workflow-state.cjs +610 -58
- package/mcp-server/server.cjs +278 -33
- package/orchestrator/daemon.cjs +2 -10
- package/orchestrator/parallel-orchestrator.cjs +314 -50
- package/orchestrator/planner/plan-builder.cjs +1 -1
- package/orchestrator/recovery.cjs +125 -7
- package/orchestrator/session-close-journal-worker.cjs +5 -0
- package/orchestrator/session-close-journal.cjs +1014 -0
- package/orchestrator/session-runner.cjs +679 -69
- package/orchestrator/worktree-manager.cjs +186 -35
- package/package.json +2 -3
- package/skills/cc-collab/SKILL.md +4 -1
- package/skills/codex-collab/SKILL.md +5 -2
- package/skills/ultra-change/SKILL.md +26 -8
- package/skills/ultra-deliver/SKILL.md +23 -11
- package/skills/ultra-dev/SKILL.md +33 -6
- package/skills/ultra-doctor/SKILL.md +6 -2
- package/skills/ultra-init/SKILL.md +4 -2
- package/skills/ultra-plan/SKILL.md +49 -16
- package/skills/ultra-research/SKILL.md +30 -27
- package/skills/ultra-review/SKILL.md +9 -5
- package/skills/ultra-status/SKILL.md +9 -4
- package/skills/ultra-test/SKILL.md +10 -5
- package/skills/ultra-think/SKILL.md +6 -2
- package/skills/ultra-think/references/decision-dialogue.md +17 -6
- package/skills/ultra-verify/SKILL.md +4 -1
- package/spec/cli-protocol.md +18 -7
- package/spec/mcp-tools.yaml +502 -44
- package/spec/schemas/change-delta.v1.schema.json +153 -0
- package/spec/schemas/documentation-reconciliation.v1.schema.json +106 -0
- package/spec/schemas/mcp-tools.schema.json +1 -1
- package/spec/schemas/state-db.sql +50 -13
- package/spec/schemas/tasks.v4.5.schema.json +1 -1
- package/ultra-tools/cli.cjs +3 -2
- package/ultra-tools/commands/db.cjs +76 -25
- package/ultra-tools/commands/migrate.cjs +995 -80
- package/ultra-tools/commands/session.cjs +34 -8
- package/ultra-tools/commands/status.cjs +32 -6
- package/ultra-tools/commands/system.cjs +129 -22
- package/commands/learn.md +0 -10
- package/orchestrator/skill-miner.cjs +0 -133
- package/output-styles/coding-vibes.md +0 -67
- package/output-styles/structural-thinking.md +0 -61
- package/skills/learn/SKILL.md +0 -58
|
@@ -16,6 +16,13 @@ const GATES = new Set([
|
|
|
16
16
|
const CONTEXT_KINDS = new Set(['spec', 'source', 'test', 'docs', 'external']);
|
|
17
17
|
const SLICE_KINDS = new Set(['tracer_bullet', 'expand_contract', 'integration_checkpoint']);
|
|
18
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
|
+
]);
|
|
19
26
|
const ADVISORY_CONTEXT_CODES = new Set([
|
|
20
27
|
'CONTEXT_FILE_BUDGET_EXCEEDED',
|
|
21
28
|
'CONTEXT_TOKEN_BUDGET_EXCEEDED',
|
|
@@ -132,6 +139,26 @@ function taskStateDigest(task) {
|
|
|
132
139
|
return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex');
|
|
133
140
|
}
|
|
134
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
|
+
|
|
135
162
|
function changeStateDigest(change) {
|
|
136
163
|
if (!change) return null;
|
|
137
164
|
const payload = {
|
|
@@ -154,9 +181,20 @@ function nonEmpty(value, field) {
|
|
|
154
181
|
return text;
|
|
155
182
|
}
|
|
156
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
|
+
|
|
157
194
|
function localPath(rootDir, ref) {
|
|
158
195
|
const candidate = ref.split('#', 1)[0];
|
|
159
|
-
if (!candidate || path.isAbsolute(candidate)
|
|
196
|
+
if (!candidate || path.isAbsolute(candidate)
|
|
197
|
+
|| /^[a-z][a-z0-9+.-]*:\/\//i.test(candidate)) {
|
|
160
198
|
throw new ContextSpineError('VALIDATION_ERROR', `context ref must be project-relative: ${ref}`);
|
|
161
199
|
}
|
|
162
200
|
const root = path.resolve(rootDir);
|
|
@@ -167,6 +205,102 @@ function localPath(rootDir, ref) {
|
|
|
167
205
|
return resolved;
|
|
168
206
|
}
|
|
169
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
|
+
|
|
170
304
|
function normalizeBudget(value, inherited) {
|
|
171
305
|
const source = value || inherited || DEFAULT_BUDGET;
|
|
172
306
|
const maxTokens = Number(source.max_tokens ?? DEFAULT_BUDGET.max_tokens);
|
|
@@ -178,44 +312,110 @@ function normalizeBudget(value, inherited) {
|
|
|
178
312
|
}
|
|
179
313
|
|
|
180
314
|
function specRefToContextRef(value, role) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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);
|
|
185
330
|
return {
|
|
186
331
|
ref,
|
|
187
332
|
kind: 'spec',
|
|
188
|
-
reason:
|
|
189
|
-
|
|
190
|
-
|
|
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,
|
|
191
341
|
};
|
|
192
342
|
}
|
|
193
343
|
|
|
194
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
|
+
}
|
|
195
351
|
let refs;
|
|
196
352
|
if (input.context_refs !== undefined) refs = input.context_refs;
|
|
197
|
-
else if (input.spec_refs !== undefined)
|
|
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
|
+
}
|
|
198
364
|
else refs = inherited || [];
|
|
199
365
|
if (!Array.isArray(refs)) {
|
|
200
366
|
throw new ContextSpineError('VALIDATION_ERROR', 'context_refs must be an array');
|
|
201
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
|
+
}
|
|
202
373
|
return refs.map((value, index) => {
|
|
203
374
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
204
375
|
throw new ContextSpineError('VALIDATION_ERROR', `context_refs[${index}] must be an object`);
|
|
205
376
|
}
|
|
206
|
-
const ref =
|
|
377
|
+
const ref = boundedText(
|
|
378
|
+
value.ref || value.path || value.reference,
|
|
379
|
+
`context_refs[${index}].ref`,
|
|
380
|
+
MAX_REF_LENGTH,
|
|
381
|
+
);
|
|
207
382
|
const kind = value.kind || 'source';
|
|
208
383
|
if (!CONTEXT_KINDS.has(kind)) {
|
|
209
384
|
throw new ContextSpineError('VALIDATION_ERROR', `unsupported context kind: ${kind}`);
|
|
210
385
|
}
|
|
211
|
-
const reason =
|
|
212
|
-
|
|
213
|
-
|
|
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
|
+
);
|
|
214
394
|
}
|
|
215
|
-
|
|
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 = {
|
|
216
404
|
ref, kind, reason, required: value.required !== false,
|
|
217
|
-
expected_digest:
|
|
405
|
+
expected_digest: expectedDigest,
|
|
406
|
+
freshness_policy: freshnessPolicy,
|
|
218
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;
|
|
219
419
|
});
|
|
220
420
|
}
|
|
221
421
|
|
|
@@ -230,6 +430,9 @@ function authoritativeTaskRefs(input, role, task) {
|
|
|
230
430
|
reason: item.reason,
|
|
231
431
|
required: item.required,
|
|
232
432
|
expected_digest: item.expected_digest,
|
|
433
|
+
anchor: item.anchor,
|
|
434
|
+
scope: item.scope,
|
|
435
|
+
freshness_policy: item.freshness_policy,
|
|
233
436
|
}));
|
|
234
437
|
if (JSON.stringify(comparable(supplied)) !== JSON.stringify(comparable(dbRefs))) {
|
|
235
438
|
throw new ContextSpineError(
|
|
@@ -241,22 +444,75 @@ function authoritativeTaskRefs(input, role, task) {
|
|
|
241
444
|
}
|
|
242
445
|
|
|
243
446
|
function inspectRef(rootDir, role, value) {
|
|
244
|
-
if (value.kind === 'external'
|
|
447
|
+
if (value.kind === 'external') {
|
|
245
448
|
return { ...value, role, status: 'external', digest: value.expected_digest, estimated_tokens: 0 };
|
|
246
449
|
}
|
|
247
|
-
const
|
|
248
|
-
if (!
|
|
450
|
+
const read = readProjectFile(rootDir, value.ref);
|
|
451
|
+
if (!read) {
|
|
249
452
|
return { ...value, role, status: 'missing', digest: null, estimated_tokens: 0 };
|
|
250
453
|
}
|
|
251
|
-
const content =
|
|
454
|
+
const content = read.bytes;
|
|
252
455
|
const digest = crypto.createHash('sha256').update(content).digest('hex');
|
|
253
|
-
const status = value.
|
|
456
|
+
const status = value.freshness_policy !== 'existence'
|
|
457
|
+
&& value.expected_digest && value.expected_digest !== digest ? 'stale' : 'current';
|
|
254
458
|
return {
|
|
255
459
|
...value, role, status, digest,
|
|
256
460
|
estimated_tokens: Math.max(1, Math.ceil(content.byteLength / 4)),
|
|
257
461
|
};
|
|
258
462
|
}
|
|
259
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
|
+
|
|
260
516
|
function normalizeExecutionContract(input, inherited, { task }) {
|
|
261
517
|
const taskContract = task
|
|
262
518
|
&& task.slice_kind && task.public_seam && task.verification_command ? {
|
|
@@ -413,17 +669,139 @@ function deliveryCapabilityReady(db, change, tasks, rootDir) {
|
|
|
413
669
|
}
|
|
414
670
|
}
|
|
415
671
|
|
|
416
|
-
function
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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);
|
|
424
688
|
return parseJson(row?.context_json, {});
|
|
425
689
|
}
|
|
426
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
|
+
|
|
427
805
|
function compileRoleContext(db, { input, change, tasks, rootDir }) {
|
|
428
806
|
if (input.next_action !== undefined) {
|
|
429
807
|
throw new ContextSpineError(
|
|
@@ -435,21 +813,51 @@ function compileRoleContext(db, { input, change, tasks, rootDir }) {
|
|
|
435
813
|
if (input.task_id && !task) {
|
|
436
814
|
throw new ContextSpineError('TASK_NOT_FOUND', `task ${input.task_id} is not linked to change ${change.id}`);
|
|
437
815
|
}
|
|
438
|
-
const inherited = latestContext(db, change.id, input.task_id || null);
|
|
439
816
|
const role = input.role || (input.task_id ? 'implement' : 'plan');
|
|
440
817
|
if (!ROLES.has(role)) throw new ContextSpineError('VALIDATION_ERROR', `unsupported context role: ${role}`);
|
|
441
818
|
const gate = input.gate || defaultGate(role);
|
|
442
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
|
+
});
|
|
443
826
|
|
|
444
827
|
const refs = authoritativeTaskRefs(input, role, task)
|
|
445
828
|
|| normalizeRefs(input, role, inherited.context?.items || []);
|
|
446
829
|
const items = refs.map((value) => inspectRef(rootDir, role, value));
|
|
447
830
|
const budget = normalizeBudget(input.budget, inherited.context?.budget);
|
|
448
|
-
const
|
|
449
|
-
const fileCount = items.filter((item) => item.status !== 'external').length;
|
|
831
|
+
const scopedTasks = taskNeighborhood(tasks, task, role);
|
|
450
832
|
const executionContract = normalizeExecutionContract(
|
|
451
833
|
input, inherited.execution_contract, { task },
|
|
452
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;
|
|
453
861
|
const blockers = [];
|
|
454
862
|
const warnings = [];
|
|
455
863
|
const baselineHealth = baselines.inspectBaseline(db, { rootDir });
|
|
@@ -459,7 +867,18 @@ function compileRoleContext(db, { input, change, tasks, rootDir }) {
|
|
|
459
867
|
warnings.push(...baselineGate.warnings);
|
|
460
868
|
for (const item of items) {
|
|
461
869
|
if (item.required && item.status === 'missing') blockers.push(`CONTEXT_REQUIRED_REF_MISSING:${item.ref}`);
|
|
462
|
-
if (item.required && item.status === '
|
|
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
|
+
}
|
|
463
882
|
}
|
|
464
883
|
if (fileCount > budget.max_files) warnings.push('CONTEXT_FILE_BUDGET_EXCEEDED');
|
|
465
884
|
if (tokenEstimate > budget.max_tokens) warnings.push('CONTEXT_TOKEN_BUDGET_EXCEEDED');
|
|
@@ -490,8 +909,18 @@ function compileRoleContext(db, { input, change, tasks, rootDir }) {
|
|
|
490
909
|
control,
|
|
491
910
|
recommendation: normalizeRecommendation(input.recommendation),
|
|
492
911
|
readiness: { status: readiness, blockers, warnings: [...new Set(warnings)] },
|
|
493
|
-
context: {
|
|
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
|
+
},
|
|
494
921
|
execution_contract: executionContract,
|
|
922
|
+
task_context_contract: taskContract,
|
|
923
|
+
task_scope: scopedTasks,
|
|
495
924
|
baseline: baselineHealth.baseline ? {
|
|
496
925
|
id: baselineHealth.baseline.id,
|
|
497
926
|
mode: baselineHealth.baseline.mode,
|
|
@@ -507,6 +936,7 @@ function compileRoleContext(db, { input, change, tasks, rootDir }) {
|
|
|
507
936
|
task_id: task?.id || null,
|
|
508
937
|
task_status: task?.status || null,
|
|
509
938
|
task_state_digest: taskStateDigest(task),
|
|
939
|
+
task_contract_digest: taskContractDigest(task),
|
|
510
940
|
session_id: task?.session_id || null,
|
|
511
941
|
role,
|
|
512
942
|
gate,
|
|
@@ -572,16 +1002,26 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
572
1002
|
}
|
|
573
1003
|
if (!change) {
|
|
574
1004
|
const baseline = baselineHealth.baseline;
|
|
1005
|
+
const activeWorkflow = actionableBaselineWorkflow(db, baseline?.id, rootDir);
|
|
575
1006
|
const decisionGate = decisions.decisionGate(
|
|
576
|
-
db, {
|
|
1007
|
+
db, {
|
|
1008
|
+
baseline_id: baseline?.id || null,
|
|
1009
|
+
workflow_run_id: activeWorkflow?.id || null,
|
|
1010
|
+
}, { rootDir },
|
|
577
1011
|
);
|
|
1012
|
+
const acceptedIntent = decisions.acceptedIntent(db, {
|
|
1013
|
+
baseline_id: baseline?.id || null,
|
|
1014
|
+
workflow_run_id: activeWorkflow?.id || null,
|
|
1015
|
+
});
|
|
578
1016
|
if (!decisionGate.ready) {
|
|
579
1017
|
return {
|
|
580
1018
|
change_id: null, task_id: null, role: 'plan', gate: 'alignment', readiness: 'blocked',
|
|
581
1019
|
blockers: [...new Set([...baselineHealth.blockers, ...decisionGate.blockers])],
|
|
582
1020
|
warnings: baselineHealth.warnings,
|
|
583
1021
|
allowed_transitions: ['ultra-think', 'ultra-status'], required_transition: 'ultra-think',
|
|
584
|
-
workflow:
|
|
1022
|
+
workflow: summarizeWorkflow(activeWorkflow),
|
|
1023
|
+
decision: summarizeDecision(decisionGate),
|
|
1024
|
+
accepted_intent: acceptedIntent,
|
|
585
1025
|
context_manifest_path: null, context_manifest_hash: null, git_head: currentHead(rootDir),
|
|
586
1026
|
baseline: baseline ? {
|
|
587
1027
|
id: baseline.id, mode: baseline.mode, status: baseline.status,
|
|
@@ -590,7 +1030,6 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
590
1030
|
};
|
|
591
1031
|
}
|
|
592
1032
|
if (baselineHealth.status !== 'pass') {
|
|
593
|
-
const activeWorkflow = actionableBaselineWorkflow(db, baseline?.id, rootDir);
|
|
594
1033
|
const control = baseline
|
|
595
1034
|
? {
|
|
596
1035
|
allowed_transitions: uniqueStrings([
|
|
@@ -609,7 +1048,10 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
609
1048
|
blockers: baselineHealth.blockers, warnings: baselineHealth.warnings,
|
|
610
1049
|
allowed_transitions: control.allowed_transitions,
|
|
611
1050
|
required_transition: control.required_transition,
|
|
612
|
-
workflow: summarizeWorkflow(activeWorkflow),
|
|
1051
|
+
workflow: summarizeWorkflow(activeWorkflow),
|
|
1052
|
+
decision: null,
|
|
1053
|
+
accepted_intent: acceptedIntent,
|
|
1054
|
+
context_manifest_path: null,
|
|
613
1055
|
context_manifest_hash: null, git_head: currentHead(rootDir),
|
|
614
1056
|
baseline: baseline ? {
|
|
615
1057
|
id: baseline.id, mode: baseline.mode, status: baseline.status,
|
|
@@ -623,6 +1065,7 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
623
1065
|
required_transition: null,
|
|
624
1066
|
workflow: null,
|
|
625
1067
|
decision: null,
|
|
1068
|
+
accepted_intent: acceptedIntent,
|
|
626
1069
|
context_manifest_path: null, context_manifest_hash: null, git_head: currentHead(rootDir),
|
|
627
1070
|
blockers: [], warnings: baselineHealth.warnings,
|
|
628
1071
|
baseline: {
|
|
@@ -634,13 +1077,24 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
634
1077
|
}
|
|
635
1078
|
const tasks = db.prepare('SELECT id FROM tasks WHERE change_id = ? ORDER BY created_at ASC')
|
|
636
1079
|
.all(change.id).map((row) => ops.readTask(db, row.id));
|
|
637
|
-
const
|
|
638
|
-
|
|
639
|
-
).get(change.id);
|
|
640
|
-
const context = parseJson(snapshot?.context_json, {});
|
|
641
|
-
const taskId = snapshot?.task_id || tasks.find((row) => row.status === 'in_progress')?.id
|
|
1080
|
+
const activeWorkflow = actionableChangeWorkflow(db, change.id, rootDir);
|
|
1081
|
+
const taskId = activeWorkflow?.task_id || tasks.find((row) => row.status === 'in_progress')?.id
|
|
642
1082
|
|| tasks.find((row) => row.status === 'pending')?.id || null;
|
|
643
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, {});
|
|
644
1098
|
const checkout = baselines.gitWorktreeSnapshot(rootDir, ['.']);
|
|
645
1099
|
const head = checkout.head;
|
|
646
1100
|
const snapshotMissing = !snapshot;
|
|
@@ -660,7 +1114,11 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
660
1114
|
const stateChanged = context.resume?.task_status !== undefined
|
|
661
1115
|
&& context.resume.task_status !== (task?.status || null);
|
|
662
1116
|
const taskContractChanged = Boolean(
|
|
663
|
-
context.resume &&
|
|
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
|
+
),
|
|
664
1122
|
);
|
|
665
1123
|
const changeContractChanged = Boolean(
|
|
666
1124
|
context.resume && context.resume.change_state_digest !== changeStateDigest(change),
|
|
@@ -670,8 +1128,13 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
670
1128
|
const decisionGate = decisions.decisionGate(db, {
|
|
671
1129
|
baseline_id: baselineHealth.baseline?.id || null,
|
|
672
1130
|
change_id: change.id,
|
|
673
|
-
workflow_run_id:
|
|
1131
|
+
workflow_run_id: activeWorkflow?.id || null,
|
|
674
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
|
+
});
|
|
675
1138
|
const warnings = [...baselineGate.warnings];
|
|
676
1139
|
blockers.push(...baselineGate.blockers);
|
|
677
1140
|
blockers.push(...decisionGate.blockers);
|
|
@@ -695,7 +1158,6 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
695
1158
|
}
|
|
696
1159
|
}
|
|
697
1160
|
const readiness = blockers.length > 0 ? 'blocked' : (snapshot?.readiness || 'ready');
|
|
698
|
-
const activeWorkflow = actionableChangeWorkflow(db, change.id, rootDir);
|
|
699
1161
|
const control = deriveTransitions({
|
|
700
1162
|
change,
|
|
701
1163
|
tasks,
|
|
@@ -729,6 +1191,7 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
729
1191
|
} : null,
|
|
730
1192
|
workflow: summarizeWorkflow(activeWorkflow),
|
|
731
1193
|
decision: summarizeDecision(decisionGate),
|
|
1194
|
+
accepted_intent: acceptedIntent,
|
|
732
1195
|
};
|
|
733
1196
|
}
|
|
734
1197
|
|
|
@@ -738,5 +1201,8 @@ module.exports = {
|
|
|
738
1201
|
changeStateDigest,
|
|
739
1202
|
compileRoleContext,
|
|
740
1203
|
deriveTransitions,
|
|
1204
|
+
latestContextSnapshot,
|
|
741
1205
|
readBreadcrumb,
|
|
1206
|
+
taskContractDigest,
|
|
1207
|
+
validateContextSnapshot,
|
|
742
1208
|
};
|