ultra-builder-pro-cli 0.14.0 → 0.16.0

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