ultra-builder-pro-cli 0.15.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 +120 -1
- package/README.md +67 -30
- package/adapters/_shared/codex-assets.cjs +10 -5
- package/adapters/_shared/handbook.cjs +3 -2
- 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/commands/ultra-think.md +2 -2
- package/docs/AGENT-CONTEXT.md +38 -18
- package/docs/ARCHITECTURE.md +91 -13
- package/docs/LEGACY-CLI-CROSSWALK.md +163 -0
- package/docs/PLAN.zh-CN.md +19 -12
- package/docs/ROADMAP.md +5 -3
- package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
- package/docs/STATE-DB-ACCESS-POLICY.md +8 -6
- package/docs/USER-HANDBOOK-CONTRACT.md +8 -2
- package/docs/WORKFLOW-LIFECYCLE.md +179 -39
- package/hooks/pre_stop_check.py +3 -2
- package/mcp-server/lib/baseline-workflow.cjs +84 -16
- package/mcp-server/lib/change-workflow.cjs +107 -39
- package/mcp-server/lib/context-spine.cjs +215 -87
- package/mcp-server/lib/decision-dialogue.cjs +713 -0
- package/mcp-server/lib/doctor.cjs +9 -0
- 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 +23 -14
- package/mcp-server/lib/state-db.cjs +279 -9
- package/mcp-server/lib/state-ops.cjs +64 -1
- package/mcp-server/lib/workflow-state.cjs +313 -205
- package/mcp-server/server.cjs +112 -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 -65
- package/skills/ultra-deliver/SKILL.md +37 -58
- package/skills/ultra-dev/SKILL.md +55 -49
- package/skills/ultra-doctor/SKILL.md +44 -49
- package/skills/ultra-init/SKILL.md +78 -100
- package/skills/ultra-plan/SKILL.md +78 -79
- package/skills/ultra-research/SKILL.md +99 -98
- package/skills/ultra-review/SKILL.md +38 -47
- 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 -40
- package/skills/ultra-test/SKILL.md +61 -48
- package/skills/ultra-think/SKILL.md +47 -47
- package/skills/ultra-think/references/decision-dialogue.md +106 -0
- package/spec/cli-protocol.md +46 -2
- package/spec/mcp-tools.yaml +444 -29
- package/spec/schemas/delivery-report.v1.schema.json +1 -46
- package/spec/schemas/mcp-tools.schema.json +1 -1
- package/spec/schemas/state-db.sql +66 -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 +34 -14
- package/ultra-tools/commands/system.cjs +2 -2
- package/ultra-tools/commands/task.cjs +3 -0
|
@@ -6,6 +6,7 @@ const path = require('node:path');
|
|
|
6
6
|
const { spawnSync } = require('node:child_process');
|
|
7
7
|
const baselines = require('./baseline-workflow.cjs');
|
|
8
8
|
const workflows = require('./workflow-state.cjs');
|
|
9
|
+
const decisions = require('./decision-dialogue.cjs');
|
|
9
10
|
const ops = require('./state-ops.cjs');
|
|
10
11
|
|
|
11
12
|
const ROLES = new Set(['plan', 'implement', 'check', 'review']);
|
|
@@ -51,6 +52,10 @@ function parseJson(value, fallback = {}) {
|
|
|
51
52
|
try { return JSON.parse(value); } catch { return fallback; }
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
function uniqueStrings(values) {
|
|
56
|
+
return [...new Set(values.filter((value) => typeof value === 'string' && value.length > 0))];
|
|
57
|
+
}
|
|
58
|
+
|
|
54
59
|
function hasIncidentBreakGlass(change) {
|
|
55
60
|
const bypass = change?.baseline_bypass
|
|
56
61
|
|| parseJson(change?.baseline_bypass_json, null);
|
|
@@ -127,6 +132,22 @@ function taskStateDigest(task) {
|
|
|
127
132
|
return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex');
|
|
128
133
|
}
|
|
129
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
|
+
|
|
130
151
|
function nonEmpty(value, field) {
|
|
131
152
|
const text = typeof value === 'string' ? value.trim() : '';
|
|
132
153
|
if (!text) throw new ContextSpineError('VALIDATION_ERROR', `${field} must be a non-empty string`);
|
|
@@ -290,27 +311,106 @@ function defaultGate(role) {
|
|
|
290
311
|
}[role];
|
|
291
312
|
}
|
|
292
313
|
|
|
293
|
-
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
|
+
}
|
|
294
358
|
if (readiness === 'blocked') {
|
|
359
|
+
if (blockers.some((blocker) => blocker.startsWith('CONTEXT_')
|
|
360
|
+
|| blocker.startsWith('EXECUTION_'))) {
|
|
361
|
+
transitions.push('change.context');
|
|
362
|
+
}
|
|
295
363
|
if (blockers.some((blocker) => blocker.startsWith('BASELINE_'))) {
|
|
296
|
-
|
|
364
|
+
transitions.push('ultra-research', 'ultra-doctor');
|
|
297
365
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
if (change.status === '
|
|
302
|
-
|
|
303
|
-
if (
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
+
}
|
|
314
414
|
}
|
|
315
415
|
|
|
316
416
|
function latestContext(db, changeId, taskId) {
|
|
@@ -327,8 +427,8 @@ function latestContext(db, changeId, taskId) {
|
|
|
327
427
|
function compileRoleContext(db, { input, change, tasks, rootDir }) {
|
|
328
428
|
if (input.next_action !== undefined) {
|
|
329
429
|
throw new ContextSpineError(
|
|
330
|
-
'
|
|
331
|
-
'next_action is
|
|
430
|
+
'CONTEXT_LEGACY_NEXT_ACTION_UNSUPPORTED',
|
|
431
|
+
'next_action is not an authority field; provide a non-authoritative recommendation instead',
|
|
332
432
|
);
|
|
333
433
|
}
|
|
334
434
|
const task = input.task_id ? tasks.find((row) => row.id === input.task_id) : null;
|
|
@@ -375,13 +475,20 @@ function compileRoleContext(db, { input, change, tasks, rootDir }) {
|
|
|
375
475
|
}
|
|
376
476
|
}
|
|
377
477
|
const readiness = blockers.length === 0 ? 'ready' : 'blocked';
|
|
378
|
-
const
|
|
379
|
-
change,
|
|
478
|
+
const control = deriveTransitions({
|
|
479
|
+
change,
|
|
480
|
+
tasks,
|
|
481
|
+
task,
|
|
482
|
+
role,
|
|
483
|
+
readiness,
|
|
484
|
+
blockers,
|
|
485
|
+
deliveryReady: deliveryCapabilityReady(db, change, tasks, rootDir),
|
|
380
486
|
});
|
|
381
487
|
return {
|
|
382
488
|
role,
|
|
383
489
|
gate,
|
|
384
|
-
|
|
490
|
+
control,
|
|
491
|
+
recommendation: normalizeRecommendation(input.recommendation),
|
|
385
492
|
readiness: { status: readiness, blockers, warnings: [...new Set(warnings)] },
|
|
386
493
|
context: { items, budget, token_estimate: tokenEstimate, file_count: fileCount },
|
|
387
494
|
execution_contract: executionContract,
|
|
@@ -396,6 +503,7 @@ function compileRoleContext(db, { input, change, tasks, rootDir }) {
|
|
|
396
503
|
selected_task: task,
|
|
397
504
|
resume: {
|
|
398
505
|
change_id: change.id,
|
|
506
|
+
change_state_digest: changeStateDigest(change),
|
|
399
507
|
task_id: task?.id || null,
|
|
400
508
|
task_status: task?.status || null,
|
|
401
509
|
task_state_digest: taskStateDigest(task),
|
|
@@ -409,15 +517,6 @@ function compileRoleContext(db, { input, change, tasks, rootDir }) {
|
|
|
409
517
|
};
|
|
410
518
|
}
|
|
411
519
|
|
|
412
|
-
function recommendedWorkflow(change, task, tasks, readiness) {
|
|
413
|
-
if (!change) return 'ultra-change';
|
|
414
|
-
if (readiness !== 'ready' || change.status === 'blocked') return 'ultra-change';
|
|
415
|
-
if (change.status === 'ready') return 'ultra-deliver';
|
|
416
|
-
if (task && ['pending', 'in_progress'].includes(task.status)) return 'ultra-dev';
|
|
417
|
-
if (tasks.length > 0 && tasks.every((row) => ['completed', 'expanded'].includes(row.status))) return 'ultra-test';
|
|
418
|
-
return 'ultra-change';
|
|
419
|
-
}
|
|
420
|
-
|
|
421
520
|
function actionableBaselineWorkflow(db, baselineId, rootDir) {
|
|
422
521
|
if (!baselineId) return null;
|
|
423
522
|
const candidates = workflows.listWorkflows(db, { baseline_id: baselineId, limit: 100 }, { rootDir })
|
|
@@ -442,14 +541,23 @@ function summarizeWorkflow(run) {
|
|
|
442
541
|
};
|
|
443
542
|
}
|
|
444
543
|
|
|
445
|
-
function
|
|
446
|
-
if (
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
544
|
+
function summarizeDecision(gate) {
|
|
545
|
+
if (!gate?.thread) return null;
|
|
546
|
+
const current = gate.current_decision;
|
|
547
|
+
return {
|
|
548
|
+
thread_id: gate.thread.id,
|
|
549
|
+
status: gate.thread.status,
|
|
550
|
+
mode: gate.thread.mode,
|
|
551
|
+
current: current ? {
|
|
552
|
+
id: current.id,
|
|
553
|
+
phase: current.phase,
|
|
554
|
+
question: current.question,
|
|
555
|
+
why_now: current.why_now,
|
|
556
|
+
recommendation: current.recommendation,
|
|
557
|
+
options: current.options,
|
|
558
|
+
effects: current.effects,
|
|
559
|
+
} : null,
|
|
560
|
+
};
|
|
453
561
|
}
|
|
454
562
|
|
|
455
563
|
function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
@@ -464,18 +572,44 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
464
572
|
}
|
|
465
573
|
if (!change) {
|
|
466
574
|
const baseline = baselineHealth.baseline;
|
|
575
|
+
const decisionGate = decisions.decisionGate(
|
|
576
|
+
db, { baseline_id: baseline?.id || null }, { rootDir },
|
|
577
|
+
);
|
|
578
|
+
if (!decisionGate.ready) {
|
|
579
|
+
return {
|
|
580
|
+
change_id: null, task_id: null, role: 'plan', gate: 'alignment', readiness: 'blocked',
|
|
581
|
+
blockers: [...new Set([...baselineHealth.blockers, ...decisionGate.blockers])],
|
|
582
|
+
warnings: baselineHealth.warnings,
|
|
583
|
+
allowed_transitions: ['ultra-think', 'ultra-status'], required_transition: 'ultra-think',
|
|
584
|
+
workflow: null, decision: summarizeDecision(decisionGate),
|
|
585
|
+
context_manifest_path: null, context_manifest_hash: null, git_head: currentHead(rootDir),
|
|
586
|
+
baseline: baseline ? {
|
|
587
|
+
id: baseline.id, mode: baseline.mode, status: baseline.status,
|
|
588
|
+
repository_revision: baseline.repository_revision,
|
|
589
|
+
} : null,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
467
592
|
if (baselineHealth.status !== 'pass') {
|
|
468
593
|
const activeWorkflow = actionableBaselineWorkflow(db, baseline?.id, rootDir);
|
|
469
|
-
const
|
|
470
|
-
?
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
+
};
|
|
474
607
|
return {
|
|
475
608
|
change_id: null, task_id: null, role: 'plan', gate: 'alignment', readiness: 'blocked',
|
|
476
|
-
blockers: baselineHealth.blockers, warnings: baselineHealth.warnings,
|
|
477
|
-
|
|
478
|
-
|
|
609
|
+
blockers: baselineHealth.blockers, warnings: baselineHealth.warnings,
|
|
610
|
+
allowed_transitions: control.allowed_transitions,
|
|
611
|
+
required_transition: control.required_transition,
|
|
612
|
+
workflow: summarizeWorkflow(activeWorkflow), decision: null, context_manifest_path: null,
|
|
479
613
|
context_manifest_hash: null, git_head: currentHead(rootDir),
|
|
480
614
|
baseline: baseline ? {
|
|
481
615
|
id: baseline.id, mode: baseline.mode, status: baseline.status,
|
|
@@ -485,8 +619,10 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
485
619
|
}
|
|
486
620
|
return {
|
|
487
621
|
change_id: null, task_id: null, role: 'plan', gate: 'alignment', readiness: 'ready',
|
|
488
|
-
|
|
622
|
+
allowed_transitions: ['ultra-change', 'ultra-research', 'ultra-think', 'ultra-status'],
|
|
623
|
+
required_transition: null,
|
|
489
624
|
workflow: null,
|
|
625
|
+
decision: null,
|
|
490
626
|
context_manifest_path: null, context_manifest_hash: null, git_head: currentHead(rootDir),
|
|
491
627
|
blockers: [], warnings: baselineHealth.warnings,
|
|
492
628
|
baseline: {
|
|
@@ -513,6 +649,7 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
513
649
|
!context.resume || !context.context || !context.readiness || !('baseline' in context)
|
|
514
650
|
|| !Object.prototype.hasOwnProperty.call(context.resume, 'worktree_digest')
|
|
515
651
|
|| !Object.prototype.hasOwnProperty.call(context.resume, 'task_state_digest')
|
|
652
|
+
|| !Object.prototype.hasOwnProperty.call(context.resume, 'change_state_digest')
|
|
516
653
|
),
|
|
517
654
|
);
|
|
518
655
|
const headStale = Boolean(snapshot?.git_head && head && snapshot.git_head !== head);
|
|
@@ -525,17 +662,28 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
525
662
|
const taskContractChanged = Boolean(
|
|
526
663
|
context.resume && context.resume.task_state_digest !== taskStateDigest(task),
|
|
527
664
|
);
|
|
665
|
+
const changeContractChanged = Boolean(
|
|
666
|
+
context.resume && context.resume.change_state_digest !== changeStateDigest(change),
|
|
667
|
+
);
|
|
528
668
|
const blockers = [];
|
|
529
669
|
const baselineGate = baselineGateForChange(db, change, baselineHealth);
|
|
670
|
+
const decisionGate = decisions.decisionGate(db, {
|
|
671
|
+
baseline_id: baselineHealth.baseline?.id || null,
|
|
672
|
+
change_id: change.id,
|
|
673
|
+
workflow_run_id: actionableChangeWorkflow(db, change.id, rootDir)?.id || null,
|
|
674
|
+
}, { rootDir });
|
|
530
675
|
const warnings = [...baselineGate.warnings];
|
|
531
676
|
blockers.push(...baselineGate.blockers);
|
|
532
|
-
|
|
677
|
+
blockers.push(...decisionGate.blockers);
|
|
678
|
+
if (snapshotMissing && tasks.length > 0) blockers.push('CONTEXT_NOT_COMPILED');
|
|
679
|
+
else if (snapshotMissing) warnings.push('CONTEXT_NOT_COMPILED');
|
|
533
680
|
else {
|
|
534
681
|
if (legacyContext) blockers.push('CONTEXT_SNAPSHOT_UPGRADE_REQUIRED');
|
|
535
682
|
if (headStale) blockers.push('CONTEXT_HEAD_STALE');
|
|
536
683
|
if (worktreeStale) blockers.push('CONTEXT_WORKTREE_STALE');
|
|
537
684
|
if (stateChanged) blockers.push('CONTEXT_TASK_STATE_STALE');
|
|
538
685
|
if (taskContractChanged) blockers.push('CONTEXT_TASK_CONTRACT_STALE');
|
|
686
|
+
if (changeContractChanged) blockers.push('CONTEXT_CHANGE_CONTRACT_STALE');
|
|
539
687
|
for (const blocker of parseJson(snapshot.blockers_json, [])) {
|
|
540
688
|
if (ADVISORY_CONTEXT_CODES.has(blocker)
|
|
541
689
|
|| (blocker.startsWith('BASELINE_') && baselineGate.blockers.length === 0)) {
|
|
@@ -546,36 +694,18 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
546
694
|
if (!warnings.includes(warning)) warnings.push(warning);
|
|
547
695
|
}
|
|
548
696
|
}
|
|
549
|
-
const readiness = blockers.length > 0 ? 'blocked' : snapshot
|
|
697
|
+
const readiness = blockers.length > 0 ? 'blocked' : (snapshot?.readiness || 'ready');
|
|
550
698
|
const activeWorkflow = actionableChangeWorkflow(db, change.id, rootDir);
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
} else if (legacyContext) {
|
|
562
|
-
nextAction = 'Recompile change.context to upgrade this legacy context snapshot.';
|
|
563
|
-
} else if (headStale) {
|
|
564
|
-
nextAction = 'Git HEAD changed after context compilation; recompile change.context.';
|
|
565
|
-
} else if (worktreeStale) {
|
|
566
|
-
nextAction = 'The worktree changed after context compilation; recompile change.context.';
|
|
567
|
-
} else if (stateChanged) {
|
|
568
|
-
nextAction = 'Task state changed after context compilation; recompile change.context.';
|
|
569
|
-
} else if (taskContractChanged) {
|
|
570
|
-
nextAction = 'The task execution contract changed after context compilation; recompile change.context.';
|
|
571
|
-
} else if (activeWorkflow) {
|
|
572
|
-
nextAction = workflowNextAction(activeWorkflow);
|
|
573
|
-
} else {
|
|
574
|
-
nextAction = deriveNextAction({
|
|
575
|
-
change, tasks, task, role: snapshot?.role || 'plan', readiness,
|
|
576
|
-
blockers,
|
|
577
|
-
});
|
|
578
|
-
}
|
|
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
|
+
});
|
|
579
709
|
return {
|
|
580
710
|
change_id: change.id,
|
|
581
711
|
change_status: change.status,
|
|
@@ -583,16 +713,12 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
583
713
|
task_status: task?.status || null,
|
|
584
714
|
session_id: task?.session_id || null,
|
|
585
715
|
role: snapshot?.role || 'plan',
|
|
586
|
-
gate: snapshot?.gate || 'alignment',
|
|
716
|
+
gate: decisionGate.ready ? (snapshot?.gate || 'alignment') : 'alignment',
|
|
587
717
|
readiness,
|
|
588
718
|
blockers,
|
|
589
719
|
warnings: [...new Set(warnings)],
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
? (baselineHealth.baseline?.status === 'ready' ? 'ultra-doctor' : 'ultra-init')
|
|
593
|
-
: activeWorkflow
|
|
594
|
-
? `ultra-${activeWorkflow.kind}`
|
|
595
|
-
: recommendedWorkflow(change, task, tasks, readiness),
|
|
720
|
+
allowed_transitions: control.allowed_transitions,
|
|
721
|
+
required_transition: control.required_transition,
|
|
596
722
|
context_manifest_path: snapshot?.manifest_path || null,
|
|
597
723
|
context_manifest_hash: snapshot?.manifest_hash || null,
|
|
598
724
|
git_head: head,
|
|
@@ -602,13 +728,15 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
602
728
|
repository_revision: baselineHealth.baseline.repository_revision,
|
|
603
729
|
} : null,
|
|
604
730
|
workflow: summarizeWorkflow(activeWorkflow),
|
|
731
|
+
decision: summarizeDecision(decisionGate),
|
|
605
732
|
};
|
|
606
733
|
}
|
|
607
734
|
|
|
608
735
|
module.exports = {
|
|
609
736
|
ContextSpineError,
|
|
610
737
|
baselineGateForChange,
|
|
738
|
+
changeStateDigest,
|
|
611
739
|
compileRoleContext,
|
|
612
|
-
|
|
740
|
+
deriveTransitions,
|
|
613
741
|
readBreadcrumb,
|
|
614
742
|
};
|