ultra-builder-pro-cli 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ultra-template/docs/research/README.md +11 -7
- package/.ultra-template/specs/architecture.md +36 -36
- package/.ultra-template/specs/discovery.md +46 -33
- package/.ultra-template/specs/product.md +38 -29
- package/.ultra-template/specs/research-distillate.md +48 -0
- package/.ultra-template/tasks/contexts/TEMPLATE.md +11 -28
- package/CHANGELOG.md +77 -1
- package/README.md +46 -24
- package/adapters/_shared/codex-assets.cjs +6 -2
- package/adapters/_shared/file-ops.cjs +6 -2
- package/adapters/_shared/handbook.cjs +2 -1
- package/adapters/claude.js +3 -2
- package/adapters/codex.js +1 -0
- package/adapters/kimi.js +3 -2
- package/adapters/opencode.js +3 -2
- package/agents/review-coordinator.md +9 -6
- package/commands/ultra-change.md +1 -1
- package/commands/ultra-plan.md +1 -1
- package/commands/ultra-research.md +1 -1
- package/commands/ultra-think.md +2 -2
- package/docs/AGENT-CONTEXT.md +43 -19
- package/docs/ARCHITECTURE.md +54 -13
- package/docs/PLAN.zh-CN.md +4 -2
- package/docs/ROADMAP.md +10 -4
- package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
- package/docs/STATE-DB-ACCESS-POLICY.md +33 -29
- package/docs/USER-HANDBOOK-CONTRACT.md +8 -2
- package/docs/WORKFLOW-LIFECYCLE.md +342 -0
- package/hooks/README.md +6 -1
- package/hooks/health_check.py +12 -3
- package/hooks/subagent_tracker.py +52 -77
- package/mcp-server/hook-event.cjs +34 -0
- package/mcp-server/lib/archive-journal.cjs +17 -5
- package/mcp-server/lib/baseline-workflow.cjs +158 -18
- package/mcp-server/lib/change-workflow.cjs +598 -85
- package/mcp-server/lib/context-spine.cjs +305 -25
- package/mcp-server/lib/decision-dialogue.cjs +713 -0
- package/mcp-server/lib/doctor.cjs +42 -3
- package/mcp-server/lib/init-project.cjs +210 -16
- package/mcp-server/lib/plan-store.cjs +4 -1
- package/mcp-server/lib/pricing.cjs +7 -10
- package/mcp-server/lib/project-breadcrumb.cjs +15 -0
- package/mcp-server/lib/projector.cjs +57 -3
- package/mcp-server/lib/spec-learning.cjs +79 -2
- package/mcp-server/lib/state-db.cjs +168 -8
- package/mcp-server/lib/state-ops.cjs +160 -9
- package/mcp-server/lib/workflow-state.cjs +2305 -0
- package/mcp-server/server.cjs +215 -15
- package/orchestrator/daemon.cjs +1 -2
- package/orchestrator/dispatch-rules.cjs +5 -31
- package/orchestrator/planner/plan-builder.cjs +21 -12
- package/package.json +13 -3
- package/skills/ultra-change/SKILL.md +94 -80
- package/skills/ultra-change/references/change-contract.md +36 -0
- package/skills/ultra-deliver/SKILL.md +76 -79
- package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
- package/skills/ultra-dev/SKILL.md +62 -59
- package/skills/ultra-doctor/SKILL.md +61 -82
- package/skills/ultra-init/SKILL.md +101 -103
- package/skills/ultra-init/references/brownfield-adoption.md +2 -2
- package/skills/ultra-plan/SKILL.md +99 -85
- package/skills/ultra-plan/references/semantic-preflight.md +18 -0
- package/skills/ultra-research/SKILL.md +102 -60
- package/skills/ultra-research/references/00-problem-validation.md +36 -0
- package/skills/ultra-research/references/01-opportunity-discovery.md +33 -0
- package/skills/ultra-research/references/02-market-assessment.md +30 -0
- package/skills/ultra-research/references/03-competitive-landscape.md +31 -0
- package/skills/ultra-research/references/04-product-strategy.md +32 -0
- package/skills/ultra-research/references/05-assumptions-validation.md +31 -0
- package/skills/ultra-research/references/10-user-personas.md +29 -0
- package/skills/ultra-research/references/11-user-scenarios.md +28 -0
- package/skills/ultra-research/references/20-user-stories.md +30 -0
- package/skills/ultra-research/references/21-features-scope.md +29 -0
- package/skills/ultra-research/references/22-success-metrics.md +28 -0
- package/skills/ultra-research/references/30-architecture-context.md +30 -0
- package/skills/ultra-research/references/31-solution-strategy.md +28 -0
- package/skills/ultra-research/references/32-building-blocks.md +28 -0
- package/skills/ultra-research/references/40-deployment.md +29 -0
- package/skills/ultra-research/references/41-quality-risks.md +28 -0
- package/skills/ultra-research/references/99-synthesis.md +35 -0
- package/skills/ultra-review/SKILL.md +63 -73
- package/skills/ultra-review/references/review-modes.md +27 -0
- package/skills/ultra-review/references/unified-schema.md +25 -0
- package/skills/ultra-review/scripts/review_wait.py +40 -3
- package/skills/ultra-status/SKILL.md +41 -41
- package/skills/ultra-test/SKILL.md +54 -55
- package/skills/ultra-think/SKILL.md +26 -12
- package/skills/ultra-think/references/decision-dialogue.md +130 -0
- package/skills/ultra-verify/SKILL.md +1 -1
- package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
- package/spec/cli-protocol.md +17 -3
- package/spec/mcp-tools.yaml +683 -64
- package/spec/schemas/baseline-reconciliation.v1.schema.json +73 -0
- package/spec/schemas/delivery-report.v1.schema.json +111 -0
- package/spec/schemas/mcp-tools.schema.json +1 -1
- package/spec/schemas/state-db.sql +140 -1
- package/spec/schemas/tasks.v4.5.schema.json +8 -1
- package/spec/schemas/test-report.v1.schema.json +194 -0
- package/ultra-tools/cli.cjs +1 -1
- package/ultra-tools/commands/migrate.cjs +3 -3
- package/ultra-tools/commands/status.cjs +228 -12
- package/.ultra-template/delivery-report.json +0 -9
- package/.ultra-template/test-report.json +0 -8
- package/adapters/_shared/tests/file-ops.test.cjs +0 -54
- package/adapters/_shared/tests/frontmatter.test.cjs +0 -47
- package/adapters/_shared/tests/handbook.test.cjs +0 -101
- package/adapters/_shared/tests/path-rewrite.test.cjs +0 -60
- package/adapters/_shared/tests/provenance.test.cjs +0 -133
- package/adapters/_shared/tests/resolve-target.test.cjs +0 -75
- package/adapters/_shared/tests/runtime-assets.test.cjs +0 -149
- package/adapters/_shared/tests/settings-merge.test.cjs +0 -85
- package/adapters/_shared/tests/validate.test.cjs +0 -30
- package/adapters/tests/claude.test.cjs +0 -138
- package/adapters/tests/codex-hook.test.cjs +0 -142
- package/adapters/tests/codex.test.cjs +0 -430
- package/adapters/tests/kimi-hook.test.cjs +0 -149
- package/adapters/tests/kimi.test.cjs +0 -259
- package/adapters/tests/opencode.test.cjs +0 -357
- package/mcp-server/lib/archive-journal.test.cjs +0 -35
- package/mcp-server/lib/baseline-workflow.test.cjs +0 -477
- package/mcp-server/lib/change-workflow.test.cjs +0 -971
- package/mcp-server/lib/context-spine.test.cjs +0 -93
- package/mcp-server/lib/doctor.test.cjs +0 -148
- package/mcp-server/lib/plan-store.test.cjs +0 -132
- package/mcp-server/lib/prd-parser.test.cjs +0 -82
- package/mcp-server/lib/project-breadcrumb.test.cjs +0 -112
- package/mcp-server/lib/runtime-state.test.cjs +0 -92
- package/mcp-server/lib/spec-learning.test.cjs +0 -67
- package/mcp-server/lib/state-db.test.cjs +0 -314
- package/mcp-server/lib/task-expander.test.cjs +0 -169
- package/mcp-server/lib/topo.test.cjs +0 -158
- package/mcp-server/tests/circuit-breaker.test.cjs +0 -125
- package/mcp-server/tests/commit-hash-flow.test.cjs +0 -117
- package/mcp-server/tests/concurrency-update.test.cjs +0 -96
- package/mcp-server/tests/concurrency.test.cjs +0 -112
- package/mcp-server/tests/db-ops.test.cjs +0 -121
- package/mcp-server/tests/init-project.test.cjs +0 -393
- package/mcp-server/tests/migrate.test.cjs +0 -232
- package/mcp-server/tests/projector.test.cjs +0 -162
- package/mcp-server/tests/server-contract.test.cjs +0 -1467
- package/mcp-server/tests/staleness.test.cjs +0 -190
- package/mcp-server/tests/state-db.test.cjs +0 -106
- package/mcp-server/tests/state-ops.test.cjs +0 -449
- package/mcp-server/tests/tagged-tasks.test.cjs +0 -163
- package/mcp-server/tests/telemetry.test.cjs +0 -219
- package/orchestrator/planner/plan-builder.test.cjs +0 -165
- package/orchestrator/tests/auto-merge.test.cjs +0 -216
- package/orchestrator/tests/daemon.test.cjs +0 -311
- package/orchestrator/tests/dispatch-rules.test.cjs +0 -124
- package/orchestrator/tests/parallel-orchestrator.test.cjs +0 -233
- package/orchestrator/tests/phase-8b-gate.test.cjs +0 -183
- package/orchestrator/tests/recovery.test.cjs +0 -190
- package/orchestrator/tests/session-runner.test.cjs +0 -314
- package/orchestrator/tests/skill-miner.test.cjs +0 -147
- package/orchestrator/tests/worktree-manager.test.cjs +0 -148
- package/skills/ultra-research/references/architecture.md +0 -38
- package/skills/ultra-research/references/problem-product.md +0 -39
- package/skills/ultra-research/references/quality-delivery.md +0 -34
- package/skills/ultra-research/references/synthesis.md +0 -38
- package/skills/ultra-research/references/users-scope.md +0 -40
- package/ultra-tools/legacy-memory.test.cjs +0 -80
- package/ultra-tools/status-command.test.cjs +0 -181
- package/ultra-tools/system-command.test.cjs +0 -309
|
@@ -5,6 +5,9 @@ const fs = require('node:fs');
|
|
|
5
5
|
const path = require('node:path');
|
|
6
6
|
const { spawnSync } = require('node:child_process');
|
|
7
7
|
const baselines = require('./baseline-workflow.cjs');
|
|
8
|
+
const workflows = require('./workflow-state.cjs');
|
|
9
|
+
const decisions = require('./decision-dialogue.cjs');
|
|
10
|
+
const ops = require('./state-ops.cjs');
|
|
8
11
|
|
|
9
12
|
const ROLES = new Set(['plan', 'implement', 'check', 'review']);
|
|
10
13
|
const GATES = new Set([
|
|
@@ -19,6 +22,15 @@ const ADVISORY_CONTEXT_CODES = new Set([
|
|
|
19
22
|
'EXECUTION_CONTEXT_BUDGET_EXCEEDED',
|
|
20
23
|
'EXECUTION_CONTEXT_BUDGET_ADVISORY',
|
|
21
24
|
]);
|
|
25
|
+
const ACTIVE_CHANGE_BASELINE_DRIFT = Object.freeze([
|
|
26
|
+
/^BASELINE_HEAD_STALE$/,
|
|
27
|
+
/^BASELINE_BRANCH_STALE$/,
|
|
28
|
+
/^BASELINE_WORKSPACE_STALE$/,
|
|
29
|
+
/^BASELINE_WORKTREE_STALE$/,
|
|
30
|
+
/^BASELINE_WORKTREE_DIRTY$/,
|
|
31
|
+
/^BASELINE_SPEC_STALE:/,
|
|
32
|
+
/^BASELINE_EVIDENCE_STALE:/,
|
|
33
|
+
]);
|
|
22
34
|
|
|
23
35
|
class ContextSpineError extends Error {
|
|
24
36
|
constructor(code, message) {
|
|
@@ -40,6 +52,82 @@ function parseJson(value, fallback = {}) {
|
|
|
40
52
|
try { return JSON.parse(value); } catch { return fallback; }
|
|
41
53
|
}
|
|
42
54
|
|
|
55
|
+
function hasIncidentBreakGlass(change) {
|
|
56
|
+
const bypass = change?.baseline_bypass
|
|
57
|
+
|| parseJson(change?.baseline_bypass_json, null);
|
|
58
|
+
return change?.kind === 'incident'
|
|
59
|
+
&& bypass?.mode === 'incident_break_glass'
|
|
60
|
+
&& Boolean(String(bypass.reason || '').trim())
|
|
61
|
+
&& Boolean(String(bypass.approved_by || '').trim());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function expectedActiveChangeDrift(code) {
|
|
65
|
+
return ACTIVE_CHANGE_BASELINE_DRIFT.some((pattern) => pattern.test(code));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function baselineGateForChange(db, change, baselineHealth) {
|
|
69
|
+
const baseWarnings = Array.isArray(baselineHealth.warnings) ? baselineHealth.warnings : [];
|
|
70
|
+
if (baselineHealth.status === 'pass') {
|
|
71
|
+
return { blockers: [], warnings: [...new Set(baseWarnings)], mode: 'healthy' };
|
|
72
|
+
}
|
|
73
|
+
const healthBlockers = Array.isArray(baselineHealth.blockers)
|
|
74
|
+
? baselineHealth.blockers : ['BASELINE_STATE_UNAVAILABLE'];
|
|
75
|
+
if (hasIncidentBreakGlass(change)) {
|
|
76
|
+
return {
|
|
77
|
+
blockers: [], warnings: [...new Set([...healthBlockers, ...baseWarnings])],
|
|
78
|
+
mode: 'incident_break_glass',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const baseline = baselineHealth.baseline;
|
|
82
|
+
if (!baseline || baseline.status !== 'ready' || baseline.mode === 'migrated') {
|
|
83
|
+
return { blockers: healthBlockers, warnings: baseWarnings, mode: 'baseline_required' };
|
|
84
|
+
}
|
|
85
|
+
const binding = db.prepare(
|
|
86
|
+
`SELECT wr.baseline_id, ws.status AS binding_status, ws.evidence_json
|
|
87
|
+
FROM workflow_runs wr
|
|
88
|
+
LEFT JOIN workflow_steps ws
|
|
89
|
+
ON ws.run_id = wr.id AND ws.step_id = 'bind-baseline'
|
|
90
|
+
WHERE wr.kind = 'change' AND wr.change_id = ?
|
|
91
|
+
ORDER BY wr.started_at DESC, wr.rowid DESC LIMIT 1`,
|
|
92
|
+
).get(change.id);
|
|
93
|
+
let evidence = [];
|
|
94
|
+
try { evidence = JSON.parse(binding?.evidence_json || '[]'); } catch { evidence = []; }
|
|
95
|
+
const boundFromReadyAuthority = binding?.baseline_id === baseline.id
|
|
96
|
+
&& binding.binding_status === 'completed'
|
|
97
|
+
&& evidence.some((item) => item?.ref === baseline.id);
|
|
98
|
+
if (!boundFromReadyAuthority) {
|
|
99
|
+
return { blockers: healthBlockers, warnings: baseWarnings, mode: 'baseline_binding_required' };
|
|
100
|
+
}
|
|
101
|
+
const blockers = healthBlockers.filter((code) => !expectedActiveChangeDrift(code));
|
|
102
|
+
const driftWarnings = healthBlockers.filter(expectedActiveChangeDrift);
|
|
103
|
+
return {
|
|
104
|
+
blockers,
|
|
105
|
+
warnings: [...new Set([...driftWarnings, ...baseWarnings])],
|
|
106
|
+
mode: blockers.length === 0 ? 'active_change_drift' : 'baseline_invalid',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function taskStateDigest(task) {
|
|
111
|
+
if (!task) return null;
|
|
112
|
+
const payload = {
|
|
113
|
+
id: task.id,
|
|
114
|
+
change_id: task.change_id,
|
|
115
|
+
status: task.status,
|
|
116
|
+
stale: Boolean(task.stale),
|
|
117
|
+
deps: task.deps || [],
|
|
118
|
+
outcome: task.outcome,
|
|
119
|
+
slice_kind: task.slice_kind,
|
|
120
|
+
public_seam: task.public_seam,
|
|
121
|
+
verification_command: task.verification_command,
|
|
122
|
+
acceptance: task.acceptance || [],
|
|
123
|
+
context_refs: task.context_refs || [],
|
|
124
|
+
docs_impact: task.docs_impact || {},
|
|
125
|
+
ownership: task.ownership || {},
|
|
126
|
+
trace_to: task.trace_to,
|
|
127
|
+
};
|
|
128
|
+
return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex');
|
|
129
|
+
}
|
|
130
|
+
|
|
43
131
|
function nonEmpty(value, field) {
|
|
44
132
|
const text = typeof value === 'string' ? value.trim() : '';
|
|
45
133
|
if (!text) throw new ContextSpineError('VALIDATION_ERROR', `${field} must be a non-empty string`);
|
|
@@ -111,6 +199,27 @@ function normalizeRefs(input, role, inherited) {
|
|
|
111
199
|
});
|
|
112
200
|
}
|
|
113
201
|
|
|
202
|
+
function authoritativeTaskRefs(input, role, task) {
|
|
203
|
+
if (!task) return null;
|
|
204
|
+
const dbRefs = normalizeRefs({ context_refs: task.context_refs || [] }, role, []);
|
|
205
|
+
if (input.context_refs === undefined && input.spec_refs === undefined) return dbRefs;
|
|
206
|
+
const supplied = normalizeRefs(input, role, []);
|
|
207
|
+
const comparable = (items) => items.map((item) => ({
|
|
208
|
+
ref: item.ref,
|
|
209
|
+
kind: item.kind,
|
|
210
|
+
reason: item.reason,
|
|
211
|
+
required: item.required,
|
|
212
|
+
expected_digest: item.expected_digest,
|
|
213
|
+
}));
|
|
214
|
+
if (JSON.stringify(comparable(supplied)) !== JSON.stringify(comparable(dbRefs))) {
|
|
215
|
+
throw new ContextSpineError(
|
|
216
|
+
'EXECUTION_CONTEXT_REFS_CONFLICT',
|
|
217
|
+
`context_refs conflict with task ${task.id} authority; update the task contract first`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
return dbRefs;
|
|
221
|
+
}
|
|
222
|
+
|
|
114
223
|
function inspectRef(rootDir, role, value) {
|
|
115
224
|
if (value.kind === 'external' || /^[a-z][a-z0-9+.-]*:\/\//i.test(value.ref)) {
|
|
116
225
|
return { ...value, role, status: 'external', digest: value.expected_digest, estimated_tokens: 0 };
|
|
@@ -128,9 +237,35 @@ function inspectRef(rootDir, role, value) {
|
|
|
128
237
|
};
|
|
129
238
|
}
|
|
130
239
|
|
|
131
|
-
function normalizeExecutionContract(input, inherited, {
|
|
132
|
-
const
|
|
133
|
-
|
|
240
|
+
function normalizeExecutionContract(input, inherited, { task }) {
|
|
241
|
+
const taskContract = task
|
|
242
|
+
&& task.slice_kind && task.public_seam && task.verification_command ? {
|
|
243
|
+
slice_kind: task.slice_kind,
|
|
244
|
+
public_seam: task.public_seam,
|
|
245
|
+
verification_command: task.verification_command,
|
|
246
|
+
context_budget_percent: input.execution_contract?.context_budget_percent
|
|
247
|
+
?? inherited?.context_budget_percent
|
|
248
|
+
?? 40,
|
|
249
|
+
} : null;
|
|
250
|
+
if (input.execution_contract) {
|
|
251
|
+
for (const field of ['slice_kind', 'public_seam', 'verification_command']) {
|
|
252
|
+
if (input.execution_contract[field] !== undefined) {
|
|
253
|
+
throw new ContextSpineError(
|
|
254
|
+
'EXECUTION_CONTRACT_CONFLICT',
|
|
255
|
+
`execution_contract.${field} is owned by the task row and cannot be supplied by Prompt input`,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (!task && input.execution_contract !== undefined) {
|
|
261
|
+
throw new ContextSpineError(
|
|
262
|
+
'EXECUTION_CONTRACT_CONFLICT',
|
|
263
|
+
'execution_contract is valid only for a DB-backed task context',
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
const source = taskContract || input.execution_contract || inherited || null;
|
|
267
|
+
const taskId = task?.id || null;
|
|
268
|
+
if (!taskId) return null;
|
|
134
269
|
if (!source || typeof source !== 'object' || Array.isArray(source)) return null;
|
|
135
270
|
const sliceKind = source.slice_kind || 'tracer_bullet';
|
|
136
271
|
if (!SLICE_KINDS.has(sliceKind)) {
|
|
@@ -156,14 +291,13 @@ function defaultGate(role) {
|
|
|
156
291
|
}[role];
|
|
157
292
|
}
|
|
158
293
|
|
|
159
|
-
function deriveNextAction({ change, tasks, task, role, readiness, blockers = []
|
|
294
|
+
function deriveNextAction({ change, tasks, task, role, readiness, blockers = [] }) {
|
|
160
295
|
if (readiness === 'blocked') {
|
|
161
296
|
if (blockers.some((blocker) => blocker.startsWith('BASELINE_'))) {
|
|
162
297
|
return 'Complete or refresh the Ultra project baseline, then recompile change.context.';
|
|
163
298
|
}
|
|
164
299
|
return 'Resolve the context readiness blockers, then recompile change.context.';
|
|
165
300
|
}
|
|
166
|
-
if (explicit && String(explicit).trim()) return String(explicit).trim();
|
|
167
301
|
if (change.status === 'ready') return `Run ultra-deliver for change ${change.id}.`;
|
|
168
302
|
if (change.status === 'blocked') return `Resolve the blockers for change ${change.id}, then recompile context.`;
|
|
169
303
|
if (!task && tasks.length === 0) return `Create the first fresh-context tracer-bullet task for change ${change.id}.`;
|
|
@@ -192,6 +326,12 @@ function latestContext(db, changeId, taskId) {
|
|
|
192
326
|
}
|
|
193
327
|
|
|
194
328
|
function compileRoleContext(db, { input, change, tasks, rootDir }) {
|
|
329
|
+
if (input.next_action !== undefined) {
|
|
330
|
+
throw new ContextSpineError(
|
|
331
|
+
'CONTEXT_NEXT_ACTION_AUTHORITY_VIOLATION',
|
|
332
|
+
'next_action is derived from authoritative workflow and task state',
|
|
333
|
+
);
|
|
334
|
+
}
|
|
195
335
|
const task = input.task_id ? tasks.find((row) => row.id === input.task_id) : null;
|
|
196
336
|
if (input.task_id && !task) {
|
|
197
337
|
throw new ContextSpineError('TASK_NOT_FOUND', `task ${input.task_id} is not linked to change ${change.id}`);
|
|
@@ -202,22 +342,22 @@ function compileRoleContext(db, { input, change, tasks, rootDir }) {
|
|
|
202
342
|
const gate = input.gate || defaultGate(role);
|
|
203
343
|
if (!GATES.has(gate)) throw new ContextSpineError('VALIDATION_ERROR', `unsupported workflow gate: ${gate}`);
|
|
204
344
|
|
|
205
|
-
const refs =
|
|
345
|
+
const refs = authoritativeTaskRefs(input, role, task)
|
|
346
|
+
|| normalizeRefs(input, role, inherited.context?.items || []);
|
|
206
347
|
const items = refs.map((value) => inspectRef(rootDir, role, value));
|
|
207
348
|
const budget = normalizeBudget(input.budget, inherited.context?.budget);
|
|
208
349
|
const tokenEstimate = items.reduce((sum, item) => sum + item.estimated_tokens, 0);
|
|
209
350
|
const fileCount = items.filter((item) => item.status !== 'external').length;
|
|
210
351
|
const executionContract = normalizeExecutionContract(
|
|
211
|
-
input, inherited.execution_contract, {
|
|
352
|
+
input, inherited.execution_contract, { task },
|
|
212
353
|
);
|
|
213
354
|
const blockers = [];
|
|
214
355
|
const warnings = [];
|
|
215
356
|
const baselineHealth = baselines.inspectBaseline(db, { rootDir });
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
warnings.push(...baselineHealth.blockers, ...baselineHealth.warnings);
|
|
357
|
+
const checkout = baselines.gitWorktreeSnapshot(rootDir, ['.']);
|
|
358
|
+
const baselineGate = baselineGateForChange(db, change, baselineHealth);
|
|
359
|
+
blockers.push(...baselineGate.blockers);
|
|
360
|
+
warnings.push(...baselineGate.warnings);
|
|
221
361
|
for (const item of items) {
|
|
222
362
|
if (item.required && item.status === 'missing') blockers.push(`CONTEXT_REQUIRED_REF_MISSING:${item.ref}`);
|
|
223
363
|
if (item.required && item.status === 'stale') blockers.push(`CONTEXT_REQUIRED_REF_STALE:${item.ref}`);
|
|
@@ -237,7 +377,7 @@ function compileRoleContext(db, { input, change, tasks, rootDir }) {
|
|
|
237
377
|
}
|
|
238
378
|
const readiness = blockers.length === 0 ? 'ready' : 'blocked';
|
|
239
379
|
const nextAction = deriveNextAction({
|
|
240
|
-
change, tasks, task, role, readiness, blockers,
|
|
380
|
+
change, tasks, task, role, readiness, blockers,
|
|
241
381
|
});
|
|
242
382
|
return {
|
|
243
383
|
role,
|
|
@@ -259,10 +399,13 @@ function compileRoleContext(db, { input, change, tasks, rootDir }) {
|
|
|
259
399
|
change_id: change.id,
|
|
260
400
|
task_id: task?.id || null,
|
|
261
401
|
task_status: task?.status || null,
|
|
402
|
+
task_state_digest: taskStateDigest(task),
|
|
262
403
|
session_id: task?.session_id || null,
|
|
263
404
|
role,
|
|
264
405
|
gate,
|
|
265
|
-
git_head:
|
|
406
|
+
git_head: checkout.head,
|
|
407
|
+
worktree_state: checkout.state,
|
|
408
|
+
worktree_digest: checkout.digest,
|
|
266
409
|
},
|
|
267
410
|
};
|
|
268
411
|
}
|
|
@@ -276,6 +419,72 @@ function recommendedWorkflow(change, task, tasks, readiness) {
|
|
|
276
419
|
return 'ultra-change';
|
|
277
420
|
}
|
|
278
421
|
|
|
422
|
+
function actionableBaselineWorkflow(db, baselineId, rootDir) {
|
|
423
|
+
if (!baselineId) return null;
|
|
424
|
+
const candidates = workflows.listWorkflows(db, { baseline_id: baselineId, limit: 100 }, { rootDir })
|
|
425
|
+
.filter((run) => ['active', 'blocked', 'ready'].includes(run.status));
|
|
426
|
+
return candidates.find((run) => run.kind === 'research')
|
|
427
|
+
|| candidates.find((run) => run.kind === 'init')
|
|
428
|
+
|| candidates[0]
|
|
429
|
+
|| null;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function actionableChangeWorkflow(db, changeId, rootDir) {
|
|
433
|
+
const candidates = workflows.listWorkflows(db, { change_id: changeId, limit: 100 }, { rootDir })
|
|
434
|
+
.filter((run) => ['active', 'blocked', 'ready'].includes(run.status));
|
|
435
|
+
return candidates.find((run) => run.status === 'blocked') || candidates[0] || null;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function summarizeWorkflow(run) {
|
|
439
|
+
if (!run) return null;
|
|
440
|
+
return {
|
|
441
|
+
id: run.id, kind: run.kind, mode: run.mode, status: run.status,
|
|
442
|
+
current_step: run.current_step, blockers: run.blockers,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
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
|
+
function summarizeDecision(gate) {
|
|
457
|
+
if (!gate?.thread) return null;
|
|
458
|
+
const current = gate.current_decision;
|
|
459
|
+
return {
|
|
460
|
+
thread_id: gate.thread.id,
|
|
461
|
+
status: gate.thread.status,
|
|
462
|
+
mode: gate.thread.mode,
|
|
463
|
+
current: current ? {
|
|
464
|
+
id: current.id,
|
|
465
|
+
phase: current.phase,
|
|
466
|
+
question: current.question,
|
|
467
|
+
why_now: current.why_now,
|
|
468
|
+
recommendation: current.recommendation,
|
|
469
|
+
options: current.options,
|
|
470
|
+
effects: current.effects,
|
|
471
|
+
} : null,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
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
|
+
|
|
279
488
|
function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
280
489
|
const baselineHealth = baselines.inspectBaseline(db, { rootDir });
|
|
281
490
|
const change = id
|
|
@@ -288,14 +497,35 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
288
497
|
}
|
|
289
498
|
if (!change) {
|
|
290
499
|
const baseline = baselineHealth.baseline;
|
|
500
|
+
const decisionGate = decisions.decisionGate(
|
|
501
|
+
db, { baseline_id: baseline?.id || null }, { rootDir },
|
|
502
|
+
);
|
|
503
|
+
if (!decisionGate.ready) {
|
|
504
|
+
return {
|
|
505
|
+
change_id: null, task_id: null, role: 'plan', gate: 'alignment', readiness: 'blocked',
|
|
506
|
+
blockers: [...new Set([...baselineHealth.blockers, ...decisionGate.blockers])],
|
|
507
|
+
warnings: baselineHealth.warnings,
|
|
508
|
+
next_action: decisionNextAction(decisionGate), recommended_workflow: 'ultra-think',
|
|
509
|
+
workflow: null, decision: summarizeDecision(decisionGate),
|
|
510
|
+
context_manifest_path: null, context_manifest_hash: null, git_head: currentHead(rootDir),
|
|
511
|
+
baseline: baseline ? {
|
|
512
|
+
id: baseline.id, mode: baseline.mode, status: baseline.status,
|
|
513
|
+
repository_revision: baseline.repository_revision,
|
|
514
|
+
} : null,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
291
517
|
if (baselineHealth.status !== 'pass') {
|
|
292
|
-
const
|
|
518
|
+
const activeWorkflow = actionableBaselineWorkflow(db, baseline?.id, rootDir);
|
|
519
|
+
const nextAction = activeWorkflow
|
|
520
|
+
? workflowNextAction(activeWorkflow)
|
|
521
|
+
: baseline
|
|
293
522
|
? `Complete ${baseline.mode} baseline adoption ${baseline.id} through ultra-init.`
|
|
294
523
|
: 'Initialize the project and start baseline adoption with ultra-init.';
|
|
295
524
|
return {
|
|
296
525
|
change_id: null, task_id: null, role: 'plan', gate: 'alignment', readiness: 'blocked',
|
|
297
526
|
blockers: baselineHealth.blockers, warnings: baselineHealth.warnings, next_action: nextAction,
|
|
298
|
-
recommended_workflow: 'ultra-init',
|
|
527
|
+
recommended_workflow: activeWorkflow ? `ultra-${activeWorkflow.kind}` : 'ultra-init',
|
|
528
|
+
workflow: summarizeWorkflow(activeWorkflow), decision: null, context_manifest_path: null,
|
|
299
529
|
context_manifest_hash: null, git_head: currentHead(rootDir),
|
|
300
530
|
baseline: baseline ? {
|
|
301
531
|
id: baseline.id, mode: baseline.mode, status: baseline.status,
|
|
@@ -306,6 +536,8 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
306
536
|
return {
|
|
307
537
|
change_id: null, task_id: null, role: 'plan', gate: 'alignment', readiness: 'ready',
|
|
308
538
|
next_action: 'Start daily work with ultra-change.', recommended_workflow: 'ultra-change',
|
|
539
|
+
workflow: null,
|
|
540
|
+
decision: null,
|
|
309
541
|
context_manifest_path: null, context_manifest_hash: null, git_head: currentHead(rootDir),
|
|
310
542
|
blockers: [], warnings: baselineHealth.warnings,
|
|
311
543
|
baseline: {
|
|
@@ -315,7 +547,8 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
315
547
|
},
|
|
316
548
|
};
|
|
317
549
|
}
|
|
318
|
-
const tasks = db.prepare('SELECT
|
|
550
|
+
const tasks = db.prepare('SELECT id FROM tasks WHERE change_id = ? ORDER BY created_at ASC')
|
|
551
|
+
.all(change.id).map((row) => ops.readTask(db, row.id));
|
|
319
552
|
const snapshot = db.prepare(
|
|
320
553
|
'SELECT * FROM context_snapshots WHERE change_id = ? ORDER BY created_at DESC, rowid DESC LIMIT 1',
|
|
321
554
|
).get(change.id);
|
|
@@ -323,23 +556,46 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
323
556
|
const taskId = snapshot?.task_id || tasks.find((row) => row.status === 'in_progress')?.id
|
|
324
557
|
|| tasks.find((row) => row.status === 'pending')?.id || null;
|
|
325
558
|
const task = tasks.find((row) => row.id === taskId) || null;
|
|
326
|
-
const
|
|
559
|
+
const checkout = baselines.gitWorktreeSnapshot(rootDir, ['.']);
|
|
560
|
+
const head = checkout.head;
|
|
327
561
|
const snapshotMissing = !snapshot;
|
|
328
562
|
const legacyContext = Boolean(
|
|
329
|
-
snapshot && (
|
|
563
|
+
snapshot && (
|
|
564
|
+
!context.resume || !context.context || !context.readiness || !('baseline' in context)
|
|
565
|
+
|| !Object.prototype.hasOwnProperty.call(context.resume, 'worktree_digest')
|
|
566
|
+
|| !Object.prototype.hasOwnProperty.call(context.resume, 'task_state_digest')
|
|
567
|
+
),
|
|
330
568
|
);
|
|
331
569
|
const headStale = Boolean(snapshot?.git_head && head && snapshot.git_head !== head);
|
|
570
|
+
const worktreeStale = Boolean(
|
|
571
|
+
context.resume && context.resume.worktree_digest && checkout.digest
|
|
572
|
+
&& context.resume.worktree_digest !== checkout.digest,
|
|
573
|
+
);
|
|
332
574
|
const stateChanged = context.resume?.task_status !== undefined
|
|
333
575
|
&& context.resume.task_status !== (task?.status || null);
|
|
576
|
+
const taskContractChanged = Boolean(
|
|
577
|
+
context.resume && context.resume.task_state_digest !== taskStateDigest(task),
|
|
578
|
+
);
|
|
334
579
|
const blockers = [];
|
|
335
|
-
const
|
|
580
|
+
const baselineGate = baselineGateForChange(db, change, baselineHealth);
|
|
581
|
+
const decisionGate = decisions.decisionGate(db, {
|
|
582
|
+
baseline_id: baselineHealth.baseline?.id || null,
|
|
583
|
+
change_id: change.id,
|
|
584
|
+
workflow_run_id: actionableChangeWorkflow(db, change.id, rootDir)?.id || null,
|
|
585
|
+
}, { rootDir });
|
|
586
|
+
const warnings = [...baselineGate.warnings];
|
|
587
|
+
blockers.push(...baselineGate.blockers);
|
|
588
|
+
blockers.push(...decisionGate.blockers);
|
|
336
589
|
if (snapshotMissing) blockers.push('CONTEXT_NOT_COMPILED');
|
|
337
590
|
else {
|
|
338
591
|
if (legacyContext) blockers.push('CONTEXT_SNAPSHOT_UPGRADE_REQUIRED');
|
|
339
592
|
if (headStale) blockers.push('CONTEXT_HEAD_STALE');
|
|
593
|
+
if (worktreeStale) blockers.push('CONTEXT_WORKTREE_STALE');
|
|
340
594
|
if (stateChanged) blockers.push('CONTEXT_TASK_STATE_STALE');
|
|
595
|
+
if (taskContractChanged) blockers.push('CONTEXT_TASK_CONTRACT_STALE');
|
|
341
596
|
for (const blocker of parseJson(snapshot.blockers_json, [])) {
|
|
342
|
-
if (ADVISORY_CONTEXT_CODES.has(blocker)
|
|
597
|
+
if (ADVISORY_CONTEXT_CODES.has(blocker)
|
|
598
|
+
|| (blocker.startsWith('BASELINE_') && baselineGate.blockers.length === 0)) {
|
|
343
599
|
if (!warnings.includes(blocker)) warnings.push(blocker);
|
|
344
600
|
} else if (!blockers.includes(blocker)) blockers.push(blocker);
|
|
345
601
|
}
|
|
@@ -348,20 +604,35 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
348
604
|
}
|
|
349
605
|
}
|
|
350
606
|
const readiness = blockers.length > 0 ? 'blocked' : snapshot.readiness;
|
|
607
|
+
const activeWorkflow = actionableChangeWorkflow(db, change.id, rootDir);
|
|
351
608
|
let nextAction;
|
|
352
|
-
if (
|
|
609
|
+
if (baselineGate.blockers.length > 0) {
|
|
610
|
+
nextAction = deriveNextAction({
|
|
611
|
+
change, tasks, task, role: snapshot?.role || 'plan', readiness: 'blocked',
|
|
612
|
+
blockers: baselineGate.blockers,
|
|
613
|
+
});
|
|
614
|
+
} else if (!decisionGate.ready) {
|
|
615
|
+
nextAction = decisionNextAction(decisionGate);
|
|
616
|
+
} else if (activeWorkflow?.status === 'blocked') {
|
|
617
|
+
nextAction = workflowNextAction(activeWorkflow);
|
|
618
|
+
} else if (snapshotMissing) {
|
|
353
619
|
nextAction = `Compile change.context for change ${change.id} before continuing.`;
|
|
354
620
|
} else if (legacyContext) {
|
|
355
621
|
nextAction = 'Recompile change.context to upgrade this legacy context snapshot.';
|
|
356
622
|
} else if (headStale) {
|
|
357
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.';
|
|
358
626
|
} else if (stateChanged) {
|
|
359
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);
|
|
360
632
|
} else {
|
|
361
633
|
nextAction = deriveNextAction({
|
|
362
634
|
change, tasks, task, role: snapshot?.role || 'plan', readiness,
|
|
363
635
|
blockers,
|
|
364
|
-
explicit: snapshot?.next_action,
|
|
365
636
|
});
|
|
366
637
|
}
|
|
367
638
|
return {
|
|
@@ -371,12 +642,18 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
371
642
|
task_status: task?.status || null,
|
|
372
643
|
session_id: task?.session_id || null,
|
|
373
644
|
role: snapshot?.role || 'plan',
|
|
374
|
-
gate: snapshot?.gate || 'alignment',
|
|
645
|
+
gate: decisionGate.ready ? (snapshot?.gate || 'alignment') : 'alignment',
|
|
375
646
|
readiness,
|
|
376
647
|
blockers,
|
|
377
648
|
warnings: [...new Set(warnings)],
|
|
378
649
|
next_action: nextAction,
|
|
379
|
-
recommended_workflow:
|
|
650
|
+
recommended_workflow: baselineGate.blockers.length > 0
|
|
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),
|
|
380
657
|
context_manifest_path: snapshot?.manifest_path || null,
|
|
381
658
|
context_manifest_hash: snapshot?.manifest_hash || null,
|
|
382
659
|
git_head: head,
|
|
@@ -385,11 +662,14 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
385
662
|
status: baselineHealth.baseline.status,
|
|
386
663
|
repository_revision: baselineHealth.baseline.repository_revision,
|
|
387
664
|
} : null,
|
|
665
|
+
workflow: summarizeWorkflow(activeWorkflow),
|
|
666
|
+
decision: summarizeDecision(decisionGate),
|
|
388
667
|
};
|
|
389
668
|
}
|
|
390
669
|
|
|
391
670
|
module.exports = {
|
|
392
671
|
ContextSpineError,
|
|
672
|
+
baselineGateForChange,
|
|
393
673
|
compileRoleContext,
|
|
394
674
|
deriveNextAction,
|
|
395
675
|
readBreadcrumb,
|