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
@@ -1,355 +1,84 @@
1
1
  #!/usr/bin/env python3
2
- """Read and render the compact DB-derived Ultra Context Spine breadcrumb."""
2
+ """Thin bridge to the single bundled JavaScript Context Spine reader."""
3
3
 
4
4
  import json
5
- import hashlib
6
- import sqlite3
5
+ import os
6
+ import shutil
7
7
  import subprocess
8
8
  from pathlib import Path
9
9
 
10
10
 
11
- ACTIVE_CHANGE_STATUSES = ("active", "blocked", "ready")
12
- TERMINAL_TASK_STATUSES = ("completed", "expanded")
13
- CONTEXT_COLUMNS = {
14
- "role", "gate", "next_action", "readiness", "blockers_json",
15
- "context_json", "manifest_path", "manifest_hash", "git_head", "task_id",
16
- }
17
- ADVISORY_CONTEXT_CODES = {
18
- "CONTEXT_FILE_BUDGET_EXCEEDED",
19
- "CONTEXT_TOKEN_BUDGET_EXCEEDED",
20
- "EXECUTION_CONTEXT_BUDGET_EXCEEDED",
21
- "EXECUTION_CONTEXT_BUDGET_ADVISORY",
22
- }
11
+ class ContextSpineError(RuntimeError):
12
+ """The canonical breadcrumb helper could not be executed or decoded."""
13
+
14
+
15
+ class Breadcrumb(dict):
16
+ """Dictionary breadcrumb carrying its canonical rendered injection text."""
17
+
18
+ def __init__(self, value: dict, rendered: str):
19
+ super().__init__(value)
20
+ self.rendered = rendered
23
21
 
24
22
 
25
23
  def find_root(start: Path):
26
24
  for root in (start, *start.parents):
27
- ultra = root / ".ultra"
28
- if (
29
- (ultra / "state.db").is_file()
30
- or (ultra / "workflow-state.json").is_file()
31
- or (ultra / "changes" / "active").is_dir()
32
- ):
25
+ if (root / ".ultra").is_dir():
33
26
  return root
34
27
  return None
35
28
 
36
29
 
37
- def safe_json(raw, fallback):
38
- if not isinstance(raw, str) or not raw.strip():
39
- return fallback
40
- try:
41
- value = json.loads(raw)
42
- return value
43
- except json.JSONDecodeError:
44
- return fallback
45
-
46
-
47
- def git_head(root: Path):
48
- result = subprocess.run(
49
- ["git", "rev-parse", "HEAD"],
50
- cwd=root,
51
- text=True,
52
- capture_output=True,
53
- check=False,
54
- timeout=2,
55
- )
56
- return result.stdout.strip() if result.returncode == 0 else None
57
-
58
-
59
- def git_branch(root: Path):
60
- result = subprocess.run(
61
- ["git", "branch", "--show-current"],
62
- cwd=root,
63
- text=True,
64
- capture_output=True,
65
- check=False,
66
- timeout=2,
67
- )
68
- return (result.stdout.strip() or None) if result.returncode == 0 else None
69
-
30
+ def _helper_path() -> Path:
31
+ hook_root = Path(__file__).resolve().parent
32
+ installed = hook_root.parent / "runtime" / "breadcrumb.cjs"
33
+ if installed.is_file():
34
+ return installed
35
+ source = hook_root.parent / "mcp-server" / "breadcrumb.cjs"
36
+ if source.is_file():
37
+ return source
38
+ raise ContextSpineError("bundled breadcrumb helper is missing")
70
39
 
71
- def baseline_health(conn, root: Path):
72
- columns = {row[1] for row in conn.execute("PRAGMA table_info(baselines)")}
73
- optional = [
74
- name for name in (
75
- "repository_branch", "worktree_state", "worktree_accepted", "gaps_json"
76
- ) if name in columns
77
- ]
78
- row = conn.execute(
79
- f"""SELECT id, mode, status, repository_revision, spec_refs_json{''.join(f', {name}' for name in optional)}
80
- FROM baselines WHERE status != 'superseded'
81
- ORDER BY updated_at DESC, rowid DESC LIMIT 1"""
82
- ).fetchone()
83
- if row is None:
84
- return {"status": "fail", "blockers": ["BASELINE_MISSING"], "warnings": [], "baseline": None}
85
- baseline = dict(row)
86
- blockers = []
87
- warnings = []
88
- if baseline["status"] != "ready":
89
- blockers.append(f"BASELINE_NOT_READY:{baseline['status']}")
90
- elif baseline["mode"] == "migrated":
91
- blockers.append("BASELINE_MIGRATION_REVIEW_REQUIRED")
92
- else:
93
- if "gaps_json" not in columns:
94
- blockers.append("BASELINE_SCHEMA_MIGRATION_REQUIRED")
95
- for gap in safe_json(baseline.get("gaps_json"), []):
96
- if (
97
- isinstance(gap, dict)
98
- and gap.get("status") == "open"
99
- and gap.get("blocking") is True
100
- ):
101
- blockers.append(f"BASELINE_GAP_BLOCKING:{gap.get('id', 'unknown')}")
102
- if baseline.get("worktree_state") == "dirty" and not baseline.get("worktree_accepted"):
103
- blockers.append("BASELINE_DIRTY_WORKTREE_NOT_ACCEPTED")
104
- branch = git_branch(root)
105
- if baseline.get("repository_branch") and branch and baseline["repository_branch"] != branch:
106
- blockers.append("BASELINE_BRANCH_STALE")
107
- for spec in safe_json(baseline.get("spec_refs_json"), []):
108
- ref = spec.get("path") if isinstance(spec, dict) else None
109
- expected = spec.get("digest") if isinstance(spec, dict) else None
110
- if not isinstance(ref, str) or not ref or Path(ref).is_absolute():
111
- blockers.append(f"BASELINE_SPEC_INVALID:{ref}")
112
- continue
113
- candidate = (root / ref).resolve()
114
- try:
115
- candidate.relative_to(root.resolve())
116
- except ValueError:
117
- blockers.append(f"BASELINE_SPEC_INVALID:{ref}")
118
- continue
119
- if not candidate.is_file():
120
- blockers.append(f"BASELINE_SPEC_MISSING:{ref}")
121
- continue
122
- digest = hashlib.sha256(candidate.read_bytes()).hexdigest()
123
- if not expected or digest != expected:
124
- blockers.append(f"BASELINE_SPEC_STALE:{ref}")
125
- head = git_head(root)
126
- revision = baseline.get("repository_revision")
127
- if head and revision and not str(revision).startswith("workspace:") and head != revision:
128
- blockers.append("BASELINE_HEAD_STALE")
129
- baseline.pop("spec_refs_json", None)
130
- baseline.pop("gaps_json", None)
131
- return {
132
- "status": "fail" if blockers else "pass",
133
- "blockers": blockers,
134
- "warnings": warnings,
135
- "baseline": baseline,
136
- }
137
40
 
138
-
139
- def _next_action(change, task, tasks, role, readiness, stored, state_changed, blockers=None):
140
- if readiness != "ready":
141
- if any(str(item).startswith("BASELINE_") for item in (blockers or [])):
142
- return "Complete or refresh the Ultra project baseline through ultra-init."
143
- return "Resolve the context readiness blockers, then recompile change.context."
144
- if not state_changed and isinstance(stored, str) and stored.strip():
145
- return stored.strip()
146
- if change["status"] == "ready":
147
- return f"Run ultra-deliver for change {change['id']}."
148
- if change["status"] == "blocked":
149
- return f"Resolve the blockers for change {change['id']}, then recompile context."
150
- if task and task["status"] == "pending":
151
- return f"Start task {task['id']} through ultra-dev."
152
- if task and task["status"] == "in_progress":
153
- if role == "check":
154
- return f"Run the exact verification command for task {task['id']}."
155
- if role == "review":
156
- return (
157
- "Complete independent spec-fidelity and engineering-standards "
158
- f"review for task {task['id']}."
159
- )
160
- return f"Continue the approved vertical slice for task {task['id']}."
161
- if tasks and all(row["status"] in TERMINAL_TASK_STATUSES for row in tasks):
162
- return f"Run ultra-test for change {change['id']}."
163
- pending = next((row for row in tasks if row["status"] == "pending"), None)
164
- if pending:
165
- return f"Compile implement context for task {pending['id']}."
166
- return f"Create the first fresh-context tracer-bullet task for change {change['id']}."
167
-
168
-
169
- def _route(change, task, tasks, readiness, blockers=None):
170
- if readiness != "ready" or change["status"] == "blocked":
171
- return "ultra-change"
172
- if change["status"] == "ready":
173
- return "ultra-deliver"
174
- if task and task["status"] in ("pending", "in_progress"):
175
- return "ultra-dev"
176
- if tasks and all(row["status"] in TERMINAL_TASK_STATUSES for row in tasks):
177
- return "ultra-test"
178
- return "ultra-change"
41
+ def _node_binary() -> str:
42
+ configured = os.environ.get("UBP_NODE")
43
+ if configured:
44
+ return configured
45
+ discovered = shutil.which("node")
46
+ if discovered:
47
+ return discovered
48
+ raise ContextSpineError("node executable is unavailable")
179
49
 
180
50
 
181
51
  def read_breadcrumb(root: Path):
182
- db_path = root / ".ultra" / "state.db"
183
- if not db_path.is_file():
184
- return None
185
- with sqlite3.connect(f"file:{db_path}?mode=ro", uri=True, timeout=1) as conn:
186
- conn.row_factory = sqlite3.Row
187
- tables = {
188
- row[0]
189
- for row in conn.execute("SELECT name FROM sqlite_master WHERE type='table'")
190
- }
191
- if not {"baselines", "changes", "tasks", "context_snapshots"}.issubset(tables):
192
- return None
193
- baseline = baseline_health(conn, root)
194
- change = conn.execute(
195
- """SELECT id, status FROM changes
196
- WHERE status IN ('active', 'blocked', 'ready')
197
- ORDER BY updated_at DESC LIMIT 1"""
198
- ).fetchone()
199
- if change is None:
200
- current = baseline["baseline"]
201
- if baseline["status"] == "pass":
202
- next_action = "Start daily work with ultra-change."
203
- route = "ultra-change"
204
- readiness = "ready"
205
- else:
206
- next_action = "Initialize or complete project baseline adoption through ultra-init."
207
- route = "ultra-init"
208
- readiness = "blocked"
209
- return {
210
- "change_id": None,
211
- "task_id": None,
212
- "role": "plan",
213
- "gate": "alignment",
214
- "readiness": readiness,
215
- "blockers": baseline["blockers"],
216
- "warnings": baseline["warnings"],
217
- "next_action": next_action,
218
- "recommended_workflow": route,
219
- "context_manifest_path": None,
220
- "context_manifest_hash": None,
221
- "git_head": git_head(root),
222
- "baseline": current,
223
- }
224
- tasks = list(conn.execute(
225
- "SELECT id, status, session_id FROM tasks WHERE change_id = ? ORDER BY created_at ASC",
226
- (change["id"],),
227
- ))
228
- columns = {
229
- row[1] for row in conn.execute("PRAGMA table_info(context_snapshots)")
230
- }
231
- snapshot = None
232
- if CONTEXT_COLUMNS.issubset(columns):
233
- snapshot = conn.execute(
234
- """SELECT task_id, git_head, manifest_path, manifest_hash, role, gate,
235
- next_action, readiness, blockers_json, context_json
236
- FROM context_snapshots WHERE change_id = ?
237
- ORDER BY created_at DESC, rowid DESC LIMIT 1""",
238
- (change["id"],),
239
- ).fetchone()
240
-
241
- task_id = snapshot["task_id"] if snapshot else None
242
- if not task_id:
243
- active = next((row for row in tasks if row["status"] == "in_progress"), None)
244
- pending = next((row for row in tasks if row["status"] == "pending"), None)
245
- task_id = (active or pending)["id"] if (active or pending) else None
246
- task = next((row for row in tasks if row["id"] == task_id), None)
247
- context = safe_json(snapshot["context_json"], {}) if snapshot else {}
248
- role = snapshot["role"] if snapshot else "plan"
249
- gate = snapshot["gate"] if snapshot else "alignment"
250
- current_head = git_head(root)
251
- head_stale = bool(
252
- snapshot and snapshot["git_head"] and current_head
253
- and snapshot["git_head"] != current_head
254
- )
255
- snapshot_missing = snapshot is None
256
- legacy_context = bool(
257
- snapshot and (
258
- not isinstance(context.get("resume"), dict)
259
- or not isinstance(context.get("context"), dict)
260
- or not isinstance(context.get("readiness"), dict)
261
- or "baseline" not in context
262
- )
263
- )
264
- stored_status = context.get("resume", {}).get("task_status")
265
- current_status = task["status"] if task else None
266
- state_changed = stored_status is not None and stored_status != current_status
267
- stored_action = snapshot["next_action"] if snapshot else None
268
- blockers = []
269
- warnings = list(dict.fromkeys([*baseline["blockers"], *baseline["warnings"]]))
270
- if snapshot_missing:
271
- blockers.append("CONTEXT_NOT_COMPILED")
272
- else:
273
- if legacy_context:
274
- blockers.append("CONTEXT_SNAPSHOT_UPGRADE_REQUIRED")
275
- if head_stale:
276
- blockers.append("CONTEXT_HEAD_STALE")
277
- if state_changed:
278
- blockers.append("CONTEXT_TASK_STATE_STALE")
279
- for blocker in safe_json(snapshot["blockers_json"], []):
280
- if blocker in ADVISORY_CONTEXT_CODES or str(blocker).startswith("BASELINE_"):
281
- if blocker not in warnings:
282
- warnings.append(blocker)
283
- elif blocker not in blockers:
284
- blockers.append(blocker)
285
- for warning in context.get("readiness", {}).get("warnings", []):
286
- if warning not in warnings:
287
- warnings.append(warning)
288
- readiness = "blocked" if blockers else snapshot["readiness"]
289
- if snapshot_missing:
290
- next_action = f"Compile change.context for change {change['id']} before continuing."
291
- elif legacy_context:
292
- next_action = "Recompile change.context to upgrade this legacy context snapshot."
293
- elif head_stale:
294
- next_action = "Git HEAD changed after context compilation; recompile change.context."
295
- elif state_changed:
296
- next_action = "Task state changed after context compilation; recompile change.context."
297
- else:
298
- next_action = _next_action(
299
- change, task, tasks, role, readiness, stored_action, False, blockers
52
+ try:
53
+ result = subprocess.run(
54
+ [_node_binary(), str(_helper_path()), str(root.resolve())],
55
+ text=True,
56
+ capture_output=True,
57
+ check=False,
58
+ timeout=5,
300
59
  )
301
- return {
302
- "change_id": change["id"],
303
- "change_status": change["status"],
304
- "task_id": task["id"] if task else None,
305
- "task_status": task["status"] if task else None,
306
- "session_id": task["session_id"] if task else None,
307
- "role": role,
308
- "gate": gate,
309
- "readiness": readiness,
310
- "blockers": blockers,
311
- "warnings": warnings,
312
- "next_action": next_action,
313
- "recommended_workflow": _route(change, task, tasks, readiness, blockers),
314
- "context_manifest_path": snapshot["manifest_path"] if snapshot else None,
315
- "context_manifest_hash": snapshot["manifest_hash"] if snapshot else None,
316
- "git_head": current_head,
317
- "baseline": baseline["baseline"],
318
- }
60
+ except (OSError, subprocess.TimeoutExpired) as exc:
61
+ raise ContextSpineError(str(exc)) from exc
62
+ if result.returncode != 0:
63
+ detail = result.stderr.strip() or f"exit {result.returncode}"
64
+ raise ContextSpineError(detail)
65
+ try:
66
+ payload = json.loads(result.stdout)
67
+ except json.JSONDecodeError as exc:
68
+ raise ContextSpineError(f"invalid breadcrumb response: {exc}") from exc
69
+ if not isinstance(payload, dict):
70
+ raise ContextSpineError("breadcrumb response must be an object")
71
+ value = payload.get("breadcrumb")
72
+ if value is None:
73
+ return None
74
+ rendered = payload.get("text")
75
+ if not isinstance(value, dict) or not isinstance(rendered, str) or not rendered:
76
+ raise ContextSpineError("breadcrumb response is missing canonical state or text")
77
+ return Breadcrumb(value, rendered)
319
78
 
320
79
 
321
- def render_breadcrumb(root: Path, breadcrumb: dict) -> str:
322
- task = breadcrumb.get("task_id") or "none"
323
- task_status = breadcrumb.get("task_status") or "none"
324
- baseline = breadcrumb.get("baseline") or {}
325
- if breadcrumb.get("change_id"):
326
- lines = [
327
- "[Ultra context spine]",
328
- f"Project: {root}",
329
- f"Baseline: {baseline.get('id', 'unknown')} ({baseline.get('mode', 'unknown')}/{baseline.get('status', 'unknown')})",
330
- f"Change: {breadcrumb['change_id']} ({breadcrumb.get('change_status', 'active')})",
331
- f"Task: {task} ({task_status})",
332
- f"Role: {breadcrumb.get('role', 'plan')}",
333
- f"Gate: {breadcrumb.get('gate', 'alignment')}",
334
- f"Readiness: {breadcrumb.get('readiness', 'ready')}",
335
- ]
336
- else:
337
- lines = [
338
- "[Ultra baseline]",
339
- f"Project: {root}",
340
- f"Baseline: {baseline.get('id', 'missing')} ({baseline.get('mode', 'unknown')}/{baseline.get('status', 'missing')})",
341
- f"Readiness: {breadcrumb.get('readiness', 'blocked')}",
342
- ]
343
- if breadcrumb.get("blockers"):
344
- lines.append("Blockers: " + ", ".join(breadcrumb["blockers"]))
345
- if breadcrumb.get("warnings"):
346
- lines.append("Warnings: " + ", ".join(breadcrumb["warnings"]))
347
- lines.extend([
348
- f"Next: {breadcrumb.get('next_action', 'Inspect Ultra status.')}",
349
- f"Route: {breadcrumb.get('recommended_workflow', 'ultra-change')}",
350
- ])
351
- if breadcrumb.get("context_manifest_path"):
352
- digest = breadcrumb.get("context_manifest_hash") or "unknown"
353
- lines.append(f"Context: {breadcrumb['context_manifest_path']} sha256={digest}")
354
- lines.append("Authority: .ultra/state.db; JSON/Markdown remain projections or evidence artifacts.")
355
- return "\n".join(lines)
80
+ def render_breadcrumb(_root: Path, breadcrumb: dict) -> str:
81
+ rendered = getattr(breadcrumb, "rendered", None)
82
+ if not isinstance(rendered, str) or not rendered:
83
+ raise ContextSpineError("breadcrumb was not produced by the canonical reader")
84
+ return rendered
@@ -6,7 +6,7 @@ import sqlite3
6
6
  import sys
7
7
  from pathlib import Path
8
8
 
9
- from context_spine import baseline_health
9
+ from context_spine import ContextSpineError, find_root, read_breadcrumb
10
10
 
11
11
 
12
12
  REQUIRED_TABLES = {
@@ -14,7 +14,7 @@ REQUIRED_TABLES = {
14
14
  "tasks", "events", "sessions", "schema_version", "migration_history",
15
15
  "telemetry", "specs_refs", "circuit_breaker", "changes", "artifacts",
16
16
  "context_snapshots", "spec_learning_candidates", "trace_links", "incidents", "projection_jobs",
17
- "event_consumers",
17
+ "event_consumers", "workflow_runs", "workflow_steps",
18
18
  }
19
19
 
20
20
 
@@ -27,18 +27,6 @@ def hook_input() -> dict:
27
27
  return {}
28
28
 
29
29
 
30
- def find_root(start: Path):
31
- for candidate in (start, *start.parents):
32
- ultra = candidate / ".ultra"
33
- if (
34
- (ultra / "state.db").is_file()
35
- or (ultra / "workflow-state.json").is_file()
36
- or (ultra / "changes" / "active").is_dir()
37
- ):
38
- return candidate
39
- return None
40
-
41
-
42
30
  def inspect(root: Path) -> dict:
43
31
  db_path = root / ".ultra" / "state.db"
44
32
  report = {"status": "healthy", "project": str(root), "checks": {}}
@@ -48,6 +36,13 @@ def inspect(root: Path) -> dict:
48
36
  report["status"] = "degraded"
49
37
  return report
50
38
 
39
+ try:
40
+ breadcrumb = read_breadcrumb(root)
41
+ except ContextSpineError as exc:
42
+ report["checks"]["authority"] = {"status": "fail", "reason": str(exc)}
43
+ report["status"] = "degraded"
44
+ return report
45
+
51
46
  try:
52
47
  uri = f"file:{db_path}?mode=ro"
53
48
  with sqlite3.connect(uri, uri=True, timeout=1) as conn:
@@ -70,12 +65,28 @@ def inspect(root: Path) -> dict:
70
65
  report["status"] = "degraded"
71
66
  return report
72
67
 
73
- baseline = baseline_health(conn, root)
68
+ baseline_codes = [
69
+ code for code in [
70
+ *(breadcrumb.get("blockers", []) if breadcrumb else []),
71
+ *(breadcrumb.get("warnings", []) if breadcrumb else []),
72
+ ]
73
+ if str(code).startswith("BASELINE_")
74
+ ]
75
+ baseline = breadcrumb.get("baseline") if breadcrumb else None
76
+ baseline_is_ready = isinstance(baseline, dict) and baseline.get("status") == "ready"
77
+ baseline_status = (
78
+ "fail" if baseline_codes and baseline_is_ready
79
+ else "warning" if baseline_codes
80
+ else "pass"
81
+ )
74
82
  report["checks"]["baseline"] = {
75
- **baseline,
76
- "readiness": baseline["status"],
77
- "status": "pass" if baseline["status"] == "pass" else "warning",
83
+ "baseline": baseline,
84
+ "blockers": baseline_codes,
85
+ "readiness": "blocked" if baseline_codes else "ready",
86
+ "status": baseline_status,
78
87
  }
88
+ if baseline_status == "fail":
89
+ issues.append("baseline")
79
90
 
80
91
  incidents = [
81
92
  {"id": row[0], "code": row[1], "severity": row[2], "retryable": bool(row[3])}
@@ -1,26 +1,22 @@
1
1
  #!/usr/bin/env python3
2
- """Report unfinished Ultra workflow position without trapping session stop."""
2
+ """Report unfinished DB-authoritative Ultra work without trapping session stop."""
3
3
 
4
- import sys
5
4
  import json
5
+ import sys
6
6
  from pathlib import Path
7
7
 
8
- TERMINAL = {"committed", "completed", "done", "cancelled"}
8
+ from context_spine import ContextSpineError, find_root, read_breadcrumb
9
9
 
10
10
 
11
11
  def allow_stop() -> None:
12
12
  print(json.dumps({}))
13
13
 
14
14
 
15
- def advise_stop(reason: str) -> None:
16
- print(f"[Ultra stop advisory] {reason}", file=sys.stderr)
17
- allow_stop()
18
-
19
-
20
- def main():
15
+ def main() -> None:
21
16
  try:
22
- hook_data = json.loads(sys.stdin.read())
23
- except (json.JSONDecodeError, OSError) as exc:
17
+ hook_data = json.loads(sys.stdin.read() or "{}")
18
+ hook_data = hook_data if isinstance(hook_data, dict) else {}
19
+ except json.JSONDecodeError as exc:
24
20
  print(f"[pre_stop_check] invalid hook input: {exc}", file=sys.stderr)
25
21
  allow_stop()
26
22
  return
@@ -28,30 +24,29 @@ def main():
28
24
  if hook_data.get("stop_hook_active", False):
29
25
  allow_stop()
30
26
  return
31
- start = Path(hook_data.get("cwd") or Path.cwd()).resolve()
32
- for root in (start, *start.parents):
33
- state_file = root / ".ultra" / "workflow-state.json"
34
- if not state_file.is_file():
35
- continue
36
- try:
37
- state = json.loads(state_file.read_text(encoding="utf-8"))
38
- except (OSError, json.JSONDecodeError) as exc:
39
- print(f"[pre_stop_check] cannot read {state_file}: {exc}", file=sys.stderr)
40
- allow_stop()
41
- return
42
- if not isinstance(state, dict) or state.get("status") in TERMINAL:
43
- allow_stop()
44
- return
45
- advise_stop(
46
- "Active Ultra workflow is incomplete: "
47
- f"command={state.get('command', 'unknown')}, "
48
- f"task={state.get('task_id', state.get('task', 'unknown'))}, "
49
- f"step={state.get('step', 'unknown')}, status={state.get('status', 'active')}. "
50
- "Resume or explicitly cancel it in a later session; stop is allowed."
51
- )
27
+ root = find_root(Path(hook_data.get("cwd") or Path.cwd()).resolve())
28
+ if root is None:
29
+ allow_stop()
52
30
  return
31
+ try:
32
+ breadcrumb = read_breadcrumb(root)
33
+ except ContextSpineError as exc:
34
+ print(f"[pre_stop_check] cannot inspect Context Spine: {exc}", file=sys.stderr)
35
+ allow_stop()
36
+ return
37
+ if breadcrumb and breadcrumb.get("change_id"):
38
+ print(
39
+ "[Ultra stop advisory] Active Ultra change remains open: "
40
+ f"change={breadcrumb['change_id']}, "
41
+ f"task={breadcrumb.get('task_id') or 'none'}, "
42
+ f"gate={breadcrumb.get('gate') or 'alignment'}, "
43
+ f"route={breadcrumb.get('recommended_workflow') or 'ultra-status'}. "
44
+ f"Next: {breadcrumb.get('next_action') or 'Inspect Ultra status.'} "
45
+ "Stop is allowed; resume or explicitly archive the change later.",
46
+ file=sys.stderr,
47
+ )
53
48
  allow_stop()
54
49
 
55
50
 
56
- if __name__ == '__main__':
51
+ if __name__ == "__main__":
57
52
  main()