ultra-builder-pro-cli 0.13.0 → 0.15.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 +86 -1
- package/README.md +40 -26
- package/adapters/_shared/codex-assets.cjs +29 -165
- package/adapters/_shared/file-ops.cjs +6 -2
- package/adapters/_shared/runtime-assets.cjs +1 -0
- package/adapters/claude.js +3 -2
- package/adapters/codex.js +1 -0
- package/adapters/kimi.js +3 -2
- package/adapters/opencode.js +33 -122
- package/agents/review-coordinator.md +9 -6
- package/docs/AGENT-CONTEXT.md +21 -11
- package/docs/ARCHITECTURE.md +41 -13
- package/docs/PLAN.zh-CN.md +3 -2
- package/docs/ROADMAP.md +9 -5
- package/docs/RUNTIME-COMPAT-MATRIX.md +6 -6
- package/docs/STATE-DB-ACCESS-POLICY.md +27 -25
- package/docs/WORKFLOW-LIFECYCLE.md +266 -0
- package/hooks/README.md +6 -1
- package/hooks/active_task_context.py +22 -50
- package/hooks/context_spine.py +61 -332
- package/hooks/health_check.py +29 -18
- package/hooks/pre_stop_check.py +28 -33
- package/hooks/subagent_tracker.py +54 -85
- package/hooks/workflow_checkpoint.py +39 -47
- package/hooks/workflow_context.py +4 -9
- package/hooks/workflow_resume.py +48 -128
- package/mcp-server/breadcrumb.cjs +22 -0
- 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 +550 -85
- package/mcp-server/lib/context-spine.cjs +242 -24
- package/mcp-server/lib/doctor.cjs +33 -3
- package/mcp-server/lib/init-project.cjs +210 -16
- package/mcp-server/lib/plan-store.cjs +4 -1
- package/mcp-server/lib/prd-parser.cjs +14 -41
- package/mcp-server/lib/pricing.cjs +7 -10
- package/mcp-server/lib/project-breadcrumb.cjs +145 -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 +145 -8
- package/mcp-server/lib/state-ops.cjs +160 -9
- package/mcp-server/lib/task-expander.cjs +9 -38
- package/mcp-server/lib/workflow-state.cjs +2292 -0
- package/mcp-server/server.cjs +182 -50
- package/orchestrator/daemon.cjs +1 -2
- package/orchestrator/dispatch-rules.cjs +5 -31
- package/orchestrator/planner/plan-builder.cjs +21 -12
- package/package.json +14 -6
- package/skills/cc-collab/SKILL.md +2 -2
- package/skills/codex-collab/SKILL.md +2 -3
- package/skills/ultra-change/SKILL.md +53 -68
- package/skills/ultra-change/references/change-contract.md +36 -0
- package/skills/ultra-deliver/SKILL.md +69 -79
- package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
- package/skills/ultra-dev/SKILL.md +54 -59
- package/skills/ultra-doctor/SKILL.md +40 -71
- package/skills/ultra-init/SKILL.md +96 -103
- package/skills/ultra-init/references/brownfield-adoption.md +2 -2
- package/skills/ultra-plan/SKILL.md +79 -75
- package/skills/ultra-plan/references/semantic-preflight.md +18 -0
- package/skills/ultra-research/SKILL.md +87 -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 +57 -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 -45
- package/skills/ultra-test/SKILL.md +48 -55
- package/skills/ultra-verify/SKILL.md +22 -31
- package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
- package/skills/ultra-verify/scripts/verify_wait.py +54 -54
- package/spec/cli-protocol.md +7 -2
- package/spec/mcp-tools.yaml +422 -73
- 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 +82 -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 +213 -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 -131
- 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 -137
- package/adapters/tests/codex-hook.test.cjs +0 -115
- package/adapters/tests/codex.test.cjs +0 -423
- package/adapters/tests/kimi-hook.test.cjs +0 -122
- package/adapters/tests/kimi.test.cjs +0 -259
- package/adapters/tests/opencode.test.cjs +0 -371
- 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/llm-client.cjs +0 -157
- package/mcp-server/lib/llm-client.test.cjs +0 -156
- package/mcp-server/lib/plan-store.test.cjs +0 -132
- package/mcp-server/lib/prd-parser.test.cjs +0 -171
- 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 -324
- 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 -1471
- 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/expand-prompt.cjs +0 -62
- package/orchestrator/planner/expand-prompt.test.cjs +0 -52
- package/orchestrator/planner/plan-builder.test.cjs +0 -165
- package/orchestrator/planner/prd-prompt.cjs +0 -44
- package/orchestrator/planner/prd-prompt.test.cjs +0 -30
- 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,8 @@ 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 ops = require('./state-ops.cjs');
|
|
8
10
|
|
|
9
11
|
const ROLES = new Set(['plan', 'implement', 'check', 'review']);
|
|
10
12
|
const GATES = new Set([
|
|
@@ -19,6 +21,15 @@ const ADVISORY_CONTEXT_CODES = new Set([
|
|
|
19
21
|
'EXECUTION_CONTEXT_BUDGET_EXCEEDED',
|
|
20
22
|
'EXECUTION_CONTEXT_BUDGET_ADVISORY',
|
|
21
23
|
]);
|
|
24
|
+
const ACTIVE_CHANGE_BASELINE_DRIFT = Object.freeze([
|
|
25
|
+
/^BASELINE_HEAD_STALE$/,
|
|
26
|
+
/^BASELINE_BRANCH_STALE$/,
|
|
27
|
+
/^BASELINE_WORKSPACE_STALE$/,
|
|
28
|
+
/^BASELINE_WORKTREE_STALE$/,
|
|
29
|
+
/^BASELINE_WORKTREE_DIRTY$/,
|
|
30
|
+
/^BASELINE_SPEC_STALE:/,
|
|
31
|
+
/^BASELINE_EVIDENCE_STALE:/,
|
|
32
|
+
]);
|
|
22
33
|
|
|
23
34
|
class ContextSpineError extends Error {
|
|
24
35
|
constructor(code, message) {
|
|
@@ -40,6 +51,82 @@ function parseJson(value, fallback = {}) {
|
|
|
40
51
|
try { return JSON.parse(value); } catch { return fallback; }
|
|
41
52
|
}
|
|
42
53
|
|
|
54
|
+
function hasIncidentBreakGlass(change) {
|
|
55
|
+
const bypass = change?.baseline_bypass
|
|
56
|
+
|| parseJson(change?.baseline_bypass_json, null);
|
|
57
|
+
return change?.kind === 'incident'
|
|
58
|
+
&& bypass?.mode === 'incident_break_glass'
|
|
59
|
+
&& Boolean(String(bypass.reason || '').trim())
|
|
60
|
+
&& Boolean(String(bypass.approved_by || '').trim());
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function expectedActiveChangeDrift(code) {
|
|
64
|
+
return ACTIVE_CHANGE_BASELINE_DRIFT.some((pattern) => pattern.test(code));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function baselineGateForChange(db, change, baselineHealth) {
|
|
68
|
+
const baseWarnings = Array.isArray(baselineHealth.warnings) ? baselineHealth.warnings : [];
|
|
69
|
+
if (baselineHealth.status === 'pass') {
|
|
70
|
+
return { blockers: [], warnings: [...new Set(baseWarnings)], mode: 'healthy' };
|
|
71
|
+
}
|
|
72
|
+
const healthBlockers = Array.isArray(baselineHealth.blockers)
|
|
73
|
+
? baselineHealth.blockers : ['BASELINE_STATE_UNAVAILABLE'];
|
|
74
|
+
if (hasIncidentBreakGlass(change)) {
|
|
75
|
+
return {
|
|
76
|
+
blockers: [], warnings: [...new Set([...healthBlockers, ...baseWarnings])],
|
|
77
|
+
mode: 'incident_break_glass',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const baseline = baselineHealth.baseline;
|
|
81
|
+
if (!baseline || baseline.status !== 'ready' || baseline.mode === 'migrated') {
|
|
82
|
+
return { blockers: healthBlockers, warnings: baseWarnings, mode: 'baseline_required' };
|
|
83
|
+
}
|
|
84
|
+
const binding = db.prepare(
|
|
85
|
+
`SELECT wr.baseline_id, ws.status AS binding_status, ws.evidence_json
|
|
86
|
+
FROM workflow_runs wr
|
|
87
|
+
LEFT JOIN workflow_steps ws
|
|
88
|
+
ON ws.run_id = wr.id AND ws.step_id = 'bind-baseline'
|
|
89
|
+
WHERE wr.kind = 'change' AND wr.change_id = ?
|
|
90
|
+
ORDER BY wr.started_at DESC, wr.rowid DESC LIMIT 1`,
|
|
91
|
+
).get(change.id);
|
|
92
|
+
let evidence = [];
|
|
93
|
+
try { evidence = JSON.parse(binding?.evidence_json || '[]'); } catch { evidence = []; }
|
|
94
|
+
const boundFromReadyAuthority = binding?.baseline_id === baseline.id
|
|
95
|
+
&& binding.binding_status === 'completed'
|
|
96
|
+
&& evidence.some((item) => item?.ref === baseline.id);
|
|
97
|
+
if (!boundFromReadyAuthority) {
|
|
98
|
+
return { blockers: healthBlockers, warnings: baseWarnings, mode: 'baseline_binding_required' };
|
|
99
|
+
}
|
|
100
|
+
const blockers = healthBlockers.filter((code) => !expectedActiveChangeDrift(code));
|
|
101
|
+
const driftWarnings = healthBlockers.filter(expectedActiveChangeDrift);
|
|
102
|
+
return {
|
|
103
|
+
blockers,
|
|
104
|
+
warnings: [...new Set([...driftWarnings, ...baseWarnings])],
|
|
105
|
+
mode: blockers.length === 0 ? 'active_change_drift' : 'baseline_invalid',
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function taskStateDigest(task) {
|
|
110
|
+
if (!task) return null;
|
|
111
|
+
const payload = {
|
|
112
|
+
id: task.id,
|
|
113
|
+
change_id: task.change_id,
|
|
114
|
+
status: task.status,
|
|
115
|
+
stale: Boolean(task.stale),
|
|
116
|
+
deps: task.deps || [],
|
|
117
|
+
outcome: task.outcome,
|
|
118
|
+
slice_kind: task.slice_kind,
|
|
119
|
+
public_seam: task.public_seam,
|
|
120
|
+
verification_command: task.verification_command,
|
|
121
|
+
acceptance: task.acceptance || [],
|
|
122
|
+
context_refs: task.context_refs || [],
|
|
123
|
+
docs_impact: task.docs_impact || {},
|
|
124
|
+
ownership: task.ownership || {},
|
|
125
|
+
trace_to: task.trace_to,
|
|
126
|
+
};
|
|
127
|
+
return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex');
|
|
128
|
+
}
|
|
129
|
+
|
|
43
130
|
function nonEmpty(value, field) {
|
|
44
131
|
const text = typeof value === 'string' ? value.trim() : '';
|
|
45
132
|
if (!text) throw new ContextSpineError('VALIDATION_ERROR', `${field} must be a non-empty string`);
|
|
@@ -111,6 +198,27 @@ function normalizeRefs(input, role, inherited) {
|
|
|
111
198
|
});
|
|
112
199
|
}
|
|
113
200
|
|
|
201
|
+
function authoritativeTaskRefs(input, role, task) {
|
|
202
|
+
if (!task) return null;
|
|
203
|
+
const dbRefs = normalizeRefs({ context_refs: task.context_refs || [] }, role, []);
|
|
204
|
+
if (input.context_refs === undefined && input.spec_refs === undefined) return dbRefs;
|
|
205
|
+
const supplied = normalizeRefs(input, role, []);
|
|
206
|
+
const comparable = (items) => items.map((item) => ({
|
|
207
|
+
ref: item.ref,
|
|
208
|
+
kind: item.kind,
|
|
209
|
+
reason: item.reason,
|
|
210
|
+
required: item.required,
|
|
211
|
+
expected_digest: item.expected_digest,
|
|
212
|
+
}));
|
|
213
|
+
if (JSON.stringify(comparable(supplied)) !== JSON.stringify(comparable(dbRefs))) {
|
|
214
|
+
throw new ContextSpineError(
|
|
215
|
+
'EXECUTION_CONTEXT_REFS_CONFLICT',
|
|
216
|
+
`context_refs conflict with task ${task.id} authority; update the task contract first`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
return dbRefs;
|
|
220
|
+
}
|
|
221
|
+
|
|
114
222
|
function inspectRef(rootDir, role, value) {
|
|
115
223
|
if (value.kind === 'external' || /^[a-z][a-z0-9+.-]*:\/\//i.test(value.ref)) {
|
|
116
224
|
return { ...value, role, status: 'external', digest: value.expected_digest, estimated_tokens: 0 };
|
|
@@ -128,9 +236,35 @@ function inspectRef(rootDir, role, value) {
|
|
|
128
236
|
};
|
|
129
237
|
}
|
|
130
238
|
|
|
131
|
-
function normalizeExecutionContract(input, inherited, {
|
|
132
|
-
const
|
|
133
|
-
|
|
239
|
+
function normalizeExecutionContract(input, inherited, { task }) {
|
|
240
|
+
const taskContract = task
|
|
241
|
+
&& task.slice_kind && task.public_seam && task.verification_command ? {
|
|
242
|
+
slice_kind: task.slice_kind,
|
|
243
|
+
public_seam: task.public_seam,
|
|
244
|
+
verification_command: task.verification_command,
|
|
245
|
+
context_budget_percent: input.execution_contract?.context_budget_percent
|
|
246
|
+
?? inherited?.context_budget_percent
|
|
247
|
+
?? 40,
|
|
248
|
+
} : null;
|
|
249
|
+
if (input.execution_contract) {
|
|
250
|
+
for (const field of ['slice_kind', 'public_seam', 'verification_command']) {
|
|
251
|
+
if (input.execution_contract[field] !== undefined) {
|
|
252
|
+
throw new ContextSpineError(
|
|
253
|
+
'EXECUTION_CONTRACT_CONFLICT',
|
|
254
|
+
`execution_contract.${field} is owned by the task row and cannot be supplied by Prompt input`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (!task && input.execution_contract !== undefined) {
|
|
260
|
+
throw new ContextSpineError(
|
|
261
|
+
'EXECUTION_CONTRACT_CONFLICT',
|
|
262
|
+
'execution_contract is valid only for a DB-backed task context',
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
const source = taskContract || input.execution_contract || inherited || null;
|
|
266
|
+
const taskId = task?.id || null;
|
|
267
|
+
if (!taskId) return null;
|
|
134
268
|
if (!source || typeof source !== 'object' || Array.isArray(source)) return null;
|
|
135
269
|
const sliceKind = source.slice_kind || 'tracer_bullet';
|
|
136
270
|
if (!SLICE_KINDS.has(sliceKind)) {
|
|
@@ -156,14 +290,13 @@ function defaultGate(role) {
|
|
|
156
290
|
}[role];
|
|
157
291
|
}
|
|
158
292
|
|
|
159
|
-
function deriveNextAction({ change, tasks, task, role, readiness, blockers = []
|
|
293
|
+
function deriveNextAction({ change, tasks, task, role, readiness, blockers = [] }) {
|
|
160
294
|
if (readiness === 'blocked') {
|
|
161
295
|
if (blockers.some((blocker) => blocker.startsWith('BASELINE_'))) {
|
|
162
296
|
return 'Complete or refresh the Ultra project baseline, then recompile change.context.';
|
|
163
297
|
}
|
|
164
298
|
return 'Resolve the context readiness blockers, then recompile change.context.';
|
|
165
299
|
}
|
|
166
|
-
if (explicit && String(explicit).trim()) return String(explicit).trim();
|
|
167
300
|
if (change.status === 'ready') return `Run ultra-deliver for change ${change.id}.`;
|
|
168
301
|
if (change.status === 'blocked') return `Resolve the blockers for change ${change.id}, then recompile context.`;
|
|
169
302
|
if (!task && tasks.length === 0) return `Create the first fresh-context tracer-bullet task for change ${change.id}.`;
|
|
@@ -192,6 +325,12 @@ function latestContext(db, changeId, taskId) {
|
|
|
192
325
|
}
|
|
193
326
|
|
|
194
327
|
function compileRoleContext(db, { input, change, tasks, rootDir }) {
|
|
328
|
+
if (input.next_action !== undefined) {
|
|
329
|
+
throw new ContextSpineError(
|
|
330
|
+
'CONTEXT_NEXT_ACTION_AUTHORITY_VIOLATION',
|
|
331
|
+
'next_action is derived from authoritative workflow and task state',
|
|
332
|
+
);
|
|
333
|
+
}
|
|
195
334
|
const task = input.task_id ? tasks.find((row) => row.id === input.task_id) : null;
|
|
196
335
|
if (input.task_id && !task) {
|
|
197
336
|
throw new ContextSpineError('TASK_NOT_FOUND', `task ${input.task_id} is not linked to change ${change.id}`);
|
|
@@ -202,22 +341,22 @@ function compileRoleContext(db, { input, change, tasks, rootDir }) {
|
|
|
202
341
|
const gate = input.gate || defaultGate(role);
|
|
203
342
|
if (!GATES.has(gate)) throw new ContextSpineError('VALIDATION_ERROR', `unsupported workflow gate: ${gate}`);
|
|
204
343
|
|
|
205
|
-
const refs =
|
|
344
|
+
const refs = authoritativeTaskRefs(input, role, task)
|
|
345
|
+
|| normalizeRefs(input, role, inherited.context?.items || []);
|
|
206
346
|
const items = refs.map((value) => inspectRef(rootDir, role, value));
|
|
207
347
|
const budget = normalizeBudget(input.budget, inherited.context?.budget);
|
|
208
348
|
const tokenEstimate = items.reduce((sum, item) => sum + item.estimated_tokens, 0);
|
|
209
349
|
const fileCount = items.filter((item) => item.status !== 'external').length;
|
|
210
350
|
const executionContract = normalizeExecutionContract(
|
|
211
|
-
input, inherited.execution_contract, {
|
|
351
|
+
input, inherited.execution_contract, { task },
|
|
212
352
|
);
|
|
213
353
|
const blockers = [];
|
|
214
354
|
const warnings = [];
|
|
215
355
|
const baselineHealth = baselines.inspectBaseline(db, { rootDir });
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
warnings.push(...baselineHealth.blockers, ...baselineHealth.warnings);
|
|
356
|
+
const checkout = baselines.gitWorktreeSnapshot(rootDir, ['.']);
|
|
357
|
+
const baselineGate = baselineGateForChange(db, change, baselineHealth);
|
|
358
|
+
blockers.push(...baselineGate.blockers);
|
|
359
|
+
warnings.push(...baselineGate.warnings);
|
|
221
360
|
for (const item of items) {
|
|
222
361
|
if (item.required && item.status === 'missing') blockers.push(`CONTEXT_REQUIRED_REF_MISSING:${item.ref}`);
|
|
223
362
|
if (item.required && item.status === 'stale') blockers.push(`CONTEXT_REQUIRED_REF_STALE:${item.ref}`);
|
|
@@ -237,7 +376,7 @@ function compileRoleContext(db, { input, change, tasks, rootDir }) {
|
|
|
237
376
|
}
|
|
238
377
|
const readiness = blockers.length === 0 ? 'ready' : 'blocked';
|
|
239
378
|
const nextAction = deriveNextAction({
|
|
240
|
-
change, tasks, task, role, readiness, blockers,
|
|
379
|
+
change, tasks, task, role, readiness, blockers,
|
|
241
380
|
});
|
|
242
381
|
return {
|
|
243
382
|
role,
|
|
@@ -259,10 +398,13 @@ function compileRoleContext(db, { input, change, tasks, rootDir }) {
|
|
|
259
398
|
change_id: change.id,
|
|
260
399
|
task_id: task?.id || null,
|
|
261
400
|
task_status: task?.status || null,
|
|
401
|
+
task_state_digest: taskStateDigest(task),
|
|
262
402
|
session_id: task?.session_id || null,
|
|
263
403
|
role,
|
|
264
404
|
gate,
|
|
265
|
-
git_head:
|
|
405
|
+
git_head: checkout.head,
|
|
406
|
+
worktree_state: checkout.state,
|
|
407
|
+
worktree_digest: checkout.digest,
|
|
266
408
|
},
|
|
267
409
|
};
|
|
268
410
|
}
|
|
@@ -276,6 +418,40 @@ function recommendedWorkflow(change, task, tasks, readiness) {
|
|
|
276
418
|
return 'ultra-change';
|
|
277
419
|
}
|
|
278
420
|
|
|
421
|
+
function actionableBaselineWorkflow(db, baselineId, rootDir) {
|
|
422
|
+
if (!baselineId) return null;
|
|
423
|
+
const candidates = workflows.listWorkflows(db, { baseline_id: baselineId, limit: 100 }, { rootDir })
|
|
424
|
+
.filter((run) => ['active', 'blocked', 'ready'].includes(run.status));
|
|
425
|
+
return candidates.find((run) => run.kind === 'research')
|
|
426
|
+
|| candidates.find((run) => run.kind === 'init')
|
|
427
|
+
|| candidates[0]
|
|
428
|
+
|| null;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function actionableChangeWorkflow(db, changeId, rootDir) {
|
|
432
|
+
const candidates = workflows.listWorkflows(db, { change_id: changeId, limit: 100 }, { rootDir })
|
|
433
|
+
.filter((run) => ['active', 'blocked', 'ready'].includes(run.status));
|
|
434
|
+
return candidates.find((run) => run.status === 'blocked') || candidates[0] || null;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function summarizeWorkflow(run) {
|
|
438
|
+
if (!run) return null;
|
|
439
|
+
return {
|
|
440
|
+
id: run.id, kind: run.kind, mode: run.mode, status: run.status,
|
|
441
|
+
current_step: run.current_step, blockers: run.blockers,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function workflowNextAction(run) {
|
|
446
|
+
if (run.status === 'blocked') {
|
|
447
|
+
return `Resolve ${run.kind} workflow blockers at ${run.current_step}: ${run.blockers.join(', ')}.`;
|
|
448
|
+
}
|
|
449
|
+
if (run.status === 'ready') {
|
|
450
|
+
return `Finalize ${run.kind} workflow ${run.id} after verifying its recorded evidence and outputs.`;
|
|
451
|
+
}
|
|
452
|
+
return `Continue ${run.kind} workflow ${run.id} at ${run.current_step}.`;
|
|
453
|
+
}
|
|
454
|
+
|
|
279
455
|
function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
280
456
|
const baselineHealth = baselines.inspectBaseline(db, { rootDir });
|
|
281
457
|
const change = id
|
|
@@ -289,13 +465,17 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
289
465
|
if (!change) {
|
|
290
466
|
const baseline = baselineHealth.baseline;
|
|
291
467
|
if (baselineHealth.status !== 'pass') {
|
|
292
|
-
const
|
|
468
|
+
const activeWorkflow = actionableBaselineWorkflow(db, baseline?.id, rootDir);
|
|
469
|
+
const nextAction = activeWorkflow
|
|
470
|
+
? workflowNextAction(activeWorkflow)
|
|
471
|
+
: baseline
|
|
293
472
|
? `Complete ${baseline.mode} baseline adoption ${baseline.id} through ultra-init.`
|
|
294
473
|
: 'Initialize the project and start baseline adoption with ultra-init.';
|
|
295
474
|
return {
|
|
296
475
|
change_id: null, task_id: null, role: 'plan', gate: 'alignment', readiness: 'blocked',
|
|
297
476
|
blockers: baselineHealth.blockers, warnings: baselineHealth.warnings, next_action: nextAction,
|
|
298
|
-
recommended_workflow: 'ultra-init',
|
|
477
|
+
recommended_workflow: activeWorkflow ? `ultra-${activeWorkflow.kind}` : 'ultra-init',
|
|
478
|
+
workflow: summarizeWorkflow(activeWorkflow), context_manifest_path: null,
|
|
299
479
|
context_manifest_hash: null, git_head: currentHead(rootDir),
|
|
300
480
|
baseline: baseline ? {
|
|
301
481
|
id: baseline.id, mode: baseline.mode, status: baseline.status,
|
|
@@ -306,6 +486,7 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
306
486
|
return {
|
|
307
487
|
change_id: null, task_id: null, role: 'plan', gate: 'alignment', readiness: 'ready',
|
|
308
488
|
next_action: 'Start daily work with ultra-change.', recommended_workflow: 'ultra-change',
|
|
489
|
+
workflow: null,
|
|
309
490
|
context_manifest_path: null, context_manifest_hash: null, git_head: currentHead(rootDir),
|
|
310
491
|
blockers: [], warnings: baselineHealth.warnings,
|
|
311
492
|
baseline: {
|
|
@@ -315,7 +496,8 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
315
496
|
},
|
|
316
497
|
};
|
|
317
498
|
}
|
|
318
|
-
const tasks = db.prepare('SELECT
|
|
499
|
+
const tasks = db.prepare('SELECT id FROM tasks WHERE change_id = ? ORDER BY created_at ASC')
|
|
500
|
+
.all(change.id).map((row) => ops.readTask(db, row.id));
|
|
319
501
|
const snapshot = db.prepare(
|
|
320
502
|
'SELECT * FROM context_snapshots WHERE change_id = ? ORDER BY created_at DESC, rowid DESC LIMIT 1',
|
|
321
503
|
).get(change.id);
|
|
@@ -323,23 +505,40 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
323
505
|
const taskId = snapshot?.task_id || tasks.find((row) => row.status === 'in_progress')?.id
|
|
324
506
|
|| tasks.find((row) => row.status === 'pending')?.id || null;
|
|
325
507
|
const task = tasks.find((row) => row.id === taskId) || null;
|
|
326
|
-
const
|
|
508
|
+
const checkout = baselines.gitWorktreeSnapshot(rootDir, ['.']);
|
|
509
|
+
const head = checkout.head;
|
|
327
510
|
const snapshotMissing = !snapshot;
|
|
328
511
|
const legacyContext = Boolean(
|
|
329
|
-
snapshot && (
|
|
512
|
+
snapshot && (
|
|
513
|
+
!context.resume || !context.context || !context.readiness || !('baseline' in context)
|
|
514
|
+
|| !Object.prototype.hasOwnProperty.call(context.resume, 'worktree_digest')
|
|
515
|
+
|| !Object.prototype.hasOwnProperty.call(context.resume, 'task_state_digest')
|
|
516
|
+
),
|
|
330
517
|
);
|
|
331
518
|
const headStale = Boolean(snapshot?.git_head && head && snapshot.git_head !== head);
|
|
519
|
+
const worktreeStale = Boolean(
|
|
520
|
+
context.resume && context.resume.worktree_digest && checkout.digest
|
|
521
|
+
&& context.resume.worktree_digest !== checkout.digest,
|
|
522
|
+
);
|
|
332
523
|
const stateChanged = context.resume?.task_status !== undefined
|
|
333
524
|
&& context.resume.task_status !== (task?.status || null);
|
|
525
|
+
const taskContractChanged = Boolean(
|
|
526
|
+
context.resume && context.resume.task_state_digest !== taskStateDigest(task),
|
|
527
|
+
);
|
|
334
528
|
const blockers = [];
|
|
335
|
-
const
|
|
529
|
+
const baselineGate = baselineGateForChange(db, change, baselineHealth);
|
|
530
|
+
const warnings = [...baselineGate.warnings];
|
|
531
|
+
blockers.push(...baselineGate.blockers);
|
|
336
532
|
if (snapshotMissing) blockers.push('CONTEXT_NOT_COMPILED');
|
|
337
533
|
else {
|
|
338
534
|
if (legacyContext) blockers.push('CONTEXT_SNAPSHOT_UPGRADE_REQUIRED');
|
|
339
535
|
if (headStale) blockers.push('CONTEXT_HEAD_STALE');
|
|
536
|
+
if (worktreeStale) blockers.push('CONTEXT_WORKTREE_STALE');
|
|
340
537
|
if (stateChanged) blockers.push('CONTEXT_TASK_STATE_STALE');
|
|
538
|
+
if (taskContractChanged) blockers.push('CONTEXT_TASK_CONTRACT_STALE');
|
|
341
539
|
for (const blocker of parseJson(snapshot.blockers_json, [])) {
|
|
342
|
-
if (ADVISORY_CONTEXT_CODES.has(blocker)
|
|
540
|
+
if (ADVISORY_CONTEXT_CODES.has(blocker)
|
|
541
|
+
|| (blocker.startsWith('BASELINE_') && baselineGate.blockers.length === 0)) {
|
|
343
542
|
if (!warnings.includes(blocker)) warnings.push(blocker);
|
|
344
543
|
} else if (!blockers.includes(blocker)) blockers.push(blocker);
|
|
345
544
|
}
|
|
@@ -348,20 +547,33 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
348
547
|
}
|
|
349
548
|
}
|
|
350
549
|
const readiness = blockers.length > 0 ? 'blocked' : snapshot.readiness;
|
|
550
|
+
const activeWorkflow = actionableChangeWorkflow(db, change.id, rootDir);
|
|
351
551
|
let nextAction;
|
|
352
|
-
if (
|
|
552
|
+
if (baselineGate.blockers.length > 0) {
|
|
553
|
+
nextAction = deriveNextAction({
|
|
554
|
+
change, tasks, task, role: snapshot?.role || 'plan', readiness: 'blocked',
|
|
555
|
+
blockers: baselineGate.blockers,
|
|
556
|
+
});
|
|
557
|
+
} else if (activeWorkflow?.status === 'blocked') {
|
|
558
|
+
nextAction = workflowNextAction(activeWorkflow);
|
|
559
|
+
} else if (snapshotMissing) {
|
|
353
560
|
nextAction = `Compile change.context for change ${change.id} before continuing.`;
|
|
354
561
|
} else if (legacyContext) {
|
|
355
562
|
nextAction = 'Recompile change.context to upgrade this legacy context snapshot.';
|
|
356
563
|
} else if (headStale) {
|
|
357
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.';
|
|
358
567
|
} else if (stateChanged) {
|
|
359
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);
|
|
360
573
|
} else {
|
|
361
574
|
nextAction = deriveNextAction({
|
|
362
575
|
change, tasks, task, role: snapshot?.role || 'plan', readiness,
|
|
363
576
|
blockers,
|
|
364
|
-
explicit: snapshot?.next_action,
|
|
365
577
|
});
|
|
366
578
|
}
|
|
367
579
|
return {
|
|
@@ -376,7 +588,11 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
376
588
|
blockers,
|
|
377
589
|
warnings: [...new Set(warnings)],
|
|
378
590
|
next_action: nextAction,
|
|
379
|
-
recommended_workflow:
|
|
591
|
+
recommended_workflow: baselineGate.blockers.length > 0
|
|
592
|
+
? (baselineHealth.baseline?.status === 'ready' ? 'ultra-doctor' : 'ultra-init')
|
|
593
|
+
: activeWorkflow
|
|
594
|
+
? `ultra-${activeWorkflow.kind}`
|
|
595
|
+
: recommendedWorkflow(change, task, tasks, readiness),
|
|
380
596
|
context_manifest_path: snapshot?.manifest_path || null,
|
|
381
597
|
context_manifest_hash: snapshot?.manifest_hash || null,
|
|
382
598
|
git_head: head,
|
|
@@ -385,11 +601,13 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
|
|
|
385
601
|
status: baselineHealth.baseline.status,
|
|
386
602
|
repository_revision: baselineHealth.baseline.repository_revision,
|
|
387
603
|
} : null,
|
|
604
|
+
workflow: summarizeWorkflow(activeWorkflow),
|
|
388
605
|
};
|
|
389
606
|
}
|
|
390
607
|
|
|
391
608
|
module.exports = {
|
|
392
609
|
ContextSpineError,
|
|
610
|
+
baselineGateForChange,
|
|
393
611
|
compileRoleContext,
|
|
394
612
|
deriveNextAction,
|
|
395
613
|
readBreadcrumb,
|
|
@@ -11,6 +11,7 @@ const recovery = require('../../orchestrator/recovery.cjs');
|
|
|
11
11
|
const baselines = require('./baseline-workflow.cjs');
|
|
12
12
|
const changes = require('./change-workflow.cjs');
|
|
13
13
|
const archiveJournal = require('./archive-journal.cjs');
|
|
14
|
+
const workflows = require('./workflow-state.cjs');
|
|
14
15
|
|
|
15
16
|
const SPEC_CONSUMER = 'spec-staleness';
|
|
16
17
|
|
|
@@ -56,10 +57,20 @@ function inspectSystem(db, { rootDir = process.cwd() } = {}) {
|
|
|
56
57
|
const baseline = missing.length === 0
|
|
57
58
|
? baselines.inspectBaseline(db, { rootDir })
|
|
58
59
|
: { status: 'fail', blockers: ['BASELINE_STATE_UNAVAILABLE'], warnings: [], baseline: null };
|
|
60
|
+
const workflowHealth = missing.length === 0
|
|
61
|
+
? workflows.inspectWorkflowHealth(db, { rootDir })
|
|
62
|
+
: {
|
|
63
|
+
status: 'fail', active: 0, blocked: 0, ready: 0, stale_outputs: [],
|
|
64
|
+
historical_stale_outputs: [], terminal_authority_runs: [], untracked_active_changes: [],
|
|
65
|
+
};
|
|
66
|
+
const baselineCheckStatus = baseline.status === 'pass'
|
|
67
|
+
? 'pass'
|
|
68
|
+
: (baseline.baseline?.status === 'ready' ? 'fail' : 'warning');
|
|
59
69
|
const degraded = integrity !== 'ok' || missing.length > 0 || incidents.length > 0
|
|
60
70
|
|| pending.length > 0 || running.length > 0 || failed.length > 0 || orphanSessions > 0 || activeMissing > 0
|
|
61
71
|
|| archiveIntents.length > 0
|
|
62
|
-
|| eventCursor > projectedCursor
|
|
72
|
+
|| eventCursor > projectedCursor || workflowHealth.status === 'fail'
|
|
73
|
+
|| baselineCheckStatus === 'fail';
|
|
63
74
|
return {
|
|
64
75
|
status: degraded ? 'degraded' : 'healthy',
|
|
65
76
|
checks: {
|
|
@@ -77,7 +88,7 @@ function inspectSystem(db, { rootDir = process.cwd() } = {}) {
|
|
|
77
88
|
pending: archiveIntents.length, corrupt: corruptArchiveIntents,
|
|
78
89
|
},
|
|
79
90
|
baseline: {
|
|
80
|
-
status:
|
|
91
|
+
status: baselineCheckStatus,
|
|
81
92
|
readiness: baseline.status,
|
|
82
93
|
mode: baseline.baseline?.mode || null,
|
|
83
94
|
baseline_status: baseline.baseline?.status || null,
|
|
@@ -89,6 +100,21 @@ function inspectSystem(db, { rootDir = process.cwd() } = {}) {
|
|
|
89
100
|
blockers: baseline.blockers,
|
|
90
101
|
warnings: baseline.warnings,
|
|
91
102
|
},
|
|
103
|
+
workflows: {
|
|
104
|
+
status: workflowHealth.status === 'fail'
|
|
105
|
+
? 'fail'
|
|
106
|
+
: (workflowHealth.blocked > 0
|
|
107
|
+
|| workflowHealth.historical_stale_outputs.length > 0
|
|
108
|
+
|| workflowHealth.terminal_authority_runs.length > 0
|
|
109
|
+
|| workflowHealth.untracked_active_changes.length > 0 ? 'warning' : 'pass'),
|
|
110
|
+
active: workflowHealth.active,
|
|
111
|
+
blocked: workflowHealth.blocked,
|
|
112
|
+
ready: workflowHealth.ready,
|
|
113
|
+
stale_outputs: workflowHealth.stale_outputs,
|
|
114
|
+
historical_stale_outputs: workflowHealth.historical_stale_outputs,
|
|
115
|
+
terminal_authority_runs: workflowHealth.terminal_authority_runs,
|
|
116
|
+
untracked_active_changes: workflowHealth.untracked_active_changes,
|
|
117
|
+
},
|
|
92
118
|
external_providers: {
|
|
93
119
|
status: 'pass', ownership: 'external',
|
|
94
120
|
note: 'Ultra stores provider metadata references only and never owns memory or code-graph content.',
|
|
@@ -113,6 +139,7 @@ async function runDoctor(db, {
|
|
|
113
139
|
if (!repair) return { ...inspectSystem(db, { rootDir }), repair_performed: false };
|
|
114
140
|
const backupPath = await backupDatabase(db, rootDir);
|
|
115
141
|
const archives = changes.recoverInterruptedArchives(db, { rootDir });
|
|
142
|
+
const workflowRecovery = workflows.recoverUntrackedChangeWorkflows(db, { rootDir });
|
|
116
143
|
const recovered = recovery.recoverOnBoot(db);
|
|
117
144
|
const cursor = runtime.readConsumerCursor(db, SPEC_CONSUMER);
|
|
118
145
|
const consumed = ops.consumeSpecChangedEvents(db, { since_id: cursor, limit: 500 });
|
|
@@ -127,7 +154,10 @@ async function runDoctor(db, {
|
|
|
127
154
|
...inspectSystem(db, { rootDir }),
|
|
128
155
|
repair_performed: true,
|
|
129
156
|
backup_path: backupPath,
|
|
130
|
-
repair: {
|
|
157
|
+
repair: {
|
|
158
|
+
archives, workflows: workflowRecovery, recovered, consumed,
|
|
159
|
+
interrupted, requeued, ensured, projections,
|
|
160
|
+
},
|
|
131
161
|
};
|
|
132
162
|
}
|
|
133
163
|
|