ultra-builder-pro-cli 0.14.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 (154) 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 +44 -1
  8. package/README.md +34 -19
  9. package/adapters/_shared/codex-assets.cjs +6 -2
  10. package/adapters/_shared/file-ops.cjs +6 -2
  11. package/adapters/claude.js +3 -2
  12. package/adapters/codex.js +1 -0
  13. package/adapters/kimi.js +3 -2
  14. package/adapters/opencode.js +3 -2
  15. package/agents/review-coordinator.md +9 -6
  16. package/docs/AGENT-CONTEXT.md +15 -8
  17. package/docs/ARCHITECTURE.md +33 -12
  18. package/docs/PLAN.zh-CN.md +3 -2
  19. package/docs/ROADMAP.md +8 -4
  20. package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
  21. package/docs/STATE-DB-ACCESS-POLICY.md +27 -25
  22. package/docs/WORKFLOW-LIFECYCLE.md +266 -0
  23. package/hooks/README.md +6 -1
  24. package/hooks/health_check.py +12 -3
  25. package/hooks/subagent_tracker.py +52 -77
  26. package/mcp-server/hook-event.cjs +34 -0
  27. package/mcp-server/lib/archive-journal.cjs +17 -5
  28. package/mcp-server/lib/baseline-workflow.cjs +158 -18
  29. package/mcp-server/lib/change-workflow.cjs +550 -85
  30. package/mcp-server/lib/context-spine.cjs +242 -24
  31. package/mcp-server/lib/doctor.cjs +33 -3
  32. package/mcp-server/lib/init-project.cjs +210 -16
  33. package/mcp-server/lib/plan-store.cjs +4 -1
  34. package/mcp-server/lib/pricing.cjs +7 -10
  35. package/mcp-server/lib/project-breadcrumb.cjs +6 -0
  36. package/mcp-server/lib/projector.cjs +57 -3
  37. package/mcp-server/lib/spec-learning.cjs +79 -2
  38. package/mcp-server/lib/state-db.cjs +145 -8
  39. package/mcp-server/lib/state-ops.cjs +160 -9
  40. package/mcp-server/lib/workflow-state.cjs +2292 -0
  41. package/mcp-server/server.cjs +161 -15
  42. package/orchestrator/daemon.cjs +1 -2
  43. package/orchestrator/dispatch-rules.cjs +5 -31
  44. package/orchestrator/planner/plan-builder.cjs +21 -12
  45. package/package.json +13 -3
  46. package/skills/ultra-change/SKILL.md +53 -68
  47. package/skills/ultra-change/references/change-contract.md +36 -0
  48. package/skills/ultra-deliver/SKILL.md +69 -79
  49. package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
  50. package/skills/ultra-dev/SKILL.md +54 -59
  51. package/skills/ultra-doctor/SKILL.md +40 -71
  52. package/skills/ultra-init/SKILL.md +96 -103
  53. package/skills/ultra-init/references/brownfield-adoption.md +2 -2
  54. package/skills/ultra-plan/SKILL.md +79 -85
  55. package/skills/ultra-plan/references/semantic-preflight.md +18 -0
  56. package/skills/ultra-research/SKILL.md +87 -60
  57. package/skills/ultra-research/references/00-problem-validation.md +36 -0
  58. package/skills/ultra-research/references/01-opportunity-discovery.md +33 -0
  59. package/skills/ultra-research/references/02-market-assessment.md +30 -0
  60. package/skills/ultra-research/references/03-competitive-landscape.md +31 -0
  61. package/skills/ultra-research/references/04-product-strategy.md +32 -0
  62. package/skills/ultra-research/references/05-assumptions-validation.md +31 -0
  63. package/skills/ultra-research/references/10-user-personas.md +29 -0
  64. package/skills/ultra-research/references/11-user-scenarios.md +28 -0
  65. package/skills/ultra-research/references/20-user-stories.md +30 -0
  66. package/skills/ultra-research/references/21-features-scope.md +29 -0
  67. package/skills/ultra-research/references/22-success-metrics.md +28 -0
  68. package/skills/ultra-research/references/30-architecture-context.md +30 -0
  69. package/skills/ultra-research/references/31-solution-strategy.md +28 -0
  70. package/skills/ultra-research/references/32-building-blocks.md +28 -0
  71. package/skills/ultra-research/references/40-deployment.md +29 -0
  72. package/skills/ultra-research/references/41-quality-risks.md +28 -0
  73. package/skills/ultra-research/references/99-synthesis.md +35 -0
  74. package/skills/ultra-review/SKILL.md +57 -73
  75. package/skills/ultra-review/references/review-modes.md +27 -0
  76. package/skills/ultra-review/references/unified-schema.md +25 -0
  77. package/skills/ultra-review/scripts/review_wait.py +40 -3
  78. package/skills/ultra-status/SKILL.md +41 -45
  79. package/skills/ultra-test/SKILL.md +48 -55
  80. package/skills/ultra-verify/SKILL.md +1 -1
  81. package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
  82. package/spec/cli-protocol.md +7 -2
  83. package/spec/mcp-tools.yaml +384 -63
  84. package/spec/schemas/baseline-reconciliation.v1.schema.json +73 -0
  85. package/spec/schemas/delivery-report.v1.schema.json +111 -0
  86. package/spec/schemas/mcp-tools.schema.json +1 -1
  87. package/spec/schemas/state-db.sql +82 -1
  88. package/spec/schemas/tasks.v4.5.schema.json +8 -1
  89. package/spec/schemas/test-report.v1.schema.json +194 -0
  90. package/ultra-tools/cli.cjs +1 -1
  91. package/ultra-tools/commands/migrate.cjs +3 -3
  92. package/ultra-tools/commands/status.cjs +213 -12
  93. package/.ultra-template/delivery-report.json +0 -9
  94. package/.ultra-template/test-report.json +0 -8
  95. package/adapters/_shared/tests/file-ops.test.cjs +0 -54
  96. package/adapters/_shared/tests/frontmatter.test.cjs +0 -47
  97. package/adapters/_shared/tests/handbook.test.cjs +0 -101
  98. package/adapters/_shared/tests/path-rewrite.test.cjs +0 -60
  99. package/adapters/_shared/tests/provenance.test.cjs +0 -133
  100. package/adapters/_shared/tests/resolve-target.test.cjs +0 -75
  101. package/adapters/_shared/tests/runtime-assets.test.cjs +0 -149
  102. package/adapters/_shared/tests/settings-merge.test.cjs +0 -85
  103. package/adapters/_shared/tests/validate.test.cjs +0 -30
  104. package/adapters/tests/claude.test.cjs +0 -138
  105. package/adapters/tests/codex-hook.test.cjs +0 -142
  106. package/adapters/tests/codex.test.cjs +0 -430
  107. package/adapters/tests/kimi-hook.test.cjs +0 -149
  108. package/adapters/tests/kimi.test.cjs +0 -259
  109. package/adapters/tests/opencode.test.cjs +0 -357
  110. package/mcp-server/lib/archive-journal.test.cjs +0 -35
  111. package/mcp-server/lib/baseline-workflow.test.cjs +0 -477
  112. package/mcp-server/lib/change-workflow.test.cjs +0 -971
  113. package/mcp-server/lib/context-spine.test.cjs +0 -93
  114. package/mcp-server/lib/doctor.test.cjs +0 -148
  115. package/mcp-server/lib/plan-store.test.cjs +0 -132
  116. package/mcp-server/lib/prd-parser.test.cjs +0 -82
  117. package/mcp-server/lib/project-breadcrumb.test.cjs +0 -112
  118. package/mcp-server/lib/runtime-state.test.cjs +0 -92
  119. package/mcp-server/lib/spec-learning.test.cjs +0 -67
  120. package/mcp-server/lib/state-db.test.cjs +0 -314
  121. package/mcp-server/lib/task-expander.test.cjs +0 -169
  122. package/mcp-server/lib/topo.test.cjs +0 -158
  123. package/mcp-server/tests/circuit-breaker.test.cjs +0 -125
  124. package/mcp-server/tests/commit-hash-flow.test.cjs +0 -117
  125. package/mcp-server/tests/concurrency-update.test.cjs +0 -96
  126. package/mcp-server/tests/concurrency.test.cjs +0 -112
  127. package/mcp-server/tests/db-ops.test.cjs +0 -121
  128. package/mcp-server/tests/init-project.test.cjs +0 -393
  129. package/mcp-server/tests/migrate.test.cjs +0 -232
  130. package/mcp-server/tests/projector.test.cjs +0 -162
  131. package/mcp-server/tests/server-contract.test.cjs +0 -1467
  132. package/mcp-server/tests/staleness.test.cjs +0 -190
  133. package/mcp-server/tests/state-db.test.cjs +0 -106
  134. package/mcp-server/tests/state-ops.test.cjs +0 -449
  135. package/mcp-server/tests/tagged-tasks.test.cjs +0 -163
  136. package/mcp-server/tests/telemetry.test.cjs +0 -219
  137. package/orchestrator/planner/plan-builder.test.cjs +0 -165
  138. package/orchestrator/tests/auto-merge.test.cjs +0 -216
  139. package/orchestrator/tests/daemon.test.cjs +0 -311
  140. package/orchestrator/tests/dispatch-rules.test.cjs +0 -124
  141. package/orchestrator/tests/parallel-orchestrator.test.cjs +0 -233
  142. package/orchestrator/tests/phase-8b-gate.test.cjs +0 -183
  143. package/orchestrator/tests/recovery.test.cjs +0 -190
  144. package/orchestrator/tests/session-runner.test.cjs +0 -314
  145. package/orchestrator/tests/skill-miner.test.cjs +0 -147
  146. package/orchestrator/tests/worktree-manager.test.cjs +0 -148
  147. package/skills/ultra-research/references/architecture.md +0 -38
  148. package/skills/ultra-research/references/problem-product.md +0 -39
  149. package/skills/ultra-research/references/quality-delivery.md +0 -34
  150. package/skills/ultra-research/references/synthesis.md +0 -38
  151. package/skills/ultra-research/references/users-scope.md +0 -40
  152. package/ultra-tools/legacy-memory.test.cjs +0 -80
  153. package/ultra-tools/status-command.test.cjs +0 -181
  154. 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