ultra-builder-pro-cli 0.25.0 → 0.26.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/.ultra-template/contexts/TEMPLATE.md +87 -0
- package/.ultra-template/decisions/.gitkeep +1 -0
- package/.ultra-template/evidence/.gitkeep +1 -0
- package/.ultra-template/north-star.md +46 -0
- package/.ultra-template/research/.gitkeep +1 -0
- package/.ultra-template/specs/discovery.md +2 -2
- package/.ultra-template/tasks.json +3 -0
- package/.ultra-template/test-report.json +27 -0
- package/CHANGELOG.md +49 -0
- package/README.md +217 -579
- package/adapters/_shared/delegate-profiles/kimi-read-only.md +15 -0
- package/adapters/_shared/delegate-profiles/kimi-write.md +17 -0
- package/adapters/_shared/file-ops.cjs +76 -2
- package/adapters/_shared/host-profile.cjs +97 -0
- package/adapters/_shared/plugin-core.cjs +204 -0
- package/adapters/_shared/runtime-assets.cjs +31 -96
- package/adapters/claude.js +92 -291
- package/adapters/codex.js +309 -842
- package/adapters/grok.js +143 -1469
- package/adapters/kimi.js +124 -1053
- package/adapters/opencode.js +230 -742
- package/bin/delegate-worker.cjs +323 -0
- package/bin/delegate.cjs +456 -0
- package/bin/install.js +38 -11
- package/docs/ARCHITECTURE.md +122 -216
- package/docs/ARTIFACT-AUTHORITY.md +90 -268
- package/docs/DECISIONS.md +75 -87
- package/docs/PHILOSOPHY.md +177 -0
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +62 -39
- package/docs/RUNTIME-COMPAT-MATRIX.md +99 -156
- package/docs/SKILL-AUTHORING.md +94 -73
- package/docs/WORKFLOW-LIFECYCLE.md +93 -189
- package/hooks/README.md +17 -37
- package/hooks/_common.py +113 -0
- package/hooks/adapters/codex.py +9 -16
- package/hooks/adapters/grok.py +5 -5
- package/hooks/adapters/kimi.py +8 -14
- package/hooks/block_dangerous_commands.py +95 -0
- package/hooks/compact_context.py +65 -0
- package/hooks/mid_workflow_recall.py +25 -0
- package/hooks/post_edit_guard.py +105 -0
- package/hooks/session_context.py +30 -0
- package/package.json +17 -52
- package/skills/ultra-change/SKILL.md +97 -50
- package/skills/ultra-change/references/change-contract.md +40 -1
- package/skills/ultra-delegate/SKILL.md +82 -0
- package/skills/ultra-delegate/references/delegation-contract.md +51 -0
- package/skills/ultra-delegate/scripts/delegate_wait.py +86 -0
- package/skills/ultra-deliver/SKILL.md +61 -42
- package/skills/ultra-deliver/references/baseline-reconciliation.md +39 -20
- package/skills/ultra-dev/SKILL.md +96 -62
- package/skills/ultra-dev/references/debugging.md +22 -0
- package/skills/ultra-domain-modeling/SKILL.md +79 -0
- package/skills/ultra-grilling/SKILL.md +79 -0
- package/skills/ultra-init/SKILL.md +94 -37
- package/skills/ultra-init/references/brownfield-adoption.md +33 -83
- package/skills/ultra-init/scripts/init_project.cjs +84 -0
- package/skills/ultra-plan/SKILL.md +88 -68
- package/skills/ultra-research/SKILL.md +62 -48
- package/skills/ultra-research/references/00-problem-validation.md +5 -5
- package/skills/ultra-research/references/01-opportunity-discovery.md +3 -3
- package/skills/ultra-research/references/02-market-assessment.md +5 -4
- package/skills/ultra-research/references/03-competitive-landscape.md +3 -3
- package/skills/ultra-research/references/04-product-strategy.md +3 -3
- package/skills/ultra-research/references/05-assumptions-validation.md +5 -5
- package/skills/ultra-research/references/10-user-personas.md +3 -2
- package/skills/ultra-research/references/11-user-scenarios.md +3 -3
- package/skills/ultra-research/references/20-user-stories.md +4 -3
- package/skills/ultra-research/references/21-features-scope.md +3 -2
- package/skills/ultra-research/references/22-success-metrics.md +3 -3
- package/skills/ultra-research/references/30-architecture-context.md +3 -3
- package/skills/ultra-research/references/31-solution-strategy.md +3 -3
- package/skills/ultra-research/references/32-building-blocks.md +3 -3
- package/skills/ultra-research/references/40-deployment.md +3 -3
- package/skills/ultra-research/references/41-quality-risks.md +3 -3
- package/skills/ultra-research/references/99-synthesis.md +7 -7
- package/skills/ultra-review/SKILL.md +59 -32
- package/skills/ultra-review/references/code.md +17 -0
- package/skills/ultra-review/references/comments.md +12 -0
- package/skills/ultra-review/references/design.md +12 -0
- package/skills/ultra-review/references/errors.md +13 -0
- package/skills/ultra-review/references/spec.md +13 -0
- package/skills/ultra-review/references/tests.md +12 -0
- package/skills/ultra-review/references/worker-packet.md +40 -0
- package/skills/ultra-review/scripts/review_wait.py +26 -15
- package/skills/ultra-status/SKILL.md +61 -36
- package/skills/ultra-tdd/SKILL.md +81 -0
- package/skills/ultra-tdd/references/templates/README.md +20 -0
- package/skills/ultra-tdd/references/templates/feature-flag-default-audit.sh +79 -0
- package/skills/ultra-tdd/references/templates/persistence-real.ts +78 -0
- package/skills/ultra-tdd/references/templates/testcontainer-postgres.py +78 -0
- package/skills/ultra-tdd/references/templates/testcontainer-postgres.ts +73 -0
- package/skills/ultra-tdd/references/templates/vertical-slice.ts +66 -0
- package/skills/ultra-tdd/references/test-execution.md +18 -0
- package/skills/ultra-test/SKILL.md +70 -30
- package/skills/ultra-test/references/export-syntax.md +18 -0
- package/skills/ultra-test/scripts/worktree_digest.cjs +54 -0
- package/skills/ultra-think/SKILL.md +49 -27
- package/skills/ultra-think/references/autonomy-boundary.md +48 -0
- package/skills/ultra-think/references/interaction-boundary.md +18 -33
- package/.ultra-template/docs/research/README.md +0 -55
- package/.ultra-template/reports/templates/delivery-report.json +0 -17
- package/.ultra-template/reports/templates/test-report.json +0 -18
- package/.ultra-template/tasks/tasks.json +0 -15
- package/.ultra-template/templates/task-context.md +0 -19
- package/adapters/_shared/codex-assets.cjs +0 -725
- package/adapters/_shared/codex-ultra-tools-entry.cjs +0 -11
- package/adapters/_shared/interaction-contract.cjs +0 -129
- package/adapters/_shared/path-rewrite.cjs +0 -79
- package/adapters/_shared/runtime-native.cjs +0 -132
- package/adapters/_shared/settings-merge.cjs +0 -76
- package/agents/code-reviewer.md +0 -42
- package/agents/debugger.md +0 -40
- package/agents/review-code.md +0 -49
- package/agents/review-comments.md +0 -38
- package/agents/review-coordinator.md +0 -56
- package/agents/review-design.md +0 -40
- package/agents/review-errors.md +0 -43
- package/agents/review-spec.md +0 -39
- package/agents/review-tests.md +0 -40
- package/agents/tdd-runner.md +0 -38
- package/bin/orchestrator.js +0 -383
- package/commands/ultra-change.md +0 -10
- package/commands/ultra-deliver.md +0 -10
- package/commands/ultra-dev.md +0 -10
- package/commands/ultra-doctor.md +0 -10
- package/commands/ultra-init.md +0 -10
- package/commands/ultra-plan.md +0 -10
- package/commands/ultra-research.md +0 -10
- package/commands/ultra-review.md +0 -10
- package/commands/ultra-status.md +0 -10
- package/commands/ultra-test.md +0 -10
- package/commands/ultra-think.md +0 -10
- package/docs/AGENT-CONTEXT.md +0 -191
- package/docs/COMMIT-HASH-BACKFILL.md +0 -51
- package/docs/LEGACY-CLI-CROSSWALK.md +0 -175
- package/docs/ROADMAP.md +0 -101
- package/docs/STATE-DB-ACCESS-POLICY.md +0 -161
- package/hooks/active_task_context.py +0 -152
- package/hooks/context_envelope.py +0 -100
- package/hooks/health_check.py +0 -196
- package/hooks/pre_stop_check.py +0 -66
- package/hooks/runtime_paths.py +0 -396
- package/hooks/subagent_tracker.py +0 -88
- package/hooks/workflow_checkpoint.py +0 -76
- package/hooks/workflow_context.py +0 -48
- package/hooks/workflow_resume.py +0 -84
- package/mcp-server/hook-context.cjs +0 -38
- package/mcp-server/hook-event.cjs +0 -34
- package/mcp-server/lib/archive-journal.cjs +0 -1119
- package/mcp-server/lib/archive-mutation-worker.py +0 -365
- package/mcp-server/lib/artifact-registry.cjs +0 -1232
- package/mcp-server/lib/baseline-workflow.cjs +0 -1193
- package/mcp-server/lib/canonical-json.cjs +0 -19
- package/mcp-server/lib/change-authority.cjs +0 -26
- package/mcp-server/lib/change-packet.cjs +0 -790
- package/mcp-server/lib/change-workflow.cjs +0 -65
- package/mcp-server/lib/context-envelope.cjs +0 -581
- package/mcp-server/lib/context-paths.cjs +0 -142
- package/mcp-server/lib/decision-records.cjs +0 -423
- package/mcp-server/lib/delivery-transaction.cjs +0 -471
- package/mcp-server/lib/doctor-backup-worker.cjs +0 -328
- package/mcp-server/lib/doctor.cjs +0 -627
- package/mcp-server/lib/git-bootstrap.cjs +0 -651
- package/mcp-server/lib/init-project.cjs +0 -1021
- package/mcp-server/lib/kernel-change-workflow.cjs +0 -2029
- package/mcp-server/lib/legacy-change-workflow.cjs +0 -3032
- package/mcp-server/lib/managed-file-write.cjs +0 -172
- package/mcp-server/lib/plan-checkpoint.cjs +0 -177
- package/mcp-server/lib/plan-publication-worker.cjs +0 -830
- package/mcp-server/lib/plan-store.cjs +0 -794
- package/mcp-server/lib/prd-parser.cjs +0 -83
- package/mcp-server/lib/pricing.cjs +0 -44
- package/mcp-server/lib/project-context.cjs +0 -178
- package/mcp-server/lib/projector.cjs +0 -386
- package/mcp-server/lib/provider-refs.cjs +0 -42
- package/mcp-server/lib/runtime-paths.cjs +0 -1687
- package/mcp-server/lib/runtime-state.cjs +0 -222
- package/mcp-server/lib/safe-project-file.cjs +0 -385
- package/mcp-server/lib/stage-checkpoints.cjs +0 -463
- package/mcp-server/lib/state-authority.cjs +0 -95
- package/mcp-server/lib/state-db.cjs +0 -1580
- package/mcp-server/lib/state-ops.cjs +0 -1428
- package/mcp-server/lib/task-contract.cjs +0 -404
- package/mcp-server/lib/task-expander.cjs +0 -105
- package/mcp-server/lib/task-ledger.cjs +0 -1964
- package/mcp-server/lib/telemetry.cjs +0 -85
- package/mcp-server/lib/topo.cjs +0 -131
- package/mcp-server/lib/ultra-facade.cjs +0 -2684
- package/mcp-server/lib/worker-packet.cjs +0 -358
- package/mcp-server/server.cjs +0 -437
- package/orchestrator/auto-merge.cjs +0 -128
- package/orchestrator/daemon.cjs +0 -264
- package/orchestrator/dispatch-rules.cjs +0 -78
- package/orchestrator/parallel-orchestrator.cjs +0 -903
- package/orchestrator/planner/plan-builder.cjs +0 -125
- package/orchestrator/recovery.cjs +0 -188
- package/orchestrator/session-close-journal-worker.cjs +0 -5
- package/orchestrator/session-close-journal.cjs +0 -1014
- package/orchestrator/session-runner.cjs +0 -1153
- package/orchestrator/worktree-manager.cjs +0 -282
- package/settings.json +0 -7
- package/skills/cc-collab/SKILL.md +0 -42
- package/skills/code-review-expert/SKILL.md +0 -43
- package/skills/code-review-expert/references/correctness-reliability.md +0 -30
- package/skills/code-review-expert/references/design-boundaries.md +0 -26
- package/skills/code-review-expert/references/removal-plan.md +0 -24
- package/skills/codex-collab/SKILL.md +0 -49
- package/skills/codex-collab/evals/evals.json +0 -35
- package/skills/integration-rules/SKILL.md +0 -32
- package/skills/security-rules/SKILL.md +0 -38
- package/skills/testing-rules/SKILL.md +0 -32
- package/skills/ultra-doctor/SKILL.md +0 -41
- package/skills/ultra-plan/references/semantic-preflight.md +0 -20
- package/skills/ultra-verify/SKILL.md +0 -46
- package/skills/ultra-verify/evals/evals.json +0 -46
- package/skills/ultra-verify/references/cross-verify-modes.md +0 -28
- package/skills/ultra-verify/references/evidence-status.md +0 -21
- package/skills/ultra-verify/scripts/verify_wait.py +0 -94
- package/spec/cli-protocol.md +0 -102
- package/spec/mcp-tools.yaml +0 -312
- package/spec/schemas/baseline-reconciliation.v1.schema.json +0 -73
- package/spec/schemas/change-delta.v1.schema.json +0 -153
- package/spec/schemas/command-manifest.schema.json +0 -29
- package/spec/schemas/context-file.v4.5.schema.json +0 -54
- package/spec/schemas/delivery-report.v1.schema.json +0 -66
- package/spec/schemas/documentation-reconciliation.v1.schema.json +0 -106
- package/spec/schemas/mcp-tools.schema.json +0 -136
- package/spec/schemas/skill-manifest.schema.json +0 -24
- package/spec/schemas/state-db.sql +0 -641
- package/spec/schemas/task-ledger.v2.schema.json +0 -296
- package/spec/schemas/tasks.v4.5.schema.json +0 -96
- package/spec/schemas/test-report.v1.schema.json +0 -225
- package/ultra-tools/cli.cjs +0 -129
- package/ultra-tools/commands/db.cjs +0 -226
- package/ultra-tools/commands/legacy-memory.cjs +0 -159
- package/ultra-tools/commands/migrate.cjs +0 -1386
- package/ultra-tools/commands/session.cjs +0 -213
- package/ultra-tools/commands/status.cjs +0 -559
- package/ultra-tools/commands/system.cjs +0 -498
- package/ultra-tools/commands/task.cjs +0 -105
package/hooks/runtime_paths.py
DELETED
|
@@ -1,396 +0,0 @@
|
|
|
1
|
-
"""Read-only canonical project and state-path resolution for Ultra hooks."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import os
|
|
5
|
-
import sqlite3
|
|
6
|
-
import subprocess
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class RuntimePathError(RuntimeError):
|
|
11
|
-
"""Ultra runtime paths are invalid or have competing authorities."""
|
|
12
|
-
|
|
13
|
-
STATE_TOMBSTONE_MARKER = "MIGRATED_TO_RUNTIME.json"
|
|
14
|
-
STATE_TOMBSTONE_KIND = "ultra-state-migration-tombstone"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def _absolute(path: Path) -> Path:
|
|
18
|
-
expanded = path.expanduser()
|
|
19
|
-
return expanded if expanded.is_absolute() else Path.cwd() / expanded
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def _assert_root(root: Path, *, explicit: bool) -> Path:
|
|
23
|
-
absolute = _absolute(root)
|
|
24
|
-
try:
|
|
25
|
-
stat = absolute.lstat()
|
|
26
|
-
except FileNotFoundError as error:
|
|
27
|
-
raise RuntimePathError(f"UBP_ROOT_DIR does not exist: {absolute}") from error
|
|
28
|
-
if (explicit and absolute.is_symlink()) or not absolute.is_dir():
|
|
29
|
-
raise RuntimePathError(
|
|
30
|
-
f"UBP_ROOT_DIR must be a real directory, not a symlink or special entry: {absolute}"
|
|
31
|
-
)
|
|
32
|
-
return absolute.resolve()
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def _assert_directory(path: Path, label: str, *, allow_runtime_link_to: Path | None = None) -> None:
|
|
36
|
-
try:
|
|
37
|
-
path.lstat()
|
|
38
|
-
except FileNotFoundError:
|
|
39
|
-
return
|
|
40
|
-
if path.is_symlink():
|
|
41
|
-
if allow_runtime_link_to is not None:
|
|
42
|
-
try:
|
|
43
|
-
if (
|
|
44
|
-
path.resolve(strict=True) == allow_runtime_link_to.parent.resolve(strict=True)
|
|
45
|
-
and (path / "state.db").resolve(strict=True)
|
|
46
|
-
== allow_runtime_link_to.resolve(strict=True)
|
|
47
|
-
):
|
|
48
|
-
return
|
|
49
|
-
except FileNotFoundError:
|
|
50
|
-
pass
|
|
51
|
-
raise RuntimePathError(f"{label} may not be a symlink: {path}")
|
|
52
|
-
if not path.is_dir():
|
|
53
|
-
raise RuntimePathError(f"{label} must be a directory: {path}")
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def _is_managed_legacy_tombstone(main: Path) -> bool:
|
|
57
|
-
try:
|
|
58
|
-
stat = main.lstat()
|
|
59
|
-
except FileNotFoundError:
|
|
60
|
-
return False
|
|
61
|
-
if main.is_symlink():
|
|
62
|
-
return False
|
|
63
|
-
marker = main / STATE_TOMBSTONE_MARKER if main.is_dir() else main
|
|
64
|
-
try:
|
|
65
|
-
if main.is_dir():
|
|
66
|
-
if [entry.name for entry in main.iterdir()] != [STATE_TOMBSTONE_MARKER]:
|
|
67
|
-
return False
|
|
68
|
-
elif not main.is_file():
|
|
69
|
-
return False
|
|
70
|
-
marker_stat = marker.lstat()
|
|
71
|
-
if marker.is_symlink() or not marker_stat.st_mode:
|
|
72
|
-
return False
|
|
73
|
-
payload = json.loads(marker.read_text(encoding="utf-8"))
|
|
74
|
-
except (FileNotFoundError, OSError, ValueError, json.JSONDecodeError):
|
|
75
|
-
return False
|
|
76
|
-
canonical = (main.parent / str(payload.get("canonical_state_db", ""))).resolve()
|
|
77
|
-
expected = (main.parent / ".runtime" / "state.db").resolve()
|
|
78
|
-
return (
|
|
79
|
-
payload.get("version") == 1
|
|
80
|
-
and payload.get("kind") == STATE_TOMBSTONE_KIND
|
|
81
|
-
and marker.is_file()
|
|
82
|
-
and canonical == expected
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def _assert_state_set(
|
|
87
|
-
main: Path, label: str, *, allow_managed_tombstone: bool = False
|
|
88
|
-
) -> None:
|
|
89
|
-
present: dict[str, bool] = {}
|
|
90
|
-
for suffix in ("", "-wal", "-shm"):
|
|
91
|
-
candidate = Path(f"{main}{suffix}")
|
|
92
|
-
try:
|
|
93
|
-
candidate.lstat()
|
|
94
|
-
except FileNotFoundError:
|
|
95
|
-
present[suffix] = False
|
|
96
|
-
continue
|
|
97
|
-
tombstone = (
|
|
98
|
-
suffix == ""
|
|
99
|
-
and allow_managed_tombstone
|
|
100
|
-
and _is_managed_legacy_tombstone(candidate)
|
|
101
|
-
)
|
|
102
|
-
if not tombstone and (candidate.is_symlink() or not candidate.is_file()):
|
|
103
|
-
raise RuntimePathError(
|
|
104
|
-
f"{label} state entry must be a regular file and may not be a symlink: {candidate}"
|
|
105
|
-
)
|
|
106
|
-
present[suffix] = not tombstone
|
|
107
|
-
if not present[""] and (present["-wal"] or present["-shm"]):
|
|
108
|
-
raise RuntimePathError(f"{label} SQLite sidecar exists without state.db authority")
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def _physical_state_paths(root: Path) -> tuple[Path, Path]:
|
|
112
|
-
resolved = root.resolve()
|
|
113
|
-
return (
|
|
114
|
-
resolved / ".ultra" / ".runtime" / "state.db",
|
|
115
|
-
resolved / ".ultra" / "state.db",
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def _same_physical_authority(left: Path, right: Path) -> bool:
|
|
120
|
-
if left == right:
|
|
121
|
-
return True
|
|
122
|
-
try:
|
|
123
|
-
return left.resolve(strict=True) == right.resolve(strict=True)
|
|
124
|
-
except FileNotFoundError:
|
|
125
|
-
return False
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def _is_task_authority_link(
|
|
129
|
-
runtime: Path, candidate: Path, physical_runtime: Path, runtime_state: Path
|
|
130
|
-
) -> bool:
|
|
131
|
-
try:
|
|
132
|
-
parts = candidate.relative_to(runtime).parts
|
|
133
|
-
except ValueError:
|
|
134
|
-
return False
|
|
135
|
-
if (
|
|
136
|
-
len(parts) != 4
|
|
137
|
-
or parts[0] != "worktrees"
|
|
138
|
-
or not parts[1]
|
|
139
|
-
or len(parts[1]) > 128
|
|
140
|
-
or not parts[1][0].isalnum()
|
|
141
|
-
or any(not (char.isalnum() or char in "._-") for char in parts[1])
|
|
142
|
-
or parts[2:] != (".ultra", ".runtime")
|
|
143
|
-
):
|
|
144
|
-
return False
|
|
145
|
-
try:
|
|
146
|
-
return (
|
|
147
|
-
candidate.resolve(strict=True) == physical_runtime
|
|
148
|
-
and (candidate / "state.db").resolve(strict=True)
|
|
149
|
-
== runtime_state.resolve(strict=True)
|
|
150
|
-
)
|
|
151
|
-
except FileNotFoundError:
|
|
152
|
-
return False
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
def _has_git_metadata(root: Path) -> bool:
|
|
156
|
-
for candidate in (root, *root.parents):
|
|
157
|
-
if (candidate / ".git").exists() or (candidate / ".git").is_symlink():
|
|
158
|
-
return True
|
|
159
|
-
return False
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def _registered_worktrees(root: Path) -> set[Path]:
|
|
163
|
-
probe = subprocess.run(
|
|
164
|
-
["git", "rev-parse", "--is-inside-work-tree"],
|
|
165
|
-
cwd=root,
|
|
166
|
-
text=True,
|
|
167
|
-
capture_output=True,
|
|
168
|
-
check=False,
|
|
169
|
-
)
|
|
170
|
-
if probe.returncode != 0 or probe.stdout.strip() != "true":
|
|
171
|
-
detail = (probe.stderr or probe.stdout or "unknown Git probe failure").strip()
|
|
172
|
-
if not _has_git_metadata(root) and "not a git repository" in detail.lower():
|
|
173
|
-
return set()
|
|
174
|
-
raise RuntimePathError(f"cannot classify Git worktree authority: {detail}")
|
|
175
|
-
result = subprocess.run(
|
|
176
|
-
["git", "worktree", "list", "--porcelain"],
|
|
177
|
-
cwd=root,
|
|
178
|
-
text=True,
|
|
179
|
-
capture_output=True,
|
|
180
|
-
check=False,
|
|
181
|
-
)
|
|
182
|
-
if result.returncode != 0:
|
|
183
|
-
detail = (result.stderr or result.stdout or "unknown Git probe failure").strip()
|
|
184
|
-
raise RuntimePathError(f"cannot inspect registered Git worktrees: {detail}")
|
|
185
|
-
registered: set[Path] = set()
|
|
186
|
-
for line in result.stdout.splitlines():
|
|
187
|
-
if line.startswith("worktree "):
|
|
188
|
-
registered.add(Path(line.removeprefix("worktree ")).resolve())
|
|
189
|
-
return registered
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
def _assert_registered_worktree_boundary(
|
|
193
|
-
runtime: Path, candidate: Path, physical_runtime: Path
|
|
194
|
-
) -> None:
|
|
195
|
-
sid = candidate.name
|
|
196
|
-
if (
|
|
197
|
-
not sid
|
|
198
|
-
or len(sid) > 128
|
|
199
|
-
or not sid[0].isalnum()
|
|
200
|
-
or any(not (char.isalnum() or char in "._-") for char in sid)
|
|
201
|
-
):
|
|
202
|
-
raise RuntimePathError(f"registered worktree has an unsafe session id: {candidate}")
|
|
203
|
-
authority_link = candidate / ".ultra" / ".runtime"
|
|
204
|
-
if (
|
|
205
|
-
not authority_link.is_symlink()
|
|
206
|
-
or not _is_task_authority_link(
|
|
207
|
-
runtime, authority_link, physical_runtime, runtime / "state.db"
|
|
208
|
-
)
|
|
209
|
-
):
|
|
210
|
-
raise RuntimePathError(
|
|
211
|
-
"registered worktree must contain the exact central Ultra "
|
|
212
|
-
f"authority link: {authority_link}"
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
def _assert_runtime_tree(root: Path, runtime: Path) -> None:
|
|
217
|
-
try:
|
|
218
|
-
runtime.lstat()
|
|
219
|
-
except FileNotFoundError:
|
|
220
|
-
return
|
|
221
|
-
try:
|
|
222
|
-
physical_runtime = runtime.resolve(strict=True)
|
|
223
|
-
except FileNotFoundError as error:
|
|
224
|
-
raise RuntimePathError(
|
|
225
|
-
f"canonical runtime root cannot be resolved: {runtime}"
|
|
226
|
-
) from error
|
|
227
|
-
pending = list(runtime.iterdir())
|
|
228
|
-
runtime_state = runtime / "state.db"
|
|
229
|
-
registered: set[Path] | None = None
|
|
230
|
-
while pending:
|
|
231
|
-
candidate = pending.pop()
|
|
232
|
-
try:
|
|
233
|
-
candidate.lstat()
|
|
234
|
-
except FileNotFoundError:
|
|
235
|
-
continue
|
|
236
|
-
if candidate.is_symlink():
|
|
237
|
-
if _is_task_authority_link(
|
|
238
|
-
runtime, candidate, physical_runtime, runtime_state
|
|
239
|
-
):
|
|
240
|
-
continue
|
|
241
|
-
raise RuntimePathError(
|
|
242
|
-
f"canonical runtime entry may not be a symlink: {candidate}"
|
|
243
|
-
)
|
|
244
|
-
if not candidate.is_dir() and not candidate.is_file():
|
|
245
|
-
raise RuntimePathError(
|
|
246
|
-
"canonical runtime entry must be a regular file or real "
|
|
247
|
-
f"directory: {candidate}"
|
|
248
|
-
)
|
|
249
|
-
try:
|
|
250
|
-
physical_candidate = candidate.resolve(strict=True)
|
|
251
|
-
physical_candidate.relative_to(physical_runtime)
|
|
252
|
-
except (FileNotFoundError, ValueError) as error:
|
|
253
|
-
raise RuntimePathError(
|
|
254
|
-
f"canonical runtime entry escapes its physical root: {candidate}"
|
|
255
|
-
) from error
|
|
256
|
-
try:
|
|
257
|
-
relative_parts = candidate.relative_to(runtime).parts
|
|
258
|
-
except ValueError:
|
|
259
|
-
relative_parts = ()
|
|
260
|
-
if (
|
|
261
|
-
candidate.is_dir()
|
|
262
|
-
and len(relative_parts) == 2
|
|
263
|
-
and relative_parts[0] == "worktrees"
|
|
264
|
-
):
|
|
265
|
-
if registered is None:
|
|
266
|
-
registered = _registered_worktrees(root)
|
|
267
|
-
if physical_candidate in registered:
|
|
268
|
-
_assert_registered_worktree_boundary(
|
|
269
|
-
runtime, candidate, physical_runtime
|
|
270
|
-
)
|
|
271
|
-
continue
|
|
272
|
-
if candidate.is_dir():
|
|
273
|
-
pending.extend(candidate.iterdir())
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
def _project_root_from_state_db(configured: Path) -> Path:
|
|
277
|
-
if (
|
|
278
|
-
configured.name == "state.db"
|
|
279
|
-
and configured.parent.name == ".runtime"
|
|
280
|
-
and configured.parent.parent.name == ".ultra"
|
|
281
|
-
):
|
|
282
|
-
return configured.parent.parent.parent
|
|
283
|
-
raise RuntimePathError(
|
|
284
|
-
f"configured DB is not a canonical project runtime authority: {configured}"
|
|
285
|
-
)
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
def _assert_task_authority_binding(root: Path, configured: Path) -> None:
|
|
289
|
-
try:
|
|
290
|
-
authority_root = _project_root_from_state_db(configured).resolve(strict=True)
|
|
291
|
-
canonical_db = authority_root / ".ultra" / ".runtime" / "state.db"
|
|
292
|
-
if configured.resolve(strict=True) != canonical_db.resolve(strict=True):
|
|
293
|
-
raise RuntimePathError("configured DB is not the canonical authority")
|
|
294
|
-
task_root = root.resolve(strict=True)
|
|
295
|
-
worktrees_root = (
|
|
296
|
-
authority_root / ".ultra" / ".runtime" / "worktrees"
|
|
297
|
-
).resolve(strict=True)
|
|
298
|
-
relative = task_root.relative_to(worktrees_root)
|
|
299
|
-
if len(relative.parts) != 1:
|
|
300
|
-
raise RuntimePathError("task root is not a direct managed worktree child")
|
|
301
|
-
sid = relative.parts[0]
|
|
302
|
-
if task_root not in _registered_worktrees(authority_root):
|
|
303
|
-
raise RuntimePathError("task root is not a registered Git worktree")
|
|
304
|
-
runtime_link = root / ".ultra" / ".runtime"
|
|
305
|
-
if (
|
|
306
|
-
not runtime_link.is_symlink()
|
|
307
|
-
or runtime_link.resolve(strict=True) != canonical_db.parent.resolve(strict=True)
|
|
308
|
-
):
|
|
309
|
-
raise RuntimePathError("task authority link does not resolve to the configured DB")
|
|
310
|
-
uri = f"{configured.resolve(strict=True).as_uri()}?mode=ro"
|
|
311
|
-
with sqlite3.connect(uri, uri=True) as connection:
|
|
312
|
-
row = connection.execute(
|
|
313
|
-
"SELECT worktree_path FROM sessions WHERE sid = ?", (sid,)
|
|
314
|
-
).fetchone()
|
|
315
|
-
if row is None:
|
|
316
|
-
raise RuntimePathError(f"authority has no session binding for {sid}")
|
|
317
|
-
if Path(row[0]).resolve(strict=True) != task_root:
|
|
318
|
-
raise RuntimePathError(f"session {sid} is bound to a different worktree")
|
|
319
|
-
except (FileNotFoundError, ValueError, sqlite3.Error) as error:
|
|
320
|
-
raise RuntimePathError(
|
|
321
|
-
f"task root is not bound to the configured Ultra authority: {error}"
|
|
322
|
-
) from error
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
def validate_project_layout(
|
|
326
|
-
root: Path, *, validate_runtime_tree: bool = False
|
|
327
|
-
) -> tuple[Path, Path, Path | None]:
|
|
328
|
-
runtime, legacy = _physical_state_paths(root)
|
|
329
|
-
configured_value = os.environ.get("UBP_DB_PATH", "").strip()
|
|
330
|
-
configured = _absolute(Path(configured_value)) if configured_value else None
|
|
331
|
-
if configured is not None:
|
|
332
|
-
_assert_state_set(configured, "configured")
|
|
333
|
-
_assert_directory(root / ".ultra", ".ultra")
|
|
334
|
-
_assert_directory(
|
|
335
|
-
root / ".ultra" / ".runtime",
|
|
336
|
-
".ultra/.runtime",
|
|
337
|
-
allow_runtime_link_to=configured,
|
|
338
|
-
)
|
|
339
|
-
_assert_state_set(runtime, "runtime")
|
|
340
|
-
_assert_state_set(legacy, "legacy", allow_managed_tombstone=True)
|
|
341
|
-
if validate_runtime_tree:
|
|
342
|
-
_assert_runtime_tree(root, root.resolve() / ".ultra" / ".runtime")
|
|
343
|
-
return runtime, legacy, configured
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
def _assert_no_competing_state(root: Path) -> tuple[Path, Path]:
|
|
347
|
-
runtime, legacy, configured = validate_project_layout(root)
|
|
348
|
-
legacy_active = legacy.is_file() and not _is_managed_legacy_tombstone(legacy)
|
|
349
|
-
if runtime.is_file() and legacy_active:
|
|
350
|
-
raise RuntimePathError(
|
|
351
|
-
"both legacy .ultra/state.db and runtime "
|
|
352
|
-
".ultra/.runtime/state.db exist; refusing to choose either authority"
|
|
353
|
-
)
|
|
354
|
-
if configured is not None:
|
|
355
|
-
active = runtime if runtime.is_file() else (legacy if legacy_active else None)
|
|
356
|
-
allowed = (
|
|
357
|
-
_same_physical_authority(configured, active)
|
|
358
|
-
if active is not None
|
|
359
|
-
else configured == runtime
|
|
360
|
-
)
|
|
361
|
-
if not allowed:
|
|
362
|
-
raise RuntimePathError(
|
|
363
|
-
"UBP_DB_PATH does not name this project's canonical or "
|
|
364
|
-
f"task-linked authority: {configured}"
|
|
365
|
-
)
|
|
366
|
-
if (root / ".ultra" / ".runtime").is_symlink():
|
|
367
|
-
_assert_task_authority_binding(root, configured)
|
|
368
|
-
return runtime, legacy
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
def state_db_path(root: Path) -> Path:
|
|
372
|
-
"""Return configured, runtime, or legacy state without mutating the project."""
|
|
373
|
-
runtime, legacy = _assert_no_competing_state(root)
|
|
374
|
-
configured = os.environ.get("UBP_DB_PATH", "").strip()
|
|
375
|
-
if configured:
|
|
376
|
-
return _absolute(Path(configured)).resolve()
|
|
377
|
-
if runtime.is_file():
|
|
378
|
-
return runtime
|
|
379
|
-
if legacy.is_file() and not _is_managed_legacy_tombstone(legacy):
|
|
380
|
-
return legacy
|
|
381
|
-
return runtime
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
def find_project_root(start: Path) -> Path | None:
|
|
385
|
-
"""Resolve one project root without letting UBP_DB_PATH invent a root."""
|
|
386
|
-
configured_root = os.environ.get("UBP_ROOT_DIR", "").strip()
|
|
387
|
-
if configured_root:
|
|
388
|
-
root = _assert_root(Path(configured_root), explicit=True)
|
|
389
|
-
return root if state_db_path(root).is_file() else None
|
|
390
|
-
|
|
391
|
-
current = _assert_root(start, explicit=False)
|
|
392
|
-
for root in (current, *current.parents):
|
|
393
|
-
runtime, legacy = _assert_no_competing_state(root)
|
|
394
|
-
if runtime.is_file() or legacy.is_file():
|
|
395
|
-
return root
|
|
396
|
-
return None
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Append minimal Ultra subagent lifecycle metadata to state.db events.
|
|
3
|
-
|
|
4
|
-
The hook never stores prompts, messages, transcript paths, or a parallel JSONL log.
|
|
5
|
-
It is advisory: a failed lifecycle event is reported on stderr but never blocks the
|
|
6
|
-
host's subagent operation.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
import json
|
|
10
|
-
import os
|
|
11
|
-
import shutil
|
|
12
|
-
import subprocess
|
|
13
|
-
import sys
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
|
|
16
|
-
from context_envelope import find_root_for_hook
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def helper_path() -> Path:
|
|
20
|
-
hook_root = Path(__file__).resolve().parent
|
|
21
|
-
installed = hook_root.parent / "runtime" / "hook-event.cjs"
|
|
22
|
-
if installed.is_file():
|
|
23
|
-
return installed
|
|
24
|
-
source = hook_root.parent / "mcp-server" / "hook-event.cjs"
|
|
25
|
-
if source.is_file():
|
|
26
|
-
return source
|
|
27
|
-
raise RuntimeError("bundled hook lifecycle helper is missing")
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def node_binary() -> str:
|
|
31
|
-
configured = os.environ.get("UBP_NODE")
|
|
32
|
-
if configured:
|
|
33
|
-
return configured
|
|
34
|
-
discovered = shutil.which("node")
|
|
35
|
-
if discovered:
|
|
36
|
-
return discovered
|
|
37
|
-
raise RuntimeError("node executable is unavailable")
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def append_event(root: Path, action: str, hook_input: dict) -> None:
|
|
41
|
-
payload = {
|
|
42
|
-
"agent_id": hook_input.get("agent_id", "unknown"),
|
|
43
|
-
"agent_type": hook_input.get("agent_type", "unknown"),
|
|
44
|
-
"session_id": hook_input.get("session_id", ""),
|
|
45
|
-
}
|
|
46
|
-
result = subprocess.run(
|
|
47
|
-
[node_binary(), str(helper_path()), str(root.resolve()), action],
|
|
48
|
-
input=json.dumps(payload),
|
|
49
|
-
text=True,
|
|
50
|
-
capture_output=True,
|
|
51
|
-
check=False,
|
|
52
|
-
timeout=5,
|
|
53
|
-
)
|
|
54
|
-
if result.returncode != 0:
|
|
55
|
-
detail = result.stderr.strip() or f"exit {result.returncode}"
|
|
56
|
-
raise RuntimeError(detail)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def main() -> None:
|
|
60
|
-
action = sys.argv[1] if len(sys.argv) > 1 else ""
|
|
61
|
-
if action not in ("start", "stop"):
|
|
62
|
-
print(json.dumps({}))
|
|
63
|
-
return
|
|
64
|
-
|
|
65
|
-
try:
|
|
66
|
-
raw = sys.stdin.read()
|
|
67
|
-
hook_input = json.loads(raw) if raw.strip() else {}
|
|
68
|
-
if not isinstance(hook_input, dict):
|
|
69
|
-
hook_input = {}
|
|
70
|
-
except (json.JSONDecodeError, EOFError) as exc:
|
|
71
|
-
print(f"[subagent_tracker] invalid hook input: {exc}", file=sys.stderr)
|
|
72
|
-
hook_input = {}
|
|
73
|
-
|
|
74
|
-
root = find_root_for_hook(
|
|
75
|
-
Path(hook_input.get("cwd") or Path.cwd()).resolve(),
|
|
76
|
-
"subagent_tracker",
|
|
77
|
-
)
|
|
78
|
-
if root is not None:
|
|
79
|
-
try:
|
|
80
|
-
append_event(root, action, hook_input)
|
|
81
|
-
except (OSError, RuntimeError, subprocess.TimeoutExpired) as exc:
|
|
82
|
-
print(f"[subagent_tracker] lifecycle event unavailable: {exc}", file=sys.stderr)
|
|
83
|
-
|
|
84
|
-
print(json.dumps({}))
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if __name__ == "__main__":
|
|
88
|
-
main()
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Write an advisory recovery copy of the live Context Envelope before compaction."""
|
|
3
|
-
|
|
4
|
-
import json
|
|
5
|
-
import os
|
|
6
|
-
import sys
|
|
7
|
-
import tempfile
|
|
8
|
-
from datetime import datetime, timezone
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
|
|
11
|
-
from context_envelope import (
|
|
12
|
-
ContextEnvelopeError,
|
|
13
|
-
find_root_for_hook,
|
|
14
|
-
read_context_envelope,
|
|
15
|
-
render_context_envelope,
|
|
16
|
-
)
|
|
17
|
-
from runtime_paths import RuntimePathError, validate_project_layout
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def main() -> None:
|
|
21
|
-
try:
|
|
22
|
-
data = json.loads(sys.stdin.read() or "{}")
|
|
23
|
-
data = data if isinstance(data, dict) else {}
|
|
24
|
-
except json.JSONDecodeError as exc:
|
|
25
|
-
print(f"[workflow_checkpoint] invalid hook input: {exc}", file=sys.stderr)
|
|
26
|
-
data = {}
|
|
27
|
-
root = find_root_for_hook(
|
|
28
|
-
Path(data.get("cwd") or Path.cwd()).resolve(),
|
|
29
|
-
"workflow_checkpoint",
|
|
30
|
-
)
|
|
31
|
-
if root is None:
|
|
32
|
-
print(json.dumps({}))
|
|
33
|
-
return
|
|
34
|
-
try:
|
|
35
|
-
envelope = read_context_envelope(root)
|
|
36
|
-
except ContextEnvelopeError as exc:
|
|
37
|
-
print(f"[workflow_checkpoint] cannot inspect Context Envelope: {exc}", file=sys.stderr)
|
|
38
|
-
print(json.dumps({}))
|
|
39
|
-
return
|
|
40
|
-
if not envelope:
|
|
41
|
-
print(json.dumps({}))
|
|
42
|
-
return
|
|
43
|
-
|
|
44
|
-
try:
|
|
45
|
-
validate_project_layout(root, validate_runtime_tree=True)
|
|
46
|
-
except RuntimePathError as exc:
|
|
47
|
-
print(
|
|
48
|
-
f"[workflow_checkpoint] unsafe Ultra runtime: {exc}",
|
|
49
|
-
file=sys.stderr,
|
|
50
|
-
)
|
|
51
|
-
print(json.dumps({}))
|
|
52
|
-
return
|
|
53
|
-
runtime_dir = root / ".ultra" / ".runtime"
|
|
54
|
-
runtime_dir.mkdir(parents=True, exist_ok=True)
|
|
55
|
-
checkpoint = {
|
|
56
|
-
"schema": 3,
|
|
57
|
-
"captured_at": datetime.now(timezone.utc).isoformat(),
|
|
58
|
-
"session_id": data.get("session_id", ""),
|
|
59
|
-
"context_digest": envelope.get("digest"),
|
|
60
|
-
"context": dict(envelope),
|
|
61
|
-
"rendered": render_context_envelope(root, envelope),
|
|
62
|
-
}
|
|
63
|
-
fd, temp_name = tempfile.mkstemp(prefix="checkpoint.", suffix=".json", dir=runtime_dir)
|
|
64
|
-
try:
|
|
65
|
-
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
66
|
-
json.dump(checkpoint, handle, ensure_ascii=False, indent=2)
|
|
67
|
-
handle.write("\n")
|
|
68
|
-
os.replace(temp_name, runtime_dir / "checkpoint.json")
|
|
69
|
-
finally:
|
|
70
|
-
if os.path.exists(temp_name):
|
|
71
|
-
os.unlink(temp_name)
|
|
72
|
-
print(json.dumps({"systemMessage": "Ultra Context Envelope checkpoint saved."}))
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if __name__ == "__main__":
|
|
76
|
-
main()
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Inject one compact, read-only Ultra Context Envelope."""
|
|
3
|
-
|
|
4
|
-
import json
|
|
5
|
-
import sys
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
from context_envelope import (
|
|
9
|
-
ContextEnvelopeError,
|
|
10
|
-
find_root_for_hook,
|
|
11
|
-
read_context_envelope,
|
|
12
|
-
render_context_envelope,
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def payload() -> dict:
|
|
17
|
-
try:
|
|
18
|
-
value = json.loads(sys.stdin.read() or "{}")
|
|
19
|
-
return value if isinstance(value, dict) else {}
|
|
20
|
-
except json.JSONDecodeError as exc:
|
|
21
|
-
print(f"[workflow_context] invalid hook input: {exc}", file=sys.stderr)
|
|
22
|
-
return {}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def main() -> None:
|
|
26
|
-
data = payload()
|
|
27
|
-
root = find_root_for_hook(
|
|
28
|
-
Path(data.get("cwd") or Path.cwd()).resolve(),
|
|
29
|
-
"workflow_context",
|
|
30
|
-
)
|
|
31
|
-
if root is None:
|
|
32
|
-
print(json.dumps({}))
|
|
33
|
-
return
|
|
34
|
-
try:
|
|
35
|
-
envelope = read_context_envelope(root)
|
|
36
|
-
except ContextEnvelopeError as exc:
|
|
37
|
-
print(f"[workflow_context] cannot inspect Context Envelope: {exc}", file=sys.stderr)
|
|
38
|
-
envelope = None
|
|
39
|
-
if not envelope:
|
|
40
|
-
print(json.dumps({}))
|
|
41
|
-
return
|
|
42
|
-
context = render_context_envelope(root, envelope)
|
|
43
|
-
print(json.dumps({"hookSpecificOutput": {
|
|
44
|
-
"hookEventName": "SessionStart",
|
|
45
|
-
"additionalContext": context,
|
|
46
|
-
}}))
|
|
47
|
-
if __name__ == "__main__":
|
|
48
|
-
main()
|
package/hooks/workflow_resume.py
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Re-inject the live Context Envelope after compaction; checkpoints are advisory."""
|
|
3
|
-
|
|
4
|
-
import json
|
|
5
|
-
import sys
|
|
6
|
-
from datetime import datetime
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
|
|
9
|
-
from context_envelope import (
|
|
10
|
-
ContextEnvelopeError,
|
|
11
|
-
find_root_for_hook,
|
|
12
|
-
read_context_envelope,
|
|
13
|
-
render_context_envelope,
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def _hook_output(event: str, text: str) -> dict:
|
|
18
|
-
if event == "SessionStart":
|
|
19
|
-
return {"hookSpecificOutput": {
|
|
20
|
-
"hookEventName": "SessionStart",
|
|
21
|
-
"additionalContext": text,
|
|
22
|
-
}}
|
|
23
|
-
return {"additionalContext": text}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def _checkpoint_advisory(root: Path):
|
|
27
|
-
file = root / ".ultra" / ".runtime" / "checkpoint.json"
|
|
28
|
-
if not file.is_file():
|
|
29
|
-
return None
|
|
30
|
-
try:
|
|
31
|
-
value = json.loads(file.read_text(encoding="utf-8"))
|
|
32
|
-
datetime.fromisoformat(str(value.get("captured_at", "")).replace("Z", "+00:00"))
|
|
33
|
-
except (OSError, json.JSONDecodeError, ValueError, AttributeError) as exc:
|
|
34
|
-
print(f"[workflow_resume] cannot read checkpoint {file}: {exc}", file=sys.stderr)
|
|
35
|
-
return None
|
|
36
|
-
if (
|
|
37
|
-
not isinstance(value, dict)
|
|
38
|
-
or value.get("schema") != 3
|
|
39
|
-
or not isinstance(value.get("context"), dict)
|
|
40
|
-
or not isinstance(value.get("rendered"), str)
|
|
41
|
-
or not value["rendered"].strip()
|
|
42
|
-
or value.get("context_digest") != value["context"].get("digest")
|
|
43
|
-
):
|
|
44
|
-
print(f"[workflow_resume] invalid checkpoint schema in {file}", file=sys.stderr)
|
|
45
|
-
return None
|
|
46
|
-
return "\n".join([
|
|
47
|
-
"[Ultra checkpoint advisory]",
|
|
48
|
-
"Live .ultra/.runtime/state.db could not be read; this checkpoint is not authority.",
|
|
49
|
-
value["rendered"],
|
|
50
|
-
"Run ultra.doctor before mutation.",
|
|
51
|
-
])
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def main() -> None:
|
|
55
|
-
try:
|
|
56
|
-
data = json.loads(sys.stdin.read() or "{}")
|
|
57
|
-
data = data if isinstance(data, dict) else {}
|
|
58
|
-
except json.JSONDecodeError as exc:
|
|
59
|
-
print(f"[workflow_resume] invalid hook input: {exc}", file=sys.stderr)
|
|
60
|
-
data = {}
|
|
61
|
-
root = find_root_for_hook(
|
|
62
|
-
Path(data.get("cwd") or Path.cwd()).resolve(),
|
|
63
|
-
"workflow_resume",
|
|
64
|
-
)
|
|
65
|
-
if root is None:
|
|
66
|
-
print(json.dumps({}))
|
|
67
|
-
return
|
|
68
|
-
text = None
|
|
69
|
-
try:
|
|
70
|
-
envelope = read_context_envelope(root)
|
|
71
|
-
if envelope:
|
|
72
|
-
text = render_context_envelope(root, envelope)
|
|
73
|
-
except ContextEnvelopeError as exc:
|
|
74
|
-
print(f"[workflow_resume] cannot inspect Context Envelope: {exc}", file=sys.stderr)
|
|
75
|
-
text = _checkpoint_advisory(root)
|
|
76
|
-
if not text:
|
|
77
|
-
print(json.dumps({}))
|
|
78
|
-
return
|
|
79
|
-
event = data.get("hook_event_name", "SessionStart")
|
|
80
|
-
print(json.dumps(_hook_output(event, text)))
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if __name__ == "__main__":
|
|
84
|
-
main()
|