ultra-builder-pro-cli 0.12.1 → 0.14.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 (88) hide show
  1. package/.ultra-template/docs/research/README.md +21 -44
  2. package/.ultra-template/specs/discovery.md +10 -0
  3. package/CHANGELOG.md +95 -1
  4. package/README.md +31 -16
  5. package/adapters/_shared/codex-assets.cjs +23 -163
  6. package/adapters/_shared/handbook.cjs +2 -2
  7. package/adapters/_shared/runtime-assets.cjs +1 -0
  8. package/adapters/_shared/tests/runtime-assets.test.cjs +18 -0
  9. package/adapters/opencode.js +30 -120
  10. package/adapters/tests/claude.test.cjs +3 -2
  11. package/adapters/tests/codex-hook.test.cjs +34 -7
  12. package/adapters/tests/codex.test.cjs +21 -6
  13. package/adapters/tests/kimi-hook.test.cjs +34 -7
  14. package/adapters/tests/opencode.test.cjs +70 -84
  15. package/commands/ultra-init.md +1 -1
  16. package/docs/AGENT-CONTEXT.md +10 -6
  17. package/docs/ARCHITECTURE.md +40 -17
  18. package/docs/PLAN.zh-CN.md +5 -4
  19. package/docs/ROADMAP.md +2 -2
  20. package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
  21. package/docs/STATE-DB-ACCESS-POLICY.md +3 -3
  22. package/docs/USER-HANDBOOK-CONTRACT.md +5 -4
  23. package/hooks/active_task_context.py +22 -50
  24. package/hooks/context_spine.py +61 -299
  25. package/hooks/health_check.py +19 -17
  26. package/hooks/pre_stop_check.py +28 -33
  27. package/hooks/subagent_tracker.py +13 -19
  28. package/hooks/workflow_checkpoint.py +39 -47
  29. package/hooks/workflow_context.py +4 -9
  30. package/hooks/workflow_resume.py +48 -128
  31. package/mcp-server/breadcrumb.cjs +22 -0
  32. package/mcp-server/lib/archive-journal.cjs +192 -0
  33. package/mcp-server/lib/archive-journal.test.cjs +35 -0
  34. package/mcp-server/lib/baseline-workflow.cjs +404 -38
  35. package/mcp-server/lib/baseline-workflow.test.cjs +252 -0
  36. package/mcp-server/lib/change-workflow.cjs +249 -75
  37. package/mcp-server/lib/change-workflow.test.cjs +235 -1
  38. package/mcp-server/lib/context-spine.test.cjs +5 -0
  39. package/mcp-server/lib/doctor.cjs +14 -1
  40. package/mcp-server/lib/doctor.test.cjs +42 -4
  41. package/mcp-server/lib/init-project.cjs +292 -9
  42. package/mcp-server/lib/prd-parser.cjs +14 -41
  43. package/mcp-server/lib/prd-parser.test.cjs +49 -138
  44. package/mcp-server/lib/project-breadcrumb.cjs +139 -0
  45. package/mcp-server/lib/project-breadcrumb.test.cjs +112 -0
  46. package/mcp-server/lib/spec-learning.test.cjs +1 -1
  47. package/mcp-server/lib/state-authority.cjs +9 -3
  48. package/mcp-server/lib/state-db.cjs +154 -32
  49. package/mcp-server/lib/state-db.test.cjs +136 -4
  50. package/mcp-server/lib/state-ops.cjs +68 -6
  51. package/mcp-server/lib/task-expander.cjs +14 -37
  52. package/mcp-server/lib/task-expander.test.cjs +80 -164
  53. package/mcp-server/server.cjs +34 -37
  54. package/mcp-server/tests/init-project.test.cjs +162 -1
  55. package/mcp-server/tests/migrate.test.cjs +33 -0
  56. package/mcp-server/tests/server-contract.test.cjs +391 -17
  57. package/mcp-server/tests/state-db.test.cjs +14 -2
  58. package/mcp-server/tests/state-ops.test.cjs +108 -0
  59. package/package.json +2 -4
  60. package/skills/cc-collab/SKILL.md +2 -2
  61. package/skills/codex-collab/SKILL.md +2 -3
  62. package/skills/ultra-change/SKILL.md +18 -10
  63. package/skills/ultra-deliver/SKILL.md +13 -9
  64. package/skills/ultra-dev/SKILL.md +6 -4
  65. package/skills/ultra-doctor/SKILL.md +22 -7
  66. package/skills/ultra-init/SKILL.md +104 -59
  67. package/skills/ultra-init/references/brownfield-adoption.md +65 -37
  68. package/skills/ultra-plan/SKILL.md +20 -7
  69. package/skills/ultra-research/SKILL.md +12 -5
  70. package/skills/ultra-status/SKILL.md +6 -4
  71. package/skills/ultra-verify/SKILL.md +21 -30
  72. package/skills/ultra-verify/scripts/verify_wait.py +54 -54
  73. package/spec/cli-protocol.md +11 -1
  74. package/spec/mcp-tools.yaml +133 -19
  75. package/spec/schemas/state-db.sql +11 -0
  76. package/ultra-tools/cli.cjs +9 -5
  77. package/ultra-tools/commands/migrate.cjs +62 -36
  78. package/ultra-tools/commands/status.cjs +18 -9
  79. package/ultra-tools/commands/system.cjs +340 -6
  80. package/ultra-tools/commands/task.cjs +7 -1
  81. package/ultra-tools/status-command.test.cjs +29 -0
  82. package/ultra-tools/system-command.test.cjs +252 -1
  83. package/mcp-server/lib/llm-client.cjs +0 -157
  84. package/mcp-server/lib/llm-client.test.cjs +0 -156
  85. package/orchestrator/planner/expand-prompt.cjs +0 -62
  86. package/orchestrator/planner/expand-prompt.test.cjs +0 -52
  87. package/orchestrator/planner/prd-prompt.cjs +0 -44
  88. package/orchestrator/planner/prd-prompt.test.cjs +0 -30
@@ -1,322 +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 baseline_health(conn, root: Path):
60
- row = conn.execute(
61
- """SELECT id, mode, status, repository_revision, spec_refs_json
62
- FROM baselines WHERE status != 'superseded'
63
- ORDER BY updated_at DESC, rowid DESC LIMIT 1"""
64
- ).fetchone()
65
- if row is None:
66
- return {"status": "fail", "blockers": ["BASELINE_MISSING"], "warnings": [], "baseline": None}
67
- baseline = dict(row)
68
- blockers = []
69
- warnings = []
70
- if baseline["status"] != "ready":
71
- blockers.append(f"BASELINE_NOT_READY:{baseline['status']}")
72
- elif baseline["mode"] == "migrated":
73
- warnings.append("BASELINE_MIGRATION_REVIEW_RECOMMENDED")
74
- else:
75
- for spec in safe_json(baseline.get("spec_refs_json"), []):
76
- ref = spec.get("path") if isinstance(spec, dict) else None
77
- expected = spec.get("digest") if isinstance(spec, dict) else None
78
- if not isinstance(ref, str) or not ref or Path(ref).is_absolute():
79
- blockers.append(f"BASELINE_SPEC_INVALID:{ref}")
80
- continue
81
- candidate = (root / ref).resolve()
82
- try:
83
- candidate.relative_to(root.resolve())
84
- except ValueError:
85
- blockers.append(f"BASELINE_SPEC_INVALID:{ref}")
86
- continue
87
- if not candidate.is_file():
88
- blockers.append(f"BASELINE_SPEC_MISSING:{ref}")
89
- continue
90
- digest = hashlib.sha256(candidate.read_bytes()).hexdigest()
91
- if not expected or digest != expected:
92
- blockers.append(f"BASELINE_SPEC_STALE:{ref}")
93
- head = git_head(root)
94
- revision = baseline.get("repository_revision")
95
- if head and revision and not str(revision).startswith("workspace:") and head != revision:
96
- blockers.append("BASELINE_HEAD_STALE")
97
- baseline.pop("spec_refs_json", None)
98
- return {
99
- "status": "fail" if blockers else "pass",
100
- "blockers": blockers,
101
- "warnings": warnings,
102
- "baseline": baseline,
103
- }
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")
104
39
 
105
40
 
106
- def _next_action(change, task, tasks, role, readiness, stored, state_changed, blockers=None):
107
- if readiness != "ready":
108
- if any(str(item).startswith("BASELINE_") for item in (blockers or [])):
109
- return "Complete or refresh the Ultra project baseline through ultra-init."
110
- return "Resolve the context readiness blockers, then recompile change.context."
111
- if not state_changed and isinstance(stored, str) and stored.strip():
112
- return stored.strip()
113
- if change["status"] == "ready":
114
- return f"Run ultra-deliver for change {change['id']}."
115
- if change["status"] == "blocked":
116
- return f"Resolve the blockers for change {change['id']}, then recompile context."
117
- if task and task["status"] == "pending":
118
- return f"Start task {task['id']} through ultra-dev."
119
- if task and task["status"] == "in_progress":
120
- if role == "check":
121
- return f"Run the exact verification command for task {task['id']}."
122
- if role == "review":
123
- return (
124
- "Complete independent spec-fidelity and engineering-standards "
125
- f"review for task {task['id']}."
126
- )
127
- return f"Continue the approved vertical slice for task {task['id']}."
128
- if tasks and all(row["status"] in TERMINAL_TASK_STATUSES for row in tasks):
129
- return f"Run ultra-test for change {change['id']}."
130
- pending = next((row for row in tasks if row["status"] == "pending"), None)
131
- if pending:
132
- return f"Compile implement context for task {pending['id']}."
133
- return f"Create the first fresh-context tracer-bullet task for change {change['id']}."
134
-
135
-
136
- def _route(change, task, tasks, readiness, blockers=None):
137
- if readiness != "ready" or change["status"] == "blocked":
138
- return "ultra-change"
139
- if change["status"] == "ready":
140
- return "ultra-deliver"
141
- if task and task["status"] in ("pending", "in_progress"):
142
- return "ultra-dev"
143
- if tasks and all(row["status"] in TERMINAL_TASK_STATUSES for row in tasks):
144
- return "ultra-test"
145
- 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")
146
49
 
147
50
 
148
51
  def read_breadcrumb(root: Path):
149
- db_path = root / ".ultra" / "state.db"
150
- if not db_path.is_file():
151
- return None
152
- with sqlite3.connect(f"file:{db_path}?mode=ro", uri=True, timeout=1) as conn:
153
- conn.row_factory = sqlite3.Row
154
- tables = {
155
- row[0]
156
- for row in conn.execute("SELECT name FROM sqlite_master WHERE type='table'")
157
- }
158
- if not {"baselines", "changes", "tasks", "context_snapshots"}.issubset(tables):
159
- return None
160
- baseline = baseline_health(conn, root)
161
- change = conn.execute(
162
- """SELECT id, status FROM changes
163
- WHERE status IN ('active', 'blocked', 'ready')
164
- ORDER BY updated_at DESC LIMIT 1"""
165
- ).fetchone()
166
- if change is None:
167
- current = baseline["baseline"]
168
- if baseline["status"] == "pass":
169
- next_action = "Start daily work with ultra-change."
170
- route = "ultra-change"
171
- readiness = "ready"
172
- else:
173
- next_action = "Initialize or complete project baseline adoption through ultra-init."
174
- route = "ultra-init"
175
- readiness = "blocked"
176
- return {
177
- "change_id": None,
178
- "task_id": None,
179
- "role": "plan",
180
- "gate": "alignment",
181
- "readiness": readiness,
182
- "blockers": baseline["blockers"],
183
- "warnings": baseline["warnings"],
184
- "next_action": next_action,
185
- "recommended_workflow": route,
186
- "context_manifest_path": None,
187
- "context_manifest_hash": None,
188
- "git_head": git_head(root),
189
- "baseline": current,
190
- }
191
- tasks = list(conn.execute(
192
- "SELECT id, status, session_id FROM tasks WHERE change_id = ? ORDER BY created_at ASC",
193
- (change["id"],),
194
- ))
195
- columns = {
196
- row[1] for row in conn.execute("PRAGMA table_info(context_snapshots)")
197
- }
198
- snapshot = None
199
- if CONTEXT_COLUMNS.issubset(columns):
200
- snapshot = conn.execute(
201
- """SELECT task_id, git_head, manifest_path, manifest_hash, role, gate,
202
- next_action, readiness, blockers_json, context_json
203
- FROM context_snapshots WHERE change_id = ?
204
- ORDER BY created_at DESC, rowid DESC LIMIT 1""",
205
- (change["id"],),
206
- ).fetchone()
207
-
208
- task_id = snapshot["task_id"] if snapshot else None
209
- if not task_id:
210
- active = next((row for row in tasks if row["status"] == "in_progress"), None)
211
- pending = next((row for row in tasks if row["status"] == "pending"), None)
212
- task_id = (active or pending)["id"] if (active or pending) else None
213
- task = next((row for row in tasks if row["id"] == task_id), None)
214
- context = safe_json(snapshot["context_json"], {}) if snapshot else {}
215
- role = snapshot["role"] if snapshot else "plan"
216
- gate = snapshot["gate"] if snapshot else "alignment"
217
- current_head = git_head(root)
218
- head_stale = bool(
219
- snapshot and snapshot["git_head"] and current_head
220
- and snapshot["git_head"] != current_head
221
- )
222
- snapshot_missing = snapshot is None
223
- legacy_context = bool(
224
- snapshot and (
225
- not isinstance(context.get("resume"), dict)
226
- or not isinstance(context.get("context"), dict)
227
- or not isinstance(context.get("readiness"), dict)
228
- or "baseline" not in context
229
- )
230
- )
231
- stored_status = context.get("resume", {}).get("task_status")
232
- current_status = task["status"] if task else None
233
- state_changed = stored_status is not None and stored_status != current_status
234
- stored_action = snapshot["next_action"] if snapshot else None
235
- blockers = []
236
- warnings = list(dict.fromkeys([*baseline["blockers"], *baseline["warnings"]]))
237
- if snapshot_missing:
238
- blockers.append("CONTEXT_NOT_COMPILED")
239
- else:
240
- if legacy_context:
241
- blockers.append("CONTEXT_SNAPSHOT_UPGRADE_REQUIRED")
242
- if head_stale:
243
- blockers.append("CONTEXT_HEAD_STALE")
244
- if state_changed:
245
- blockers.append("CONTEXT_TASK_STATE_STALE")
246
- for blocker in safe_json(snapshot["blockers_json"], []):
247
- if blocker in ADVISORY_CONTEXT_CODES or str(blocker).startswith("BASELINE_"):
248
- if blocker not in warnings:
249
- warnings.append(blocker)
250
- elif blocker not in blockers:
251
- blockers.append(blocker)
252
- for warning in context.get("readiness", {}).get("warnings", []):
253
- if warning not in warnings:
254
- warnings.append(warning)
255
- readiness = "blocked" if blockers else snapshot["readiness"]
256
- if snapshot_missing:
257
- next_action = f"Compile change.context for change {change['id']} before continuing."
258
- elif legacy_context:
259
- next_action = "Recompile change.context to upgrade this legacy context snapshot."
260
- elif head_stale:
261
- next_action = "Git HEAD changed after context compilation; recompile change.context."
262
- elif state_changed:
263
- next_action = "Task state changed after context compilation; recompile change.context."
264
- else:
265
- next_action = _next_action(
266
- 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,
267
59
  )
268
- return {
269
- "change_id": change["id"],
270
- "change_status": change["status"],
271
- "task_id": task["id"] if task else None,
272
- "task_status": task["status"] if task else None,
273
- "session_id": task["session_id"] if task else None,
274
- "role": role,
275
- "gate": gate,
276
- "readiness": readiness,
277
- "blockers": blockers,
278
- "warnings": warnings,
279
- "next_action": next_action,
280
- "recommended_workflow": _route(change, task, tasks, readiness, blockers),
281
- "context_manifest_path": snapshot["manifest_path"] if snapshot else None,
282
- "context_manifest_hash": snapshot["manifest_hash"] if snapshot else None,
283
- "git_head": current_head,
284
- "baseline": baseline["baseline"],
285
- }
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)
286
78
 
287
79
 
288
- def render_breadcrumb(root: Path, breadcrumb: dict) -> str:
289
- task = breadcrumb.get("task_id") or "none"
290
- task_status = breadcrumb.get("task_status") or "none"
291
- baseline = breadcrumb.get("baseline") or {}
292
- if breadcrumb.get("change_id"):
293
- lines = [
294
- "[Ultra context spine]",
295
- f"Project: {root}",
296
- f"Baseline: {baseline.get('id', 'unknown')} ({baseline.get('mode', 'unknown')}/{baseline.get('status', 'unknown')})",
297
- f"Change: {breadcrumb['change_id']} ({breadcrumb.get('change_status', 'active')})",
298
- f"Task: {task} ({task_status})",
299
- f"Role: {breadcrumb.get('role', 'plan')}",
300
- f"Gate: {breadcrumb.get('gate', 'alignment')}",
301
- f"Readiness: {breadcrumb.get('readiness', 'ready')}",
302
- ]
303
- else:
304
- lines = [
305
- "[Ultra baseline]",
306
- f"Project: {root}",
307
- f"Baseline: {baseline.get('id', 'missing')} ({baseline.get('mode', 'unknown')}/{baseline.get('status', 'missing')})",
308
- f"Readiness: {breadcrumb.get('readiness', 'blocked')}",
309
- ]
310
- if breadcrumb.get("blockers"):
311
- lines.append("Blockers: " + ", ".join(breadcrumb["blockers"]))
312
- if breadcrumb.get("warnings"):
313
- lines.append("Warnings: " + ", ".join(breadcrumb["warnings"]))
314
- lines.extend([
315
- f"Next: {breadcrumb.get('next_action', 'Inspect Ultra status.')}",
316
- f"Route: {breadcrumb.get('recommended_workflow', 'ultra-change')}",
317
- ])
318
- if breadcrumb.get("context_manifest_path"):
319
- digest = breadcrumb.get("context_manifest_hash") or "unknown"
320
- lines.append(f"Context: {breadcrumb['context_manifest_path']} sha256={digest}")
321
- lines.append("Authority: .ultra/state.db; JSON/Markdown remain projections or evidence artifacts.")
322
- 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 = {
@@ -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,11 +65,18 @@ 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
+ ]
74
75
  report["checks"]["baseline"] = {
75
- **baseline,
76
- "readiness": baseline["status"],
77
- "status": "pass" if baseline["status"] == "pass" else "warning",
76
+ "baseline": breadcrumb.get("baseline") if breadcrumb else None,
77
+ "blockers": baseline_codes,
78
+ "readiness": "blocked" if baseline_codes else "ready",
79
+ "status": "warning" if baseline_codes else "pass",
78
80
  }
79
81
 
80
82
  incidents = [
@@ -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()
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """Subagent lifecycle tracker.
3
3
 
4
- Logs SubagentStart/Stop events to .ultra/debug/subagent-log.jsonl (project-level)
4
+ Logs SubagentStart/Stop events to .ultra/runtime/subagent-log.jsonl (project-level)
5
5
  for debugging and cost analysis.
6
6
 
7
7
  Protocol fields:
@@ -15,30 +15,26 @@ Usage:
15
15
  """
16
16
 
17
17
  import json
18
- import os
19
- import random
20
18
  import sys
21
19
  from datetime import datetime, timezone
22
20
  from pathlib import Path
23
21
 
24
- TERMINAL = {"committed", "completed", "done", "cancelled"}
22
+ from context_spine import ContextSpineError, find_root, read_breadcrumb
23
+
25
24
  MAX_LOG_LINES = 5000
26
25
 
27
26
 
28
27
  def get_log_dir(hook_input: dict):
29
- start = Path(hook_input.get("cwd") or Path.cwd()).resolve()
30
- for root in (start, *start.parents):
31
- state_file = root / ".ultra" / "workflow-state.json"
32
- if not state_file.is_file():
33
- continue
34
- try:
35
- state = json.loads(state_file.read_text(encoding="utf-8"))
36
- except (OSError, json.JSONDecodeError) as exc:
37
- print(f"[subagent_tracker] cannot read {state_file}: {exc}", file=sys.stderr)
38
- return None
39
- if isinstance(state, dict) and state.get("status") not in TERMINAL:
40
- return root / ".ultra" / "runtime"
28
+ root = find_root(Path(hook_input.get("cwd") or Path.cwd()).resolve())
29
+ if root is None:
30
+ return None
31
+ try:
32
+ breadcrumb = read_breadcrumb(root)
33
+ except ContextSpineError as exc:
34
+ print(f"[subagent_tracker] cannot inspect Context Spine: {exc}", file=sys.stderr)
41
35
  return None
36
+ if breadcrumb and breadcrumb.get("change_id"):
37
+ return root / ".ultra" / "runtime"
42
38
  return None
43
39
 
44
40
 
@@ -105,9 +101,7 @@ def main():
105
101
  except OSError as exc:
106
102
  print(f"[subagent_tracker] event write failed: {exc}", file=sys.stderr)
107
103
 
108
- # Periodic log rotation (~1% of writes)
109
- if random.random() < 0.01:
110
- rotate_log(log_file)
104
+ rotate_log(log_file)
111
105
 
112
106
  print(json.dumps({}))
113
107