ultra-builder-pro-cli 0.13.0 → 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.
- package/CHANGELOG.md +43 -1
- package/README.md +7 -8
- package/adapters/_shared/codex-assets.cjs +23 -163
- package/adapters/_shared/runtime-assets.cjs +1 -0
- package/adapters/_shared/tests/runtime-assets.test.cjs +18 -0
- package/adapters/opencode.js +30 -120
- package/adapters/tests/claude.test.cjs +3 -2
- package/adapters/tests/codex-hook.test.cjs +34 -7
- package/adapters/tests/codex.test.cjs +13 -6
- package/adapters/tests/kimi-hook.test.cjs +34 -7
- package/adapters/tests/opencode.test.cjs +70 -84
- package/docs/AGENT-CONTEXT.md +6 -3
- package/docs/ARCHITECTURE.md +8 -1
- package/docs/ROADMAP.md +1 -1
- package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
- package/hooks/active_task_context.py +22 -50
- package/hooks/context_spine.py +61 -332
- package/hooks/health_check.py +19 -17
- package/hooks/pre_stop_check.py +28 -33
- package/hooks/subagent_tracker.py +13 -19
- package/hooks/workflow_checkpoint.py +39 -47
- package/hooks/workflow_context.py +4 -9
- package/hooks/workflow_resume.py +48 -128
- package/mcp-server/breadcrumb.cjs +22 -0
- package/mcp-server/lib/prd-parser.cjs +14 -41
- package/mcp-server/lib/prd-parser.test.cjs +49 -138
- package/mcp-server/lib/project-breadcrumb.cjs +139 -0
- package/mcp-server/lib/project-breadcrumb.test.cjs +112 -0
- package/mcp-server/lib/task-expander.cjs +9 -38
- package/mcp-server/lib/task-expander.test.cjs +54 -209
- package/mcp-server/server.cjs +21 -35
- package/mcp-server/tests/server-contract.test.cjs +41 -45
- package/package.json +2 -4
- package/skills/cc-collab/SKILL.md +2 -2
- package/skills/codex-collab/SKILL.md +2 -3
- package/skills/ultra-plan/SKILL.md +15 -5
- package/skills/ultra-verify/SKILL.md +21 -30
- package/skills/ultra-verify/scripts/verify_wait.py +54 -54
- package/spec/mcp-tools.yaml +38 -10
- package/mcp-server/lib/llm-client.cjs +0 -157
- package/mcp-server/lib/llm-client.test.cjs +0 -156
- package/orchestrator/planner/expand-prompt.cjs +0 -62
- package/orchestrator/planner/expand-prompt.test.cjs +0 -52
- package/orchestrator/planner/prd-prompt.cjs +0 -44
- package/orchestrator/planner/prd-prompt.test.cjs +0 -30
package/hooks/context_spine.py
CHANGED
|
@@ -1,355 +1,84 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
2
|
+
"""Thin bridge to the single bundled JavaScript Context Spine reader."""
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
|
-
import
|
|
6
|
-
import
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
7
|
import subprocess
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return
|
|
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
|
-
|
|
140
|
-
if
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
"
|
|
310
|
-
|
|
311
|
-
"
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
"
|
|
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(
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
package/hooks/health_check.py
CHANGED
|
@@ -6,7 +6,7 @@ import sqlite3
|
|
|
6
6
|
import sys
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
|
-
from context_spine import
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
"
|
|
77
|
-
"
|
|
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 = [
|
package/hooks/pre_stop_check.py
CHANGED
|
@@ -1,26 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Report unfinished Ultra
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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__ ==
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
|