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.
Files changed (172) hide show
  1. package/.ultra-template/docs/research/README.md +11 -7
  2. package/.ultra-template/specs/architecture.md +36 -36
  3. package/.ultra-template/specs/discovery.md +46 -33
  4. package/.ultra-template/specs/product.md +38 -29
  5. package/.ultra-template/specs/research-distillate.md +48 -0
  6. package/.ultra-template/tasks/contexts/TEMPLATE.md +11 -28
  7. package/CHANGELOG.md +86 -1
  8. package/README.md +40 -26
  9. package/adapters/_shared/codex-assets.cjs +29 -165
  10. package/adapters/_shared/file-ops.cjs +6 -2
  11. package/adapters/_shared/runtime-assets.cjs +1 -0
  12. package/adapters/claude.js +3 -2
  13. package/adapters/codex.js +1 -0
  14. package/adapters/kimi.js +3 -2
  15. package/adapters/opencode.js +33 -122
  16. package/agents/review-coordinator.md +9 -6
  17. package/docs/AGENT-CONTEXT.md +21 -11
  18. package/docs/ARCHITECTURE.md +41 -13
  19. package/docs/PLAN.zh-CN.md +3 -2
  20. package/docs/ROADMAP.md +9 -5
  21. package/docs/RUNTIME-COMPAT-MATRIX.md +6 -6
  22. package/docs/STATE-DB-ACCESS-POLICY.md +27 -25
  23. package/docs/WORKFLOW-LIFECYCLE.md +266 -0
  24. package/hooks/README.md +6 -1
  25. package/hooks/active_task_context.py +22 -50
  26. package/hooks/context_spine.py +61 -332
  27. package/hooks/health_check.py +29 -18
  28. package/hooks/pre_stop_check.py +28 -33
  29. package/hooks/subagent_tracker.py +54 -85
  30. package/hooks/workflow_checkpoint.py +39 -47
  31. package/hooks/workflow_context.py +4 -9
  32. package/hooks/workflow_resume.py +48 -128
  33. package/mcp-server/breadcrumb.cjs +22 -0
  34. package/mcp-server/hook-event.cjs +34 -0
  35. package/mcp-server/lib/archive-journal.cjs +17 -5
  36. package/mcp-server/lib/baseline-workflow.cjs +158 -18
  37. package/mcp-server/lib/change-workflow.cjs +550 -85
  38. package/mcp-server/lib/context-spine.cjs +242 -24
  39. package/mcp-server/lib/doctor.cjs +33 -3
  40. package/mcp-server/lib/init-project.cjs +210 -16
  41. package/mcp-server/lib/plan-store.cjs +4 -1
  42. package/mcp-server/lib/prd-parser.cjs +14 -41
  43. package/mcp-server/lib/pricing.cjs +7 -10
  44. package/mcp-server/lib/project-breadcrumb.cjs +145 -0
  45. package/mcp-server/lib/projector.cjs +57 -3
  46. package/mcp-server/lib/spec-learning.cjs +79 -2
  47. package/mcp-server/lib/state-db.cjs +145 -8
  48. package/mcp-server/lib/state-ops.cjs +160 -9
  49. package/mcp-server/lib/task-expander.cjs +9 -38
  50. package/mcp-server/lib/workflow-state.cjs +2292 -0
  51. package/mcp-server/server.cjs +182 -50
  52. package/orchestrator/daemon.cjs +1 -2
  53. package/orchestrator/dispatch-rules.cjs +5 -31
  54. package/orchestrator/planner/plan-builder.cjs +21 -12
  55. package/package.json +14 -6
  56. package/skills/cc-collab/SKILL.md +2 -2
  57. package/skills/codex-collab/SKILL.md +2 -3
  58. package/skills/ultra-change/SKILL.md +53 -68
  59. package/skills/ultra-change/references/change-contract.md +36 -0
  60. package/skills/ultra-deliver/SKILL.md +69 -79
  61. package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
  62. package/skills/ultra-dev/SKILL.md +54 -59
  63. package/skills/ultra-doctor/SKILL.md +40 -71
  64. package/skills/ultra-init/SKILL.md +96 -103
  65. package/skills/ultra-init/references/brownfield-adoption.md +2 -2
  66. package/skills/ultra-plan/SKILL.md +79 -75
  67. package/skills/ultra-plan/references/semantic-preflight.md +18 -0
  68. package/skills/ultra-research/SKILL.md +87 -60
  69. package/skills/ultra-research/references/00-problem-validation.md +36 -0
  70. package/skills/ultra-research/references/01-opportunity-discovery.md +33 -0
  71. package/skills/ultra-research/references/02-market-assessment.md +30 -0
  72. package/skills/ultra-research/references/03-competitive-landscape.md +31 -0
  73. package/skills/ultra-research/references/04-product-strategy.md +32 -0
  74. package/skills/ultra-research/references/05-assumptions-validation.md +31 -0
  75. package/skills/ultra-research/references/10-user-personas.md +29 -0
  76. package/skills/ultra-research/references/11-user-scenarios.md +28 -0
  77. package/skills/ultra-research/references/20-user-stories.md +30 -0
  78. package/skills/ultra-research/references/21-features-scope.md +29 -0
  79. package/skills/ultra-research/references/22-success-metrics.md +28 -0
  80. package/skills/ultra-research/references/30-architecture-context.md +30 -0
  81. package/skills/ultra-research/references/31-solution-strategy.md +28 -0
  82. package/skills/ultra-research/references/32-building-blocks.md +28 -0
  83. package/skills/ultra-research/references/40-deployment.md +29 -0
  84. package/skills/ultra-research/references/41-quality-risks.md +28 -0
  85. package/skills/ultra-research/references/99-synthesis.md +35 -0
  86. package/skills/ultra-review/SKILL.md +57 -73
  87. package/skills/ultra-review/references/review-modes.md +27 -0
  88. package/skills/ultra-review/references/unified-schema.md +25 -0
  89. package/skills/ultra-review/scripts/review_wait.py +40 -3
  90. package/skills/ultra-status/SKILL.md +41 -45
  91. package/skills/ultra-test/SKILL.md +48 -55
  92. package/skills/ultra-verify/SKILL.md +22 -31
  93. package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
  94. package/skills/ultra-verify/scripts/verify_wait.py +54 -54
  95. package/spec/cli-protocol.md +7 -2
  96. package/spec/mcp-tools.yaml +422 -73
  97. package/spec/schemas/baseline-reconciliation.v1.schema.json +73 -0
  98. package/spec/schemas/delivery-report.v1.schema.json +111 -0
  99. package/spec/schemas/mcp-tools.schema.json +1 -1
  100. package/spec/schemas/state-db.sql +82 -1
  101. package/spec/schemas/tasks.v4.5.schema.json +8 -1
  102. package/spec/schemas/test-report.v1.schema.json +194 -0
  103. package/ultra-tools/cli.cjs +1 -1
  104. package/ultra-tools/commands/migrate.cjs +3 -3
  105. package/ultra-tools/commands/status.cjs +213 -12
  106. package/.ultra-template/delivery-report.json +0 -9
  107. package/.ultra-template/test-report.json +0 -8
  108. package/adapters/_shared/tests/file-ops.test.cjs +0 -54
  109. package/adapters/_shared/tests/frontmatter.test.cjs +0 -47
  110. package/adapters/_shared/tests/handbook.test.cjs +0 -101
  111. package/adapters/_shared/tests/path-rewrite.test.cjs +0 -60
  112. package/adapters/_shared/tests/provenance.test.cjs +0 -133
  113. package/adapters/_shared/tests/resolve-target.test.cjs +0 -75
  114. package/adapters/_shared/tests/runtime-assets.test.cjs +0 -131
  115. package/adapters/_shared/tests/settings-merge.test.cjs +0 -85
  116. package/adapters/_shared/tests/validate.test.cjs +0 -30
  117. package/adapters/tests/claude.test.cjs +0 -137
  118. package/adapters/tests/codex-hook.test.cjs +0 -115
  119. package/adapters/tests/codex.test.cjs +0 -423
  120. package/adapters/tests/kimi-hook.test.cjs +0 -122
  121. package/adapters/tests/kimi.test.cjs +0 -259
  122. package/adapters/tests/opencode.test.cjs +0 -371
  123. package/mcp-server/lib/archive-journal.test.cjs +0 -35
  124. package/mcp-server/lib/baseline-workflow.test.cjs +0 -477
  125. package/mcp-server/lib/change-workflow.test.cjs +0 -971
  126. package/mcp-server/lib/context-spine.test.cjs +0 -93
  127. package/mcp-server/lib/doctor.test.cjs +0 -148
  128. package/mcp-server/lib/llm-client.cjs +0 -157
  129. package/mcp-server/lib/llm-client.test.cjs +0 -156
  130. package/mcp-server/lib/plan-store.test.cjs +0 -132
  131. package/mcp-server/lib/prd-parser.test.cjs +0 -171
  132. package/mcp-server/lib/runtime-state.test.cjs +0 -92
  133. package/mcp-server/lib/spec-learning.test.cjs +0 -67
  134. package/mcp-server/lib/state-db.test.cjs +0 -314
  135. package/mcp-server/lib/task-expander.test.cjs +0 -324
  136. package/mcp-server/lib/topo.test.cjs +0 -158
  137. package/mcp-server/tests/circuit-breaker.test.cjs +0 -125
  138. package/mcp-server/tests/commit-hash-flow.test.cjs +0 -117
  139. package/mcp-server/tests/concurrency-update.test.cjs +0 -96
  140. package/mcp-server/tests/concurrency.test.cjs +0 -112
  141. package/mcp-server/tests/db-ops.test.cjs +0 -121
  142. package/mcp-server/tests/init-project.test.cjs +0 -393
  143. package/mcp-server/tests/migrate.test.cjs +0 -232
  144. package/mcp-server/tests/projector.test.cjs +0 -162
  145. package/mcp-server/tests/server-contract.test.cjs +0 -1471
  146. package/mcp-server/tests/staleness.test.cjs +0 -190
  147. package/mcp-server/tests/state-db.test.cjs +0 -106
  148. package/mcp-server/tests/state-ops.test.cjs +0 -449
  149. package/mcp-server/tests/tagged-tasks.test.cjs +0 -163
  150. package/mcp-server/tests/telemetry.test.cjs +0 -219
  151. package/orchestrator/planner/expand-prompt.cjs +0 -62
  152. package/orchestrator/planner/expand-prompt.test.cjs +0 -52
  153. package/orchestrator/planner/plan-builder.test.cjs +0 -165
  154. package/orchestrator/planner/prd-prompt.cjs +0 -44
  155. package/orchestrator/planner/prd-prompt.test.cjs +0 -30
  156. package/orchestrator/tests/auto-merge.test.cjs +0 -216
  157. package/orchestrator/tests/daemon.test.cjs +0 -311
  158. package/orchestrator/tests/dispatch-rules.test.cjs +0 -124
  159. package/orchestrator/tests/parallel-orchestrator.test.cjs +0 -233
  160. package/orchestrator/tests/phase-8b-gate.test.cjs +0 -183
  161. package/orchestrator/tests/recovery.test.cjs +0 -190
  162. package/orchestrator/tests/session-runner.test.cjs +0 -314
  163. package/orchestrator/tests/skill-miner.test.cjs +0 -147
  164. package/orchestrator/tests/worktree-manager.test.cjs +0 -148
  165. package/skills/ultra-research/references/architecture.md +0 -38
  166. package/skills/ultra-research/references/problem-product.md +0 -39
  167. package/skills/ultra-research/references/quality-delivery.md +0 -34
  168. package/skills/ultra-research/references/synthesis.md +0 -38
  169. package/skills/ultra-research/references/users-scope.md +0 -40
  170. package/ultra-tools/legacy-memory.test.cjs +0 -80
  171. package/ultra-tools/status-command.test.cjs +0 -181
  172. 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, { taskId }) {
132
- const source = input.execution_contract || inherited || null;
133
- if (!taskId) return source;
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 = [], explicit }) {
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 = normalizeRefs(input, role, inherited.context?.items);
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, { taskId: input.task_id },
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
- // An active change must remain executable even when adoption is incomplete or
217
- // the repository has moved. Approved adoption is required at convergence and
218
- // full revision/spec health is required at archive; here both are recovery
219
- // guidance, not a refusal.
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, explicit: input.next_action,
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: currentHead(rootDir),
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 nextAction = baseline
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', context_manifest_path: null,
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 * FROM tasks WHERE change_id = ? ORDER BY created_at ASC').all(change.id);
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 head = currentHead(rootDir);
508
+ const checkout = baselines.gitWorktreeSnapshot(rootDir, ['.']);
509
+ const head = checkout.head;
327
510
  const snapshotMissing = !snapshot;
328
511
  const legacyContext = Boolean(
329
- snapshot && (!context.resume || !context.context || !context.readiness || !('baseline' in context)),
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 warnings = [...baselineHealth.blockers, ...baselineHealth.warnings];
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) || blocker.startsWith('BASELINE_')) {
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 (snapshotMissing) {
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: recommendedWorkflow(change, task, tasks, readiness),
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: baseline.status === 'pass' ? 'pass' : 'warning',
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: { archives, recovered, consumed, interrupted, requeued, ensured, projections },
157
+ repair: {
158
+ archives, workflows: workflowRecovery, recovered, consumed,
159
+ interrupted, requeued, ensured, projections,
160
+ },
131
161
  };
132
162
  }
133
163