ultra-builder-pro-cli 0.22.0 → 0.24.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/docs/research/README.md +4 -4
- package/CHANGELOG.md +83 -0
- package/README.md +68 -35
- package/adapters/_shared/codex-assets.cjs +121 -60
- package/adapters/_shared/interaction-contract.cjs +20 -7
- package/adapters/_shared/provenance.cjs +7 -2
- package/adapters/_shared/runtime-assets.cjs +4 -3
- package/adapters/_shared/runtime-native.cjs +132 -0
- package/adapters/claude.js +61 -31
- package/adapters/codex.js +178 -81
- package/adapters/grok.js +372 -0
- package/adapters/kimi.js +8 -4
- package/adapters/opencode.js +211 -33
- package/agents/code-reviewer.md +4 -2
- package/agents/debugger.md +4 -1
- package/agents/review-code.md +7 -3
- package/agents/review-comments.md +7 -4
- package/agents/review-coordinator.md +11 -7
- package/agents/review-design.md +8 -4
- package/agents/review-errors.md +8 -4
- package/agents/review-spec.md +8 -5
- package/agents/review-tests.md +7 -4
- package/agents/tdd-runner.md +5 -2
- package/bin/install.js +9 -4
- package/bin/orchestrator.js +4 -13
- package/docs/AGENT-CONTEXT.md +141 -231
- package/docs/ARCHITECTURE.md +203 -498
- package/docs/ARTIFACT-AUTHORITY.md +38 -35
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +12 -9
- package/docs/ROADMAP.md +85 -110
- package/docs/RUNTIME-COMPAT-MATRIX.md +139 -174
- package/docs/SKILL-AUTHORING.md +3 -3
- package/docs/STATE-DB-ACCESS-POLICY.md +25 -20
- package/docs/WORKFLOW-LIFECYCLE.md +174 -442
- package/hooks/README.md +11 -8
- package/hooks/active_task_context.py +16 -16
- package/hooks/adapters/grok.py +134 -0
- package/hooks/{context_spine.py → context_envelope.py} +26 -22
- package/hooks/health_check.py +19 -29
- package/hooks/pre_stop_check.py +22 -12
- package/hooks/subagent_tracker.py +1 -1
- package/hooks/workflow_checkpoint.py +14 -13
- package/hooks/workflow_context.py +11 -12
- package/hooks/workflow_resume.py +14 -13
- package/mcp-server/{breadcrumb.cjs → hook-context.cjs} +7 -9
- package/mcp-server/lib/artifact-registry.cjs +25 -1
- package/mcp-server/lib/baseline-workflow.cjs +36 -38
- package/mcp-server/lib/canonical-json.cjs +19 -0
- package/mcp-server/lib/change-workflow.cjs +637 -49
- package/mcp-server/lib/context-envelope.cjs +508 -0
- package/mcp-server/lib/decision-records.cjs +337 -0
- package/mcp-server/lib/doctor.cjs +91 -33
- package/mcp-server/lib/init-project.cjs +78 -39
- package/mcp-server/lib/managed-file-write.cjs +116 -0
- package/mcp-server/lib/plan-checkpoint.cjs +177 -0
- package/mcp-server/lib/project-context.cjs +178 -0
- package/mcp-server/lib/stage-checkpoints.cjs +309 -0
- package/mcp-server/lib/state-db.cjs +155 -24
- package/mcp-server/lib/task-ledger.cjs +522 -13
- package/mcp-server/lib/ultra-facade.cjs +834 -0
- package/mcp-server/lib/worker-packet.cjs +358 -0
- package/mcp-server/server.cjs +208 -1059
- package/orchestrator/daemon.cjs +27 -9
- package/orchestrator/parallel-orchestrator.cjs +111 -76
- package/orchestrator/session-runner.cjs +135 -34
- package/package.json +10 -3
- package/skills/ultra-change/SKILL.md +39 -98
- package/skills/ultra-deliver/SKILL.md +42 -65
- package/skills/ultra-dev/SKILL.md +52 -99
- package/skills/ultra-doctor/SKILL.md +35 -57
- package/skills/ultra-init/SKILL.md +48 -88
- package/skills/ultra-init/references/brownfield-adoption.md +11 -8
- package/skills/ultra-plan/SKILL.md +54 -132
- package/skills/ultra-research/SKILL.md +58 -112
- package/skills/ultra-review/SKILL.md +28 -46
- package/skills/ultra-review/references/unified-schema.md +2 -0
- package/skills/ultra-review/scripts/review_wait.py +8 -0
- package/skills/ultra-status/SKILL.md +22 -45
- package/skills/ultra-test/SKILL.md +27 -61
- package/skills/ultra-think/SKILL.md +29 -48
- package/skills/ultra-think/references/interaction-boundary.md +56 -0
- package/spec/cli-protocol.md +80 -187
- package/spec/mcp-tools.yaml +130 -2738
- package/spec/schemas/mcp-tools.schema.json +1 -1
- package/spec/schemas/state-db.sql +107 -4
- package/spec/schemas/{task-ledger.v1.schema.json → task-ledger.v2.schema.json} +70 -6
- package/ultra-tools/commands/status.cjs +369 -243
- package/mcp-server/lib/context-spine.cjs +0 -1208
- package/mcp-server/lib/decision-dialogue.cjs +0 -1128
- package/mcp-server/lib/project-breadcrumb.cjs +0 -173
- package/mcp-server/lib/spec-learning.cjs +0 -284
- package/mcp-server/lib/workflow-state.cjs +0 -2965
- package/skills/ultra-think/references/decision-dialogue.md +0 -153
package/hooks/README.md
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# Ultra Builder Pro workflow hooks
|
|
2
2
|
|
|
3
|
-
These
|
|
4
|
-
`.ultra/.runtime/state.db` and
|
|
5
|
-
checkpoint and checkout-local projections remain
|
|
6
|
-
lifecycle behavior is recovery-oriented and
|
|
7
|
-
refusal gates.
|
|
3
|
+
These Hooks observe only Ultra-owned state. Session health/context recognizes an
|
|
4
|
+
initialized `.ultra/.runtime/state.db` and reads the canonical Context Envelope.
|
|
5
|
+
Direct writes to the MCP-owned Git checkpoint and checkout-local projections remain
|
|
6
|
+
blocked; context, compact, stop, and agent lifecycle behavior is recovery-oriented and
|
|
7
|
+
does not turn semantic diagnostics into refusal gates.
|
|
8
8
|
|
|
9
9
|
| Lifecycle | Hook | Purpose |
|
|
10
10
|
|---|---|---|
|
|
11
11
|
| Session start | `health_check.py` | Read-only integrity, incident, projection, session, and change-artifact checks |
|
|
12
|
-
| Shared helper | `
|
|
13
|
-
| Session start | `workflow_context.py` | Inject the
|
|
12
|
+
| Shared helper | `context_envelope.py` | Read the same bounded Context Envelope used by MCP, Skills, Sessions, and Workers |
|
|
13
|
+
| Session start | `workflow_context.py` | Inject the compact envelope with accepted intent, never raw interaction or provider payloads |
|
|
14
14
|
| Before edit | `active_task_context.py` | Protect `.ultra/tasks/tasks.json` (Git team checkpoint) and `.ultra/.runtime/projections/tasks.json` (local view), then restate the same DB-derived task breadcrumb |
|
|
15
15
|
| Before compact | `workflow_checkpoint.py` | Validate and atomically save a minimal workflow checkpoint |
|
|
16
16
|
| After compact/resume | `workflow_resume.py` | Prefer the DB breadcrumb; restore the minimal file checkpoint only when no active change owns recovery |
|
|
@@ -27,7 +27,7 @@ memory and graph content belongs to separately installed providers.
|
|
|
27
27
|
`subagent_started` or `subagent_stopped` to `state.db`; it never stores transcript
|
|
28
28
|
paths, messages, or a parallel JSONL lifecycle log.
|
|
29
29
|
|
|
30
|
-
`
|
|
30
|
+
`context_envelope.py` is an imported library, not an additional lifecycle registration.
|
|
31
31
|
Registering it separately would duplicate hook execution. The checkpoint is a recovery artifact, not a second authority. A valid newer
|
|
32
32
|
live workflow wins; a newer valid checkpoint may restore a missing or older
|
|
33
33
|
live file; corrupt, non-object, or terminal checkpoints are ignored.
|
|
@@ -37,3 +37,6 @@ Runtime wiring:
|
|
|
37
37
|
- Claude Code: native `hooks/hooks.json` with `${CLAUDE_PLUGIN_ROOT}` paths.
|
|
38
38
|
- Codex: native `hooks/hooks.json` through `hooks/adapters/codex.py` for wire normalization.
|
|
39
39
|
- OpenCode: native JavaScript plugin events; the Python hook suite is not copied.
|
|
40
|
+
- Kimi Code: native lifecycle events through its wire adapter.
|
|
41
|
+
- Grok Build: camelCase lifecycle events; ignored stdout channels are used for
|
|
42
|
+
observation only, and every Skill rereads authoritative Context at entry.
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Protect
|
|
2
|
+
"""Protect managed task files and restate the live Ultra Context Envelope."""
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
5
|
import sys
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
-
from
|
|
9
|
-
|
|
8
|
+
from context_envelope import (
|
|
9
|
+
ContextEnvelopeError,
|
|
10
10
|
find_root,
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
read_context_envelope,
|
|
12
|
+
render_context_envelope,
|
|
13
13
|
)
|
|
14
14
|
|
|
15
15
|
TEAM_TASK_LEDGER = Path(".ultra/tasks/tasks.json")
|
|
@@ -86,7 +86,7 @@ def main() -> None:
|
|
|
86
86
|
for target_root in managed_roots(data, start):
|
|
87
87
|
try:
|
|
88
88
|
target_authority = find_root(target_root)
|
|
89
|
-
except
|
|
89
|
+
except ContextEnvelopeError as exc:
|
|
90
90
|
print(json.dumps(managed_file_denial(
|
|
91
91
|
"Refusing a direct write to an MCP-managed Ultra task file while the "
|
|
92
92
|
f"runtime authority is conflicted: {exc}. Resolve the "
|
|
@@ -100,13 +100,13 @@ def main() -> None:
|
|
|
100
100
|
"Refusing a direct write to an MCP-managed Ultra task file. "
|
|
101
101
|
".ultra/tasks/tasks.json is the MCP-published team checkpoint and "
|
|
102
102
|
".ultra/.runtime/projections/tasks.json is the checkout-local DB view. "
|
|
103
|
-
"Use
|
|
103
|
+
"Use ultra.record for task contracts/outcomes and ultra.sync for the team checkpoint."
|
|
104
104
|
)))
|
|
105
105
|
return
|
|
106
106
|
|
|
107
107
|
try:
|
|
108
108
|
root = find_root(start)
|
|
109
|
-
except
|
|
109
|
+
except ContextEnvelopeError as exc:
|
|
110
110
|
print(
|
|
111
111
|
f"[active_task_context] cannot resolve Ultra root: {exc}",
|
|
112
112
|
file=sys.stderr,
|
|
@@ -119,7 +119,7 @@ def main() -> None:
|
|
|
119
119
|
"Refusing a direct write to an MCP-managed Ultra task file while the "
|
|
120
120
|
f"runtime authority is conflicted: {exc}. Resolve the "
|
|
121
121
|
"state.db conflict with ultra-doctor before retrying the owning "
|
|
122
|
-
"
|
|
122
|
+
"public Ultra operation."
|
|
123
123
|
)))
|
|
124
124
|
return
|
|
125
125
|
root = None
|
|
@@ -131,18 +131,18 @@ def main() -> None:
|
|
|
131
131
|
"Refusing a direct write to an MCP-managed Ultra task file. "
|
|
132
132
|
".ultra/tasks/tasks.json is the MCP-published team checkpoint and "
|
|
133
133
|
".ultra/.runtime/projections/tasks.json is the checkout-local DB view. "
|
|
134
|
-
"Use
|
|
134
|
+
"Use ultra.record for task contracts/outcomes and ultra.sync for the team checkpoint."
|
|
135
135
|
)))
|
|
136
136
|
return
|
|
137
137
|
try:
|
|
138
|
-
|
|
139
|
-
except
|
|
140
|
-
print(f"[active_task_context] cannot inspect Context
|
|
141
|
-
|
|
142
|
-
if
|
|
138
|
+
envelope = read_context_envelope(root)
|
|
139
|
+
except ContextEnvelopeError as exc:
|
|
140
|
+
print(f"[active_task_context] cannot inspect Context Envelope: {exc}", file=sys.stderr)
|
|
141
|
+
envelope = None
|
|
142
|
+
if envelope:
|
|
143
143
|
print(json.dumps({"hookSpecificOutput": {
|
|
144
144
|
"hookEventName": "PreToolUse",
|
|
145
|
-
"additionalContext":
|
|
145
|
+
"additionalContext": render_context_envelope(root, envelope),
|
|
146
146
|
}}))
|
|
147
147
|
return
|
|
148
148
|
print(json.dumps({}))
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Grok Build wire adapter for Ultra observational lifecycle hooks."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import copy
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
HOOK_ROOT = Path(__file__).resolve().parent.parent
|
|
16
|
+
ALLOWED_FEATURES = {
|
|
17
|
+
"active_task_context.py",
|
|
18
|
+
"health_check.py",
|
|
19
|
+
"pre_stop_check.py",
|
|
20
|
+
"subagent_tracker.py",
|
|
21
|
+
"workflow_checkpoint.py",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def normalize_input(payload: dict[str, Any]) -> dict[str, Any]:
|
|
26
|
+
"""Translate Grok's camelCase wire names into the shared hook contract."""
|
|
27
|
+
normalized = copy.deepcopy(payload)
|
|
28
|
+
aliases = {
|
|
29
|
+
"hookEventName": "hook_event_name",
|
|
30
|
+
"sessionId": "session_id",
|
|
31
|
+
"toolName": "tool_name",
|
|
32
|
+
"toolInput": "tool_input",
|
|
33
|
+
"toolResult": "tool_output",
|
|
34
|
+
"stopHookActive": "stop_hook_active",
|
|
35
|
+
"agentId": "agent_id",
|
|
36
|
+
"agentType": "agent_type",
|
|
37
|
+
"workspaceRoot": "workspace_root",
|
|
38
|
+
}
|
|
39
|
+
for source, target in aliases.items():
|
|
40
|
+
if source in normalized and target not in normalized:
|
|
41
|
+
normalized[target] = normalized[source]
|
|
42
|
+
return normalized
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _parse_output(stdout: str) -> dict[str, Any]:
|
|
46
|
+
text = stdout.strip()
|
|
47
|
+
if not text:
|
|
48
|
+
return {}
|
|
49
|
+
try:
|
|
50
|
+
value = json.loads(text)
|
|
51
|
+
except json.JSONDecodeError:
|
|
52
|
+
return {}
|
|
53
|
+
return value if isinstance(value, dict) else {}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def adapt_output(output: dict[str, Any], event: str, reason: str | None) -> dict[str, Any]:
|
|
57
|
+
"""Return only output fields Grok actually consumes for this event."""
|
|
58
|
+
if event == "PreToolUse":
|
|
59
|
+
specific = output.get("hookSpecificOutput")
|
|
60
|
+
specific = specific if isinstance(specific, dict) else {}
|
|
61
|
+
if specific.get("permissionDecision") == "deny":
|
|
62
|
+
return {
|
|
63
|
+
"decision": "deny",
|
|
64
|
+
"reason": str(
|
|
65
|
+
specific.get("permissionDecisionReason")
|
|
66
|
+
or "Ultra refused a direct write to a managed authority file"
|
|
67
|
+
),
|
|
68
|
+
}
|
|
69
|
+
return {"decision": "allow"}
|
|
70
|
+
# Ultra Stop is advisory. Grok's session-end Stop is observe-only and must
|
|
71
|
+
# never be confused with a genuine end_turn gate.
|
|
72
|
+
if event == "Stop" and reason != "end_turn":
|
|
73
|
+
return {}
|
|
74
|
+
return {}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def run_feature(feature: str, payload: dict[str, Any], feature_args: list[str]) -> dict[str, Any]:
|
|
78
|
+
if feature not in ALLOWED_FEATURES or Path(feature).name != feature:
|
|
79
|
+
return {}
|
|
80
|
+
script = HOOK_ROOT / feature
|
|
81
|
+
if not script.is_file():
|
|
82
|
+
return {}
|
|
83
|
+
normalized = normalize_input(payload)
|
|
84
|
+
env = os.environ.copy()
|
|
85
|
+
env["UBP_HOOK_RUNTIME"] = "grok"
|
|
86
|
+
plugin_data = env.get("GROK_PLUGIN_DATA") or env.get("CLAUDE_PLUGIN_DATA")
|
|
87
|
+
if plugin_data:
|
|
88
|
+
env["UBP_HOOK_DATA"] = plugin_data
|
|
89
|
+
cwd = normalized.get("cwd")
|
|
90
|
+
run_cwd = str(cwd) if isinstance(cwd, str) and Path(cwd).is_dir() else os.getcwd()
|
|
91
|
+
proc = subprocess.run(
|
|
92
|
+
[sys.executable, str(script), *feature_args],
|
|
93
|
+
input=json.dumps(normalized),
|
|
94
|
+
text=True,
|
|
95
|
+
capture_output=True,
|
|
96
|
+
cwd=run_cwd,
|
|
97
|
+
env=env,
|
|
98
|
+
timeout=30,
|
|
99
|
+
check=False,
|
|
100
|
+
)
|
|
101
|
+
if proc.stderr:
|
|
102
|
+
print(proc.stderr.rstrip(), file=sys.stderr)
|
|
103
|
+
if proc.returncode != 0:
|
|
104
|
+
return {}
|
|
105
|
+
return adapt_output(
|
|
106
|
+
_parse_output(proc.stdout),
|
|
107
|
+
str(payload.get("hookEventName") or ""),
|
|
108
|
+
str(payload.get("reason") or "") or None,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def main() -> int:
|
|
113
|
+
if len(sys.argv) < 2:
|
|
114
|
+
print(json.dumps({}))
|
|
115
|
+
return 0
|
|
116
|
+
feature = sys.argv[1]
|
|
117
|
+
try:
|
|
118
|
+
payload = json.loads(sys.stdin.read() or "{}")
|
|
119
|
+
if not isinstance(payload, dict):
|
|
120
|
+
payload = {}
|
|
121
|
+
except json.JSONDecodeError as exc:
|
|
122
|
+
print(f"[grok hook] invalid input: {exc}", file=sys.stderr)
|
|
123
|
+
payload = {}
|
|
124
|
+
try:
|
|
125
|
+
output = run_feature(feature, payload, sys.argv[2:])
|
|
126
|
+
except Exception as exc:
|
|
127
|
+
print(f"[grok hook] {feature} failed open: {exc}", file=sys.stderr)
|
|
128
|
+
output = {}
|
|
129
|
+
print(json.dumps(output, ensure_ascii=False))
|
|
130
|
+
return 0
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
if __name__ == "__main__":
|
|
134
|
+
raise SystemExit(main())
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Thin bridge to the
|
|
2
|
+
"""Thin read-only bridge to the bundled Ultra Context Envelope reader."""
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
5
|
import os
|
|
@@ -11,12 +11,12 @@ from pathlib import Path
|
|
|
11
11
|
from runtime_paths import RuntimePathError, find_project_root
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
class
|
|
15
|
-
"""The canonical
|
|
14
|
+
class ContextEnvelopeError(RuntimeError):
|
|
15
|
+
"""The canonical Context Envelope helper could not be executed or decoded."""
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
class
|
|
19
|
-
"""Dictionary
|
|
18
|
+
class ContextEnvelope(dict):
|
|
19
|
+
"""Dictionary envelope carrying its canonical bounded injection text."""
|
|
20
20
|
|
|
21
21
|
def __init__(self, value: dict, rendered: str):
|
|
22
22
|
super().__init__(value)
|
|
@@ -27,27 +27,27 @@ def find_root(start: Path):
|
|
|
27
27
|
try:
|
|
28
28
|
return find_project_root(start)
|
|
29
29
|
except RuntimePathError as exc:
|
|
30
|
-
raise
|
|
30
|
+
raise ContextEnvelopeError(str(exc)) from exc
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
def find_root_for_hook(start: Path, hook_name: str):
|
|
34
34
|
"""Fail open at a hook boundary while preserving path-conflict evidence."""
|
|
35
35
|
try:
|
|
36
36
|
return find_root(start)
|
|
37
|
-
except
|
|
37
|
+
except ContextEnvelopeError as exc:
|
|
38
38
|
print(f"[{hook_name}] cannot resolve Ultra root: {exc}", file=sys.stderr)
|
|
39
39
|
return None
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
def _helper_path() -> Path:
|
|
43
43
|
hook_root = Path(__file__).resolve().parent
|
|
44
|
-
installed = hook_root.parent / "runtime" / "
|
|
44
|
+
installed = hook_root.parent / "runtime" / "hook-context.cjs"
|
|
45
45
|
if installed.is_file():
|
|
46
46
|
return installed
|
|
47
|
-
source = hook_root.parent / "mcp-server" / "
|
|
47
|
+
source = hook_root.parent / "mcp-server" / "hook-context.cjs"
|
|
48
48
|
if source.is_file():
|
|
49
49
|
return source
|
|
50
|
-
raise
|
|
50
|
+
raise ContextEnvelopeError("bundled Context Envelope helper is missing")
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
def _node_binary() -> str:
|
|
@@ -57,10 +57,10 @@ def _node_binary() -> str:
|
|
|
57
57
|
discovered = shutil.which("node")
|
|
58
58
|
if discovered:
|
|
59
59
|
return discovered
|
|
60
|
-
raise
|
|
60
|
+
raise ContextEnvelopeError("node executable is unavailable")
|
|
61
61
|
|
|
62
62
|
|
|
63
|
-
def
|
|
63
|
+
def read_context_envelope(root: Path):
|
|
64
64
|
try:
|
|
65
65
|
result = subprocess.run(
|
|
66
66
|
[_node_binary(), str(_helper_path()), str(root.resolve())],
|
|
@@ -70,27 +70,31 @@ def read_breadcrumb(root: Path):
|
|
|
70
70
|
timeout=5,
|
|
71
71
|
)
|
|
72
72
|
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
73
|
-
raise
|
|
73
|
+
raise ContextEnvelopeError(str(exc)) from exc
|
|
74
74
|
if result.returncode != 0:
|
|
75
75
|
detail = result.stderr.strip() or f"exit {result.returncode}"
|
|
76
|
-
raise
|
|
76
|
+
raise ContextEnvelopeError(detail)
|
|
77
77
|
try:
|
|
78
78
|
payload = json.loads(result.stdout)
|
|
79
79
|
except json.JSONDecodeError as exc:
|
|
80
|
-
raise
|
|
80
|
+
raise ContextEnvelopeError(f"invalid Context Envelope response: {exc}") from exc
|
|
81
81
|
if not isinstance(payload, dict):
|
|
82
|
-
raise
|
|
83
|
-
value = payload.get("
|
|
82
|
+
raise ContextEnvelopeError("Context Envelope response must be an object")
|
|
83
|
+
value = payload.get("context")
|
|
84
84
|
if value is None:
|
|
85
85
|
return None
|
|
86
86
|
rendered = payload.get("text")
|
|
87
87
|
if not isinstance(value, dict) or not isinstance(rendered, str) or not rendered:
|
|
88
|
-
raise
|
|
89
|
-
|
|
88
|
+
raise ContextEnvelopeError(
|
|
89
|
+
"Context Envelope response is missing canonical state or text"
|
|
90
|
+
)
|
|
91
|
+
return ContextEnvelope(value, rendered)
|
|
90
92
|
|
|
91
93
|
|
|
92
|
-
def
|
|
93
|
-
rendered = getattr(
|
|
94
|
+
def render_context_envelope(_root: Path, context: dict) -> str:
|
|
95
|
+
rendered = getattr(context, "rendered", None)
|
|
94
96
|
if not isinstance(rendered, str) or not rendered:
|
|
95
|
-
raise
|
|
97
|
+
raise ContextEnvelopeError(
|
|
98
|
+
"Context Envelope was not produced by the canonical reader"
|
|
99
|
+
)
|
|
96
100
|
return rendered
|
package/hooks/health_check.py
CHANGED
|
@@ -6,7 +6,11 @@ import sqlite3
|
|
|
6
6
|
import sys
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
|
-
from
|
|
9
|
+
from context_envelope import (
|
|
10
|
+
ContextEnvelopeError,
|
|
11
|
+
find_root_for_hook,
|
|
12
|
+
read_context_envelope,
|
|
13
|
+
)
|
|
10
14
|
from runtime_paths import state_db_path
|
|
11
15
|
|
|
12
16
|
|
|
@@ -14,8 +18,9 @@ REQUIRED_TABLES = {
|
|
|
14
18
|
"baselines",
|
|
15
19
|
"tasks", "events", "sessions", "schema_version", "migration_history",
|
|
16
20
|
"telemetry", "specs_refs", "circuit_breaker", "changes", "artifacts",
|
|
17
|
-
"
|
|
18
|
-
"
|
|
21
|
+
"artifact_edges", "context_snapshots", "spec_learning_candidates", "trace_links",
|
|
22
|
+
"incidents", "projection_jobs", "event_consumers", "stage_checkpoints",
|
|
23
|
+
"decision_records", "context_envelopes", "worker_packets",
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
|
|
@@ -28,22 +33,6 @@ def hook_input() -> dict:
|
|
|
28
33
|
return {}
|
|
29
34
|
|
|
30
35
|
|
|
31
|
-
def has_active_workflow(db_path: Path) -> bool:
|
|
32
|
-
if not db_path.is_file():
|
|
33
|
-
return False
|
|
34
|
-
try:
|
|
35
|
-
uri = f"file:{db_path}?mode=ro"
|
|
36
|
-
with sqlite3.connect(uri, uri=True, timeout=1) as conn:
|
|
37
|
-
row = conn.execute(
|
|
38
|
-
"""SELECT 1 FROM workflow_runs
|
|
39
|
-
WHERE status IN ('active', 'blocked', 'ready')
|
|
40
|
-
LIMIT 1"""
|
|
41
|
-
).fetchone()
|
|
42
|
-
return row is not None
|
|
43
|
-
except sqlite3.Error:
|
|
44
|
-
return False
|
|
45
|
-
|
|
46
|
-
|
|
47
36
|
def inspect(root: Path) -> dict:
|
|
48
37
|
db_path = state_db_path(root)
|
|
49
38
|
report = {"status": "healthy", "project": str(root), "checks": {}}
|
|
@@ -54,8 +43,8 @@ def inspect(root: Path) -> dict:
|
|
|
54
43
|
return report
|
|
55
44
|
|
|
56
45
|
try:
|
|
57
|
-
|
|
58
|
-
except
|
|
46
|
+
context = read_context_envelope(root)
|
|
47
|
+
except ContextEnvelopeError as exc:
|
|
59
48
|
report["checks"]["authority"] = {"status": "fail", "reason": str(exc)}
|
|
60
49
|
report["status"] = "degraded"
|
|
61
50
|
return report
|
|
@@ -82,14 +71,18 @@ def inspect(root: Path) -> dict:
|
|
|
82
71
|
report["status"] = "degraded"
|
|
83
72
|
return report
|
|
84
73
|
|
|
74
|
+
envelope = context.get("envelope", {}) if context else {}
|
|
75
|
+
diagnostics = envelope.get("diagnostics", {})
|
|
85
76
|
baseline_codes = [
|
|
86
|
-
code
|
|
87
|
-
|
|
88
|
-
*
|
|
77
|
+
str(item.get("code"))
|
|
78
|
+
for item in [
|
|
79
|
+
*diagnostics.get("needs_attention", []),
|
|
80
|
+
*diagnostics.get("warnings", []),
|
|
81
|
+
*diagnostics.get("hard_conflicts", []),
|
|
89
82
|
]
|
|
90
|
-
if str(code).startswith("BASELINE_")
|
|
83
|
+
if str(item.get("code", "")).startswith("BASELINE_")
|
|
91
84
|
]
|
|
92
|
-
baseline =
|
|
85
|
+
baseline = envelope.get("baseline")
|
|
93
86
|
baseline_is_ready = isinstance(baseline, dict) and baseline.get("status") == "ready"
|
|
94
87
|
baseline_status = (
|
|
95
88
|
"fail" if baseline_codes and baseline_is_ready
|
|
@@ -188,9 +181,6 @@ def main() -> None:
|
|
|
188
181
|
if root is None:
|
|
189
182
|
print(json.dumps({}))
|
|
190
183
|
return
|
|
191
|
-
if not has_active_workflow(state_db_path(root)):
|
|
192
|
-
print(json.dumps({}))
|
|
193
|
-
return
|
|
194
184
|
report = inspect(root)
|
|
195
185
|
if report["status"] != "healthy":
|
|
196
186
|
print("[Ultra health] " + json.dumps(report, ensure_ascii=False, sort_keys=True), file=sys.stderr)
|
package/hooks/pre_stop_check.py
CHANGED
|
@@ -5,7 +5,11 @@ import json
|
|
|
5
5
|
import sys
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
-
from
|
|
8
|
+
from context_envelope import (
|
|
9
|
+
ContextEnvelopeError,
|
|
10
|
+
find_root_for_hook,
|
|
11
|
+
read_context_envelope,
|
|
12
|
+
)
|
|
9
13
|
|
|
10
14
|
|
|
11
15
|
def allow_stop() -> None:
|
|
@@ -32,21 +36,27 @@ def main() -> None:
|
|
|
32
36
|
allow_stop()
|
|
33
37
|
return
|
|
34
38
|
try:
|
|
35
|
-
|
|
36
|
-
except
|
|
37
|
-
print(f"[pre_stop_check] cannot inspect Context
|
|
39
|
+
context = read_context_envelope(root)
|
|
40
|
+
except ContextEnvelopeError as exc:
|
|
41
|
+
print(f"[pre_stop_check] cannot inspect Context Envelope: {exc}", file=sys.stderr)
|
|
38
42
|
allow_stop()
|
|
39
43
|
return
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
envelope = context.get("envelope", {}) if context else {}
|
|
45
|
+
change = envelope.get("change") or {}
|
|
46
|
+
task = envelope.get("task") or {}
|
|
47
|
+
if change.get("id") and change.get("status") not in ("archived", "cancelled"):
|
|
48
|
+
attention = envelope.get("diagnostics", {}).get("needs_attention", [])
|
|
49
|
+
warnings = envelope.get("diagnostics", {}).get("warnings", [])
|
|
50
|
+
codes = ", ".join(
|
|
51
|
+
str(item.get("code")) for item in [*attention, *warnings] if item.get("code")
|
|
52
|
+
) or "none"
|
|
43
53
|
print(
|
|
44
54
|
"[Ultra stop advisory] Active Ultra change remains open: "
|
|
45
|
-
f"change={
|
|
46
|
-
f"task={
|
|
47
|
-
f"
|
|
48
|
-
f"
|
|
49
|
-
"Stop is allowed;
|
|
55
|
+
f"change={change['id']}, "
|
|
56
|
+
f"task={task.get('id') or 'none'}, "
|
|
57
|
+
f"stage={envelope.get('execution', {}).get('stage') or 'project'}, "
|
|
58
|
+
f"diagnostics={codes}. "
|
|
59
|
+
"Stop is allowed; read ultra.context before resuming or explicitly archive later.",
|
|
50
60
|
file=sys.stderr,
|
|
51
61
|
)
|
|
52
62
|
allow_stop()
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Write
|
|
2
|
+
"""Write an advisory recovery copy of the live Context Envelope before compaction."""
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
5
|
import os
|
|
@@ -8,11 +8,11 @@ import tempfile
|
|
|
8
8
|
from datetime import datetime, timezone
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
|
|
11
|
-
from
|
|
12
|
-
|
|
11
|
+
from context_envelope import (
|
|
12
|
+
ContextEnvelopeError,
|
|
13
13
|
find_root_for_hook,
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
read_context_envelope,
|
|
15
|
+
render_context_envelope,
|
|
16
16
|
)
|
|
17
17
|
from runtime_paths import RuntimePathError, validate_project_layout
|
|
18
18
|
|
|
@@ -32,12 +32,12 @@ def main() -> None:
|
|
|
32
32
|
print(json.dumps({}))
|
|
33
33
|
return
|
|
34
34
|
try:
|
|
35
|
-
|
|
36
|
-
except
|
|
37
|
-
print(f"[workflow_checkpoint] cannot inspect Context
|
|
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
38
|
print(json.dumps({}))
|
|
39
39
|
return
|
|
40
|
-
if not
|
|
40
|
+
if not envelope:
|
|
41
41
|
print(json.dumps({}))
|
|
42
42
|
return
|
|
43
43
|
|
|
@@ -53,11 +53,12 @@ def main() -> None:
|
|
|
53
53
|
runtime_dir = root / ".ultra" / ".runtime"
|
|
54
54
|
runtime_dir.mkdir(parents=True, exist_ok=True)
|
|
55
55
|
checkpoint = {
|
|
56
|
-
"schema":
|
|
56
|
+
"schema": 3,
|
|
57
57
|
"captured_at": datetime.now(timezone.utc).isoformat(),
|
|
58
58
|
"session_id": data.get("session_id", ""),
|
|
59
|
-
"
|
|
60
|
-
"
|
|
59
|
+
"context_digest": envelope.get("digest"),
|
|
60
|
+
"context": dict(envelope),
|
|
61
|
+
"rendered": render_context_envelope(root, envelope),
|
|
61
62
|
}
|
|
62
63
|
fd, temp_name = tempfile.mkstemp(prefix="checkpoint.", suffix=".json", dir=runtime_dir)
|
|
63
64
|
try:
|
|
@@ -68,7 +69,7 @@ def main() -> None:
|
|
|
68
69
|
finally:
|
|
69
70
|
if os.path.exists(temp_name):
|
|
70
71
|
os.unlink(temp_name)
|
|
71
|
-
print(json.dumps({"systemMessage": "Ultra
|
|
72
|
+
print(json.dumps({"systemMessage": "Ultra Context Envelope checkpoint saved."}))
|
|
72
73
|
|
|
73
74
|
|
|
74
75
|
if __name__ == "__main__":
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Inject one compact
|
|
2
|
+
"""Inject one compact, read-only Ultra Context Envelope."""
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
5
|
import sys
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
-
from
|
|
9
|
-
|
|
8
|
+
from context_envelope import (
|
|
9
|
+
ContextEnvelopeError,
|
|
10
10
|
find_root_for_hook,
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
read_context_envelope,
|
|
12
|
+
render_context_envelope,
|
|
13
13
|
)
|
|
14
14
|
|
|
15
15
|
|
|
@@ -32,15 +32,14 @@ def main() -> None:
|
|
|
32
32
|
print(json.dumps({}))
|
|
33
33
|
return
|
|
34
34
|
try:
|
|
35
|
-
|
|
36
|
-
except
|
|
37
|
-
print(f"[workflow_context] cannot inspect Context
|
|
38
|
-
|
|
39
|
-
if
|
|
40
|
-
context = render_breadcrumb(root, breadcrumb)
|
|
41
|
-
else:
|
|
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:
|
|
42
40
|
print(json.dumps({}))
|
|
43
41
|
return
|
|
42
|
+
context = render_context_envelope(root, envelope)
|
|
44
43
|
print(json.dumps({"hookSpecificOutput": {
|
|
45
44
|
"hookEventName": "SessionStart",
|
|
46
45
|
"additionalContext": context,
|