ultra-builder-pro-cli 0.16.0 → 0.17.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/specs/research-distillate.md +4 -3
- package/CHANGELOG.md +86 -0
- package/README.md +54 -24
- package/adapters/_shared/codex-assets.cjs +10 -5
- package/adapters/_shared/handbook.cjs +1 -1
- package/adapters/_shared/interaction-contract.cjs +61 -0
- package/adapters/claude.js +1 -1
- package/adapters/opencode.js +1 -1
- package/bin/orchestrator.js +175 -19
- package/commands/ultra-change.md +1 -1
- package/commands/ultra-deliver.md +3 -3
- package/commands/ultra-init.md +1 -1
- package/commands/ultra-plan.md +1 -1
- package/commands/ultra-research.md +2 -2
- package/commands/ultra-review.md +10 -0
- package/commands/ultra-status.md +1 -1
- package/commands/ultra-test.md +1 -1
- package/docs/AGENT-CONTEXT.md +11 -8
- package/docs/ARCHITECTURE.md +67 -9
- package/docs/LEGACY-CLI-CROSSWALK.md +163 -0
- package/docs/PLAN.zh-CN.md +19 -13
- package/docs/RUNTIME-COMPAT-MATRIX.md +2 -2
- package/docs/WORKFLOW-LIFECYCLE.md +123 -59
- package/hooks/pre_stop_check.py +3 -2
- package/mcp-server/lib/baseline-workflow.cjs +84 -16
- package/mcp-server/lib/change-workflow.cjs +55 -35
- package/mcp-server/lib/context-spine.cjs +171 -105
- package/mcp-server/lib/git-bootstrap.cjs +240 -0
- package/mcp-server/lib/init-project.cjs +178 -41
- package/mcp-server/lib/project-breadcrumb.cjs +14 -14
- package/mcp-server/lib/state-db.cjs +257 -10
- package/mcp-server/lib/state-ops.cjs +64 -1
- package/mcp-server/lib/workflow-state.cjs +300 -205
- package/mcp-server/server.cjs +58 -34
- package/orchestrator/auto-merge.cjs +19 -0
- package/orchestrator/daemon.cjs +104 -0
- package/orchestrator/parallel-orchestrator.cjs +371 -49
- package/orchestrator/session-runner.cjs +354 -25
- package/package.json +2 -1
- package/skills/ultra-change/SKILL.md +69 -94
- package/skills/ultra-deliver/SKILL.md +37 -65
- package/skills/ultra-dev/SKILL.md +50 -52
- package/skills/ultra-doctor/SKILL.md +34 -49
- package/skills/ultra-init/SKILL.md +78 -105
- package/skills/ultra-plan/SKILL.md +78 -99
- package/skills/ultra-research/SKILL.md +99 -113
- package/skills/ultra-review/SKILL.md +38 -53
- package/skills/ultra-review/references/review-modes.md +5 -3
- package/skills/ultra-review/references/unified-schema.md +26 -6
- package/skills/ultra-review/scripts/review_wait.py +12 -0
- package/skills/ultra-status/SKILL.md +34 -44
- package/skills/ultra-test/SKILL.md +61 -54
- package/skills/ultra-think/SKILL.md +37 -51
- package/skills/ultra-think/references/decision-dialogue.md +87 -111
- package/spec/cli-protocol.md +35 -0
- package/spec/mcp-tools.yaml +145 -28
- package/spec/schemas/delivery-report.v1.schema.json +1 -46
- package/spec/schemas/state-db.sql +8 -2
- package/spec/schemas/test-report.v1.schema.json +45 -14
- package/ultra-tools/cli.cjs +1 -1
- package/ultra-tools/commands/session.cjs +10 -3
- package/ultra-tools/commands/status.cjs +19 -14
- package/ultra-tools/commands/system.cjs +2 -2
- package/ultra-tools/commands/task.cjs +3 -0
|
@@ -24,6 +24,10 @@ const CHANGE_PATCH_FIELDS = new Set([
|
|
|
24
24
|
'title', 'intent', 'status', 'docs_impact', 'provider_refs',
|
|
25
25
|
'contract', 'classification', 'research_disposition',
|
|
26
26
|
]);
|
|
27
|
+
const SEMANTIC_AUTHORITY_FIELDS = new Set([
|
|
28
|
+
'intent', 'docs_impact', 'provider_refs', 'contract', 'classification',
|
|
29
|
+
'research_disposition',
|
|
30
|
+
]);
|
|
27
31
|
const CHANGE_TRANSITIONS = Object.freeze({
|
|
28
32
|
active: new Set(['active', 'blocked', 'cancelled']),
|
|
29
33
|
blocked: new Set(['active', 'blocked', 'cancelled']),
|
|
@@ -544,9 +548,12 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
544
548
|
).all(change.id).map((task) => ops.readTask(db, task.id));
|
|
545
549
|
const specs = Array.isArray(input.spec_refs) ? input.spec_refs : [];
|
|
546
550
|
const spine = contextSpine.compileRoleContext(db, { input, change, tasks, rootDir });
|
|
551
|
+
const recommendation = spine.recommendation;
|
|
552
|
+
const durableSpine = { ...spine };
|
|
553
|
+
delete durableSpine.recommendation;
|
|
547
554
|
const snapshotId = `ctx-${crypto.randomUUID().slice(0, 12)}`;
|
|
548
555
|
const manifest = {
|
|
549
|
-
schema_version: '
|
|
556
|
+
schema_version: '3.0',
|
|
550
557
|
snapshot_id: snapshotId,
|
|
551
558
|
generated_at: nowIso(),
|
|
552
559
|
change: {
|
|
@@ -566,7 +573,7 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
566
573
|
specs,
|
|
567
574
|
role: spine.role,
|
|
568
575
|
gate: spine.gate,
|
|
569
|
-
|
|
576
|
+
control: spine.control,
|
|
570
577
|
readiness: spine.readiness,
|
|
571
578
|
context: spine.context,
|
|
572
579
|
execution_contract: spine.execution_contract,
|
|
@@ -588,14 +595,16 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
588
595
|
db.prepare(
|
|
589
596
|
`INSERT INTO context_snapshots
|
|
590
597
|
(id, change_id, task_id, git_head, provider_refs_json, manifest_path, manifest_hash,
|
|
591
|
-
role, gate, next_action,
|
|
592
|
-
token_estimate, token_budget)
|
|
593
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
598
|
+
role, gate, next_action, allowed_transitions_json, required_transition,
|
|
599
|
+
readiness, blockers_json, context_json, token_estimate, token_budget)
|
|
600
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
594
601
|
).run(
|
|
595
602
|
snapshotId, change.id, input.task_id || null,
|
|
596
603
|
spine.resume.git_head, JSON.stringify(providers), relative, hash,
|
|
597
|
-
spine.role, spine.gate,
|
|
598
|
-
JSON.stringify(spine.
|
|
604
|
+
spine.role, spine.gate, '',
|
|
605
|
+
JSON.stringify(spine.control.allowed_transitions), spine.control.required_transition,
|
|
606
|
+
spine.readiness.status,
|
|
607
|
+
JSON.stringify(spine.readiness.blockers), JSON.stringify(durableSpine),
|
|
599
608
|
spine.context.token_estimate, spine.context.budget.max_tokens,
|
|
600
609
|
);
|
|
601
610
|
upsertArtifact(db, {
|
|
@@ -623,34 +632,17 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
623
632
|
manifest_path: relative, manifest_hash: hash, git_head: spine.resume.git_head,
|
|
624
633
|
worktree_digest: spine.resume.worktree_digest,
|
|
625
634
|
role: spine.role, gate: spine.gate, readiness: spine.readiness.status,
|
|
626
|
-
|
|
635
|
+
allowed_transitions: spine.control.allowed_transitions,
|
|
636
|
+
required_transition: spine.control.required_transition,
|
|
627
637
|
},
|
|
628
638
|
});
|
|
629
|
-
const linkedWorkflow = db.prepare(
|
|
630
|
-
`SELECT id FROM workflow_runs
|
|
631
|
-
WHERE kind = 'change' AND change_id = ? AND current_step = 'compile-context'
|
|
632
|
-
AND status IN ('active', 'blocked')
|
|
633
|
-
ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
|
|
634
|
-
).get(change.id);
|
|
635
|
-
if (linkedWorkflow) {
|
|
636
|
-
if (spine.readiness.status === 'ready') {
|
|
637
|
-
workflows.recordWorkflowStep(db, {
|
|
638
|
-
id: linkedWorkflow.id, step_id: 'compile-context', status: 'completed',
|
|
639
|
-
evidence: [{
|
|
640
|
-
kind: 'context', ref: relative,
|
|
641
|
-
summary: `Role ${spine.role} context is ready for ${input.task_id || 'change planning'}.`,
|
|
642
|
-
}],
|
|
643
|
-
outputs: [{ path: relative, kind: 'context-manifest' }],
|
|
644
|
-
}, { rootDir });
|
|
645
|
-
} else {
|
|
646
|
-
workflows.recordWorkflowStep(db, {
|
|
647
|
-
id: linkedWorkflow.id, step_id: 'compile-context', status: 'blocked',
|
|
648
|
-
blockers: spine.readiness.blockers,
|
|
649
|
-
}, { rootDir });
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
639
|
});
|
|
653
|
-
return {
|
|
640
|
+
return {
|
|
641
|
+
manifest,
|
|
642
|
+
recommendation,
|
|
643
|
+
context_manifest_path: manifestPath,
|
|
644
|
+
manifest_hash: hash,
|
|
645
|
+
};
|
|
654
646
|
}
|
|
655
647
|
|
|
656
648
|
function createChange(db, input, { rootDir = process.cwd() } = {}) {
|
|
@@ -793,7 +785,6 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
793
785
|
change_id: row.id, subject: `Establish executable readiness for ${row.title}.`,
|
|
794
786
|
metadata: { change_kind: row.kind, base_commit: row.base_commit },
|
|
795
787
|
}, { rootDir });
|
|
796
|
-
const context = compileContext(db, { id: row.id }, { rootDir });
|
|
797
788
|
workflow = workflows.recordWorkflowStep(db, {
|
|
798
789
|
id: workflow.id, step_id: 'bind-baseline', status: 'completed',
|
|
799
790
|
evidence: [{
|
|
@@ -819,9 +810,10 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
819
810
|
research_disposition: row.research_disposition,
|
|
820
811
|
}],
|
|
821
812
|
}, { rootDir });
|
|
813
|
+
workflow = workflows.completeWorkflow(db, { id: workflow.id }, { rootDir });
|
|
822
814
|
return {
|
|
823
815
|
change: readChange(db, row.id), intent_path: intentPath,
|
|
824
|
-
context_manifest_path:
|
|
816
|
+
context_manifest_path: null,
|
|
825
817
|
workflow,
|
|
826
818
|
};
|
|
827
819
|
});
|
|
@@ -914,6 +906,30 @@ function updateChange(db, id, patch = {}, { rootDir = process.cwd() } = {}) {
|
|
|
914
906
|
sets.push('updated_at = ?'); values.push(nowIso(), id);
|
|
915
907
|
db.prepare(`UPDATE changes SET ${sets.join(', ')} WHERE id = ?`).run(...values);
|
|
916
908
|
const updated = readChange(db, id);
|
|
909
|
+
const invalidatedTasks = [];
|
|
910
|
+
if (contextSpine.changeStateDigest(current) !== contextSpine.changeStateDigest(updated)) {
|
|
911
|
+
const tasks = db.prepare(
|
|
912
|
+
'SELECT id, stale FROM tasks WHERE change_id = ? ORDER BY created_at ASC, id ASC',
|
|
913
|
+
).all(id);
|
|
914
|
+
const markStale = db.prepare(
|
|
915
|
+
'UPDATE tasks SET stale = 1, updated_at = ? WHERE id = ?',
|
|
916
|
+
);
|
|
917
|
+
const invalidatedAt = nowIso();
|
|
918
|
+
for (const task of tasks) {
|
|
919
|
+
if (task.stale) continue;
|
|
920
|
+
markStale.run(invalidatedAt, task.id);
|
|
921
|
+
invalidatedTasks.push(task.id);
|
|
922
|
+
ops.appendEventInTx(db, {
|
|
923
|
+
type: 'task_stale_marked',
|
|
924
|
+
task_id: task.id,
|
|
925
|
+
change_id: id,
|
|
926
|
+
payload: {
|
|
927
|
+
reason: 'change_semantic_authority_updated',
|
|
928
|
+
fields: Object.keys(patch).filter((field) => SEMANTIC_AUTHORITY_FIELDS.has(field)),
|
|
929
|
+
},
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
}
|
|
917
933
|
if (syncIntent) {
|
|
918
934
|
fs.mkdirSync(path.dirname(intentPath), { recursive: true });
|
|
919
935
|
writeIntent(intentPath, updated);
|
|
@@ -924,7 +940,11 @@ function updateChange(db, id, patch = {}, { rootDir = process.cwd() } = {}) {
|
|
|
924
940
|
contentHash: crypto.createHash('sha256').update(fs.readFileSync(intentPath)).digest('hex'),
|
|
925
941
|
});
|
|
926
942
|
}
|
|
927
|
-
ops.appendEventInTx(db, {
|
|
943
|
+
ops.appendEventInTx(db, {
|
|
944
|
+
type: 'change_updated',
|
|
945
|
+
change_id: id,
|
|
946
|
+
payload: { fields: Object.keys(patch), invalidated_tasks: invalidatedTasks },
|
|
947
|
+
});
|
|
928
948
|
return updated;
|
|
929
949
|
});
|
|
930
950
|
} catch (error) {
|
|
@@ -52,6 +52,10 @@ function parseJson(value, fallback = {}) {
|
|
|
52
52
|
try { return JSON.parse(value); } catch { return fallback; }
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
function uniqueStrings(values) {
|
|
56
|
+
return [...new Set(values.filter((value) => typeof value === 'string' && value.length > 0))];
|
|
57
|
+
}
|
|
58
|
+
|
|
55
59
|
function hasIncidentBreakGlass(change) {
|
|
56
60
|
const bypass = change?.baseline_bypass
|
|
57
61
|
|| parseJson(change?.baseline_bypass_json, null);
|
|
@@ -128,6 +132,22 @@ function taskStateDigest(task) {
|
|
|
128
132
|
return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex');
|
|
129
133
|
}
|
|
130
134
|
|
|
135
|
+
function changeStateDigest(change) {
|
|
136
|
+
if (!change) return null;
|
|
137
|
+
const payload = {
|
|
138
|
+
id: change.id,
|
|
139
|
+
kind: change.kind,
|
|
140
|
+
intent: change.intent,
|
|
141
|
+
docs_impact: change.docs_impact || parseJson(change.docs_impact_json, {}),
|
|
142
|
+
provider_refs: change.provider_refs || parseJson(change.provider_refs_json, {}),
|
|
143
|
+
contract: change.contract || parseJson(change.contract_json, {}),
|
|
144
|
+
classification: change.classification || parseJson(change.classification_json, {}),
|
|
145
|
+
research_disposition: change.research_disposition
|
|
146
|
+
|| parseJson(change.research_disposition_json, {}),
|
|
147
|
+
};
|
|
148
|
+
return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex');
|
|
149
|
+
}
|
|
150
|
+
|
|
131
151
|
function nonEmpty(value, field) {
|
|
132
152
|
const text = typeof value === 'string' ? value.trim() : '';
|
|
133
153
|
if (!text) throw new ContextSpineError('VALIDATION_ERROR', `${field} must be a non-empty string`);
|
|
@@ -291,27 +311,106 @@ function defaultGate(role) {
|
|
|
291
311
|
}[role];
|
|
292
312
|
}
|
|
293
313
|
|
|
294
|
-
function
|
|
314
|
+
function deriveTransitions({
|
|
315
|
+
change,
|
|
316
|
+
tasks = [],
|
|
317
|
+
task,
|
|
318
|
+
role,
|
|
319
|
+
readiness,
|
|
320
|
+
blockers = [],
|
|
321
|
+
activeWorkflow = null,
|
|
322
|
+
deliveryReady = false,
|
|
323
|
+
}) {
|
|
324
|
+
const unique = (values) => [...new Set(values.filter(Boolean))];
|
|
325
|
+
const has = (prefix) => blockers.some((blocker) => blocker === prefix || blocker.startsWith(`${prefix}:`));
|
|
326
|
+
if (has('STATE_DB_UNREADABLE') || has('STATE_SCHEMA_INCOMPLETE') || has('STATE_AUTHORITY_CONFLICT')) {
|
|
327
|
+
return {
|
|
328
|
+
allowed_transitions: ['ultra-doctor', 'ultra-status'],
|
|
329
|
+
required_transition: 'ultra-doctor',
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
if (has('STATE_DB_MISSING') || has('BASELINE_MISSING')) {
|
|
333
|
+
return {
|
|
334
|
+
allowed_transitions: ['ultra-init', 'ultra-status'],
|
|
335
|
+
required_transition: 'ultra-init',
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
if (blockers.some((blocker) => blocker.startsWith('DECISION_'))) {
|
|
339
|
+
return {
|
|
340
|
+
allowed_transitions: ['ultra-think', 'ultra-status'],
|
|
341
|
+
required_transition: 'ultra-think',
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
if (blockers.some((blocker) => blocker.startsWith('BASELINE_SCHEMA_')
|
|
345
|
+
|| blocker.startsWith('STATE_SCHEMA_MIGRATION_'))) {
|
|
346
|
+
return {
|
|
347
|
+
allowed_transitions: ['ultra-doctor', 'ultra-init', 'ultra-status'],
|
|
348
|
+
required_transition: 'ultra-doctor',
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const transitions = [];
|
|
353
|
+
if (activeWorkflow) transitions.push(`ultra-${activeWorkflow.kind}`);
|
|
354
|
+
if (!change) {
|
|
355
|
+
transitions.push('ultra-research', 'ultra-change', 'ultra-think', 'ultra-status', 'ultra-doctor');
|
|
356
|
+
return { allowed_transitions: unique(transitions), required_transition: null };
|
|
357
|
+
}
|
|
295
358
|
if (readiness === 'blocked') {
|
|
359
|
+
if (blockers.some((blocker) => blocker.startsWith('CONTEXT_')
|
|
360
|
+
|| blocker.startsWith('EXECUTION_'))) {
|
|
361
|
+
transitions.push('change.context');
|
|
362
|
+
}
|
|
296
363
|
if (blockers.some((blocker) => blocker.startsWith('BASELINE_'))) {
|
|
297
|
-
|
|
364
|
+
transitions.push('ultra-research', 'ultra-doctor');
|
|
298
365
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
if (change.status === '
|
|
303
|
-
|
|
304
|
-
if (
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
366
|
+
transitions.push('ultra-change', 'ultra-think', 'ultra-status');
|
|
367
|
+
return { allowed_transitions: unique(transitions), required_transition: null };
|
|
368
|
+
}
|
|
369
|
+
if (change.status === 'ready') {
|
|
370
|
+
transitions.push('ultra-deliver', 'ultra-review', 'ultra-test');
|
|
371
|
+
} else if (change.status === 'blocked') {
|
|
372
|
+
transitions.push('ultra-think', 'ultra-change');
|
|
373
|
+
} else if (!task && tasks.length === 0) {
|
|
374
|
+
transitions.push('ultra-plan', 'ultra-research', 'ultra-change');
|
|
375
|
+
} else if (task?.status === 'pending' || task?.status === 'in_progress') {
|
|
376
|
+
transitions.push('ultra-dev', 'ultra-plan');
|
|
377
|
+
if (role === 'review') transitions.push('ultra-review');
|
|
378
|
+
} else if (tasks.length > 0 && tasks.every((row) => ['completed', 'expanded'].includes(row.status))) {
|
|
379
|
+
transitions.push('ultra-test', 'ultra-review');
|
|
380
|
+
if (deliveryReady) transitions.push('ultra-deliver');
|
|
381
|
+
} else {
|
|
382
|
+
transitions.push('ultra-plan', 'ultra-change');
|
|
383
|
+
}
|
|
384
|
+
transitions.push('ultra-think', 'ultra-status');
|
|
385
|
+
return { allowed_transitions: unique(transitions), required_transition: null };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function normalizeRecommendation(value) {
|
|
389
|
+
if (value === undefined || value === null) return null;
|
|
390
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
391
|
+
throw new ContextSpineError('VALIDATION_ERROR', 'recommendation must be an object');
|
|
392
|
+
}
|
|
393
|
+
const workflow = String(value.workflow || '').trim();
|
|
394
|
+
const rationale = String(value.rationale || '').trim();
|
|
395
|
+
if (!workflow || rationale.length < 3) {
|
|
396
|
+
throw new ContextSpineError(
|
|
397
|
+
'VALIDATION_ERROR', 'recommendation requires workflow and rationale',
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
return { workflow, rationale, authoritative: false };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function deliveryCapabilityReady(db, change, tasks, rootDir) {
|
|
404
|
+
if (!change || !Array.isArray(tasks) || tasks.length === 0
|
|
405
|
+
|| !tasks.every((row) => ['completed', 'expanded'].includes(row.status))) {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
try {
|
|
409
|
+
workflows.validateDeliveryPrerequisites(db, { change_id: change.id }, rootDir);
|
|
410
|
+
return true;
|
|
411
|
+
} catch {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
315
414
|
}
|
|
316
415
|
|
|
317
416
|
function latestContext(db, changeId, taskId) {
|
|
@@ -328,8 +427,8 @@ function latestContext(db, changeId, taskId) {
|
|
|
328
427
|
function compileRoleContext(db, { input, change, tasks, rootDir }) {
|
|
329
428
|
if (input.next_action !== undefined) {
|
|
330
429
|
throw new ContextSpineError(
|
|
331
|
-
'
|
|
332
|
-
'next_action is
|
|
430
|
+
'CONTEXT_LEGACY_NEXT_ACTION_UNSUPPORTED',
|
|
431
|
+
'next_action is not an authority field; provide a non-authoritative recommendation instead',
|
|
333
432
|
);
|
|
334
433
|
}
|
|
335
434
|
const task = input.task_id ? tasks.find((row) => row.id === input.task_id) : null;
|
|
@@ -376,13 +475,20 @@ function compileRoleContext(db, { input, change, tasks, rootDir }) {
|
|
|
376
475
|
}
|
|
377
476
|
}
|
|
378
477
|
const readiness = blockers.length === 0 ? 'ready' : 'blocked';
|
|
379
|
-
const
|
|
380
|
-
change,
|
|
478
|
+
const control = deriveTransitions({
|
|
479
|
+
change,
|
|
480
|
+
tasks,
|
|
481
|
+
task,
|
|
482
|
+
role,
|
|
483
|
+
readiness,
|
|
484
|
+
blockers,
|
|
485
|
+
deliveryReady: deliveryCapabilityReady(db, change, tasks, rootDir),
|
|
381
486
|
});
|
|
382
487
|
return {
|
|
383
488
|
role,
|
|
384
489
|
gate,
|
|
385
|
-
|
|
490
|
+
control,
|
|
491
|
+
recommendation: normalizeRecommendation(input.recommendation),
|
|
386
492
|
readiness: { status: readiness, blockers, warnings: [...new Set(warnings)] },
|
|
387
493
|
context: { items, budget, token_estimate: tokenEstimate, file_count: fileCount },
|
|
388
494
|
execution_contract: executionContract,
|
|
@@ -397,6 +503,7 @@ function compileRoleContext(db, { input, change, tasks, rootDir }) {
|
|
|
397
503
|
selected_task: task,
|
|
398
504
|
resume: {
|
|
399
505
|
change_id: change.id,
|
|
506
|
+
change_state_digest: changeStateDigest(change),
|
|
400
507
|
task_id: task?.id || null,
|
|
401
508
|
task_status: task?.status || null,
|
|
402
509
|
task_state_digest: taskStateDigest(task),
|
|
@@ -410,15 +517,6 @@ function compileRoleContext(db, { input, change, tasks, rootDir }) {
|
|
|
410
517
|
};
|
|
411
518
|
}
|
|
412
519
|
|
|
413
|
-
function recommendedWorkflow(change, task, tasks, readiness) {
|
|
414
|
-
if (!change) return 'ultra-change';
|
|
415
|
-
if (readiness !== 'ready' || change.status === 'blocked') return 'ultra-change';
|
|
416
|
-
if (change.status === 'ready') return 'ultra-deliver';
|
|
417
|
-
if (task && ['pending', 'in_progress'].includes(task.status)) return 'ultra-dev';
|
|
418
|
-
if (tasks.length > 0 && tasks.every((row) => ['completed', 'expanded'].includes(row.status))) return 'ultra-test';
|
|
419
|
-
return 'ultra-change';
|
|
420
|
-
}
|
|
421
|
-
|
|
422
520
|
function actionableBaselineWorkflow(db, baselineId, rootDir) {
|
|
423
521
|
if (!baselineId) return null;
|
|
424
522
|
const candidates = workflows.listWorkflows(db, { baseline_id: baselineId, limit: 100 }, { rootDir })
|
|
@@ -443,16 +541,6 @@ function summarizeWorkflow(run) {
|
|
|
443
541
|
};
|
|
444
542
|
}
|
|
445
543
|
|
|
446
|
-
function workflowNextAction(run) {
|
|
447
|
-
if (run.status === 'blocked') {
|
|
448
|
-
return `Resolve ${run.kind} workflow blockers at ${run.current_step}: ${run.blockers.join(', ')}.`;
|
|
449
|
-
}
|
|
450
|
-
if (run.status === 'ready') {
|
|
451
|
-
return `Finalize ${run.kind} workflow ${run.id} after verifying its recorded evidence and outputs.`;
|
|
452
|
-
}
|
|
453
|
-
return `Continue ${run.kind} workflow ${run.id} at ${run.current_step}.`;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
544
|
function summarizeDecision(gate) {
|
|
457
545
|
if (!gate?.thread) return null;
|
|
458
546
|
const current = gate.current_decision;
|
|
@@ -472,19 +560,6 @@ function summarizeDecision(gate) {
|
|
|
472
560
|
};
|
|
473
561
|
}
|
|
474
562
|
|
|
475
|
-
function decisionNextAction(gate) {
|
|
476
|
-
if (gate.blockers?.some((blocker) => blocker.startsWith('DECISION_CHECKPOINT_ARTIFACT_STALE:'))) {
|
|
477
|
-
return `Reprepare and confirm decision checkpoint ${gate.thread?.id || 'for the active alignment thread'} against the current artifact.`;
|
|
478
|
-
}
|
|
479
|
-
if (gate.current_decision) {
|
|
480
|
-
return `Answer decision ${gate.current_decision.id}: ${gate.current_decision.question}`;
|
|
481
|
-
}
|
|
482
|
-
if (gate.thread?.status === 'checkpoint_ready') {
|
|
483
|
-
return `Confirm decision checkpoint ${gate.thread.id} after reviewing its durable effects.`;
|
|
484
|
-
}
|
|
485
|
-
return `Prepare the confirmed decision checkpoint for ${gate.thread?.id || 'the active alignment thread'}.`;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
563
|
function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
489
564
|
const baselineHealth = baselines.inspectBaseline(db, { rootDir });
|
|
490
565
|
const change = id
|
|
@@ -505,7 +580,7 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
505
580
|
change_id: null, task_id: null, role: 'plan', gate: 'alignment', readiness: 'blocked',
|
|
506
581
|
blockers: [...new Set([...baselineHealth.blockers, ...decisionGate.blockers])],
|
|
507
582
|
warnings: baselineHealth.warnings,
|
|
508
|
-
|
|
583
|
+
allowed_transitions: ['ultra-think', 'ultra-status'], required_transition: 'ultra-think',
|
|
509
584
|
workflow: null, decision: summarizeDecision(decisionGate),
|
|
510
585
|
context_manifest_path: null, context_manifest_hash: null, git_head: currentHead(rootDir),
|
|
511
586
|
baseline: baseline ? {
|
|
@@ -516,15 +591,24 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
516
591
|
}
|
|
517
592
|
if (baselineHealth.status !== 'pass') {
|
|
518
593
|
const activeWorkflow = actionableBaselineWorkflow(db, baseline?.id, rootDir);
|
|
519
|
-
const
|
|
520
|
-
?
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
594
|
+
const control = baseline
|
|
595
|
+
? {
|
|
596
|
+
allowed_transitions: uniqueStrings([
|
|
597
|
+
activeWorkflow ? `ultra-${activeWorkflow.kind}` : 'ultra-research',
|
|
598
|
+
'ultra-status',
|
|
599
|
+
'ultra-doctor',
|
|
600
|
+
]),
|
|
601
|
+
required_transition: null,
|
|
602
|
+
}
|
|
603
|
+
: {
|
|
604
|
+
allowed_transitions: ['ultra-init', 'ultra-status'],
|
|
605
|
+
required_transition: 'ultra-init',
|
|
606
|
+
};
|
|
524
607
|
return {
|
|
525
608
|
change_id: null, task_id: null, role: 'plan', gate: 'alignment', readiness: 'blocked',
|
|
526
|
-
blockers: baselineHealth.blockers, warnings: baselineHealth.warnings,
|
|
527
|
-
|
|
609
|
+
blockers: baselineHealth.blockers, warnings: baselineHealth.warnings,
|
|
610
|
+
allowed_transitions: control.allowed_transitions,
|
|
611
|
+
required_transition: control.required_transition,
|
|
528
612
|
workflow: summarizeWorkflow(activeWorkflow), decision: null, context_manifest_path: null,
|
|
529
613
|
context_manifest_hash: null, git_head: currentHead(rootDir),
|
|
530
614
|
baseline: baseline ? {
|
|
@@ -535,7 +619,8 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
535
619
|
}
|
|
536
620
|
return {
|
|
537
621
|
change_id: null, task_id: null, role: 'plan', gate: 'alignment', readiness: 'ready',
|
|
538
|
-
|
|
622
|
+
allowed_transitions: ['ultra-change', 'ultra-research', 'ultra-think', 'ultra-status'],
|
|
623
|
+
required_transition: null,
|
|
539
624
|
workflow: null,
|
|
540
625
|
decision: null,
|
|
541
626
|
context_manifest_path: null, context_manifest_hash: null, git_head: currentHead(rootDir),
|
|
@@ -564,6 +649,7 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
564
649
|
!context.resume || !context.context || !context.readiness || !('baseline' in context)
|
|
565
650
|
|| !Object.prototype.hasOwnProperty.call(context.resume, 'worktree_digest')
|
|
566
651
|
|| !Object.prototype.hasOwnProperty.call(context.resume, 'task_state_digest')
|
|
652
|
+
|| !Object.prototype.hasOwnProperty.call(context.resume, 'change_state_digest')
|
|
567
653
|
),
|
|
568
654
|
);
|
|
569
655
|
const headStale = Boolean(snapshot?.git_head && head && snapshot.git_head !== head);
|
|
@@ -576,6 +662,9 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
576
662
|
const taskContractChanged = Boolean(
|
|
577
663
|
context.resume && context.resume.task_state_digest !== taskStateDigest(task),
|
|
578
664
|
);
|
|
665
|
+
const changeContractChanged = Boolean(
|
|
666
|
+
context.resume && context.resume.change_state_digest !== changeStateDigest(change),
|
|
667
|
+
);
|
|
579
668
|
const blockers = [];
|
|
580
669
|
const baselineGate = baselineGateForChange(db, change, baselineHealth);
|
|
581
670
|
const decisionGate = decisions.decisionGate(db, {
|
|
@@ -586,13 +675,15 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
586
675
|
const warnings = [...baselineGate.warnings];
|
|
587
676
|
blockers.push(...baselineGate.blockers);
|
|
588
677
|
blockers.push(...decisionGate.blockers);
|
|
589
|
-
if (snapshotMissing) blockers.push('CONTEXT_NOT_COMPILED');
|
|
678
|
+
if (snapshotMissing && tasks.length > 0) blockers.push('CONTEXT_NOT_COMPILED');
|
|
679
|
+
else if (snapshotMissing) warnings.push('CONTEXT_NOT_COMPILED');
|
|
590
680
|
else {
|
|
591
681
|
if (legacyContext) blockers.push('CONTEXT_SNAPSHOT_UPGRADE_REQUIRED');
|
|
592
682
|
if (headStale) blockers.push('CONTEXT_HEAD_STALE');
|
|
593
683
|
if (worktreeStale) blockers.push('CONTEXT_WORKTREE_STALE');
|
|
594
684
|
if (stateChanged) blockers.push('CONTEXT_TASK_STATE_STALE');
|
|
595
685
|
if (taskContractChanged) blockers.push('CONTEXT_TASK_CONTRACT_STALE');
|
|
686
|
+
if (changeContractChanged) blockers.push('CONTEXT_CHANGE_CONTRACT_STALE');
|
|
596
687
|
for (const blocker of parseJson(snapshot.blockers_json, [])) {
|
|
597
688
|
if (ADVISORY_CONTEXT_CODES.has(blocker)
|
|
598
689
|
|| (blocker.startsWith('BASELINE_') && baselineGate.blockers.length === 0)) {
|
|
@@ -603,38 +694,18 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
603
694
|
if (!warnings.includes(warning)) warnings.push(warning);
|
|
604
695
|
}
|
|
605
696
|
}
|
|
606
|
-
const readiness = blockers.length > 0 ? 'blocked' : snapshot
|
|
697
|
+
const readiness = blockers.length > 0 ? 'blocked' : (snapshot?.readiness || 'ready');
|
|
607
698
|
const activeWorkflow = actionableChangeWorkflow(db, change.id, rootDir);
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
} else if (snapshotMissing) {
|
|
619
|
-
nextAction = `Compile change.context for change ${change.id} before continuing.`;
|
|
620
|
-
} else if (legacyContext) {
|
|
621
|
-
nextAction = 'Recompile change.context to upgrade this legacy context snapshot.';
|
|
622
|
-
} else if (headStale) {
|
|
623
|
-
nextAction = 'Git HEAD changed after context compilation; recompile change.context.';
|
|
624
|
-
} else if (worktreeStale) {
|
|
625
|
-
nextAction = 'The worktree changed after context compilation; recompile change.context.';
|
|
626
|
-
} else if (stateChanged) {
|
|
627
|
-
nextAction = 'Task state changed after context compilation; recompile change.context.';
|
|
628
|
-
} else if (taskContractChanged) {
|
|
629
|
-
nextAction = 'The task execution contract changed after context compilation; recompile change.context.';
|
|
630
|
-
} else if (activeWorkflow) {
|
|
631
|
-
nextAction = workflowNextAction(activeWorkflow);
|
|
632
|
-
} else {
|
|
633
|
-
nextAction = deriveNextAction({
|
|
634
|
-
change, tasks, task, role: snapshot?.role || 'plan', readiness,
|
|
635
|
-
blockers,
|
|
636
|
-
});
|
|
637
|
-
}
|
|
699
|
+
const control = deriveTransitions({
|
|
700
|
+
change,
|
|
701
|
+
tasks,
|
|
702
|
+
task,
|
|
703
|
+
role: snapshot?.role || 'plan',
|
|
704
|
+
readiness,
|
|
705
|
+
blockers,
|
|
706
|
+
activeWorkflow,
|
|
707
|
+
deliveryReady: deliveryCapabilityReady(db, change, tasks, rootDir),
|
|
708
|
+
});
|
|
638
709
|
return {
|
|
639
710
|
change_id: change.id,
|
|
640
711
|
change_status: change.status,
|
|
@@ -646,14 +717,8 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
646
717
|
readiness,
|
|
647
718
|
blockers,
|
|
648
719
|
warnings: [...new Set(warnings)],
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
? (baselineHealth.baseline?.status === 'ready' ? 'ultra-doctor' : 'ultra-init')
|
|
652
|
-
: !decisionGate.ready
|
|
653
|
-
? 'ultra-think'
|
|
654
|
-
: activeWorkflow
|
|
655
|
-
? `ultra-${activeWorkflow.kind}`
|
|
656
|
-
: recommendedWorkflow(change, task, tasks, readiness),
|
|
720
|
+
allowed_transitions: control.allowed_transitions,
|
|
721
|
+
required_transition: control.required_transition,
|
|
657
722
|
context_manifest_path: snapshot?.manifest_path || null,
|
|
658
723
|
context_manifest_hash: snapshot?.manifest_hash || null,
|
|
659
724
|
git_head: head,
|
|
@@ -670,7 +735,8 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
670
735
|
module.exports = {
|
|
671
736
|
ContextSpineError,
|
|
672
737
|
baselineGateForChange,
|
|
738
|
+
changeStateDigest,
|
|
673
739
|
compileRoleContext,
|
|
674
|
-
|
|
740
|
+
deriveTransitions,
|
|
675
741
|
readBreadcrumb,
|
|
676
742
|
};
|