ultra-builder-pro-cli 5.0.0 → 6.0.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 +113 -3
- package/README.md +67 -478
- package/README.zh-CN.md +49 -422
- package/adapters/_shared/frontmatter.cjs +1 -18
- package/adapters/_shared/plugin-core.cjs +17 -83
- package/adapters/_shared/runtime-assets.cjs +6 -120
- package/adapters/claude.js +8 -24
- package/adapters/codex.js +16 -38
- package/bin/install.js +35 -57
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +13 -101
- package/docs/examples/drive/claude-reviewer.sh +53 -0
- package/docs/examples/drive/claude-writer.sh +43 -0
- package/engine/FORMATS.md +1051 -0
- package/engine/checks.cjs +139 -0
- package/engine/cli.cjs +148 -0
- package/engine/completion.cjs +48 -0
- package/engine/context.cjs +94 -0
- package/engine/drive.cjs +220 -0
- package/engine/expect-worker.cjs +5 -0
- package/engine/expect.cjs +33 -0
- package/engine/formats.cjs +189 -0
- package/engine/git-state.cjs +112 -0
- package/engine/ledger.cjs +205 -0
- package/engine/loading.cjs +28 -0
- package/engine/merge.cjs +273 -0
- package/engine/milestone.cjs +89 -0
- package/engine/plan.cjs +232 -0
- package/engine/project.cjs +5 -0
- package/engine/signals.cjs +3 -0
- package/engine/states.cjs +175 -0
- package/hooks/session-start.cjs +54 -0
- package/package.json +17 -52
- package/skills/ultra/SKILL.md +395 -0
- package/.ultra-template/changes/abandoned/.gitkeep +0 -0
- package/.ultra-template/changes/active/.gitkeep +0 -1
- package/.ultra-template/changes/archive/.gitkeep +0 -1
- package/.ultra-template/contexts/TEMPLATE.md +0 -135
- package/.ultra-template/decisions/.gitkeep +0 -1
- package/.ultra-template/evidence/.gitkeep +0 -1
- package/.ultra-template/north-star.md +0 -52
- package/.ultra-template/project-brief.md +0 -28
- package/.ultra-template/research/.gitkeep +0 -1
- package/.ultra-template/specs/architecture.md +0 -81
- package/.ultra-template/specs/discovery.md +0 -74
- package/.ultra-template/specs/product.md +0 -80
- package/.ultra-template/specs/research-distillate.md +0 -57
- package/.ultra-template/tasks.json +0 -4
- package/.ultra-template/test-report.json +0 -44
- package/adapters/_shared/host-profile.cjs +0 -5
- package/adapters/grok.js +0 -242
- package/adapters/kimi.js +0 -232
- package/adapters/opencode.js +0 -408
- package/adapters/zcode.js +0 -375
- package/bin/delegate-worker.cjs +0 -5
- package/bin/delegate.cjs +0 -5
- package/docs/ARCHITECTURE.md +0 -274
- package/docs/ARTIFACT-AUTHORITY.md +0 -305
- package/docs/DECISIONS.md +0 -229
- package/docs/PHILOSOPHY.md +0 -261
- package/docs/RUNTIME-COMPAT-MATRIX.md +0 -315
- package/docs/SKILL-AUTHORING.md +0 -113
- package/docs/WORKFLOW-LIFECYCLE.md +0 -350
- package/docs/evals/adversarial-review-2026-08-14.md +0 -84
- package/docs/evals/ultra-5-0-journey-results.md +0 -62
- package/docs/evals/ultra-5-0-live-acp-conformance.md +0 -93
- package/docs/evals/ultra-5-0-route-journeys.md +0 -145
- package/docs/evals/zcode-automation-2026-08-14.md +0 -103
- package/hooks/README.md +0 -52
- package/hooks/_common.py +0 -1779
- package/hooks/adapters/codex.py +0 -295
- package/hooks/adapters/grok.py +0 -134
- package/hooks/adapters/kimi.py +0 -156
- package/hooks/adapters/zcode.py +0 -227
- package/hooks/block_dangerous_commands.py +0 -201
- package/hooks/compact_context.py +0 -224
- package/hooks/mid_workflow_recall.py +0 -47
- package/hooks/post_edit_guard.py +0 -215
- package/hooks/session_context.py +0 -741
- package/skills/ultra-change/SKILL.md +0 -165
- package/skills/ultra-change/references/change-contract.md +0 -199
- package/skills/ultra-change/references/execution-grant.md +0 -109
- package/skills/ultra-change/references/primary-transfer.md +0 -273
- package/skills/ultra-change/scripts/validate_primary_transfer.cjs +0 -1928
- package/skills/ultra-delegate/SKILL.md +0 -195
- package/skills/ultra-delegate/patches/zcode-acp-0.1.0-zcode-0.16.5.patch +0 -184
- package/skills/ultra-delegate/references/delegation-contract.md +0 -138
- package/skills/ultra-delegate/scripts/acp-conformance.cjs +0 -287
- package/skills/ultra-delegate/scripts/acp-driver.cjs +0 -762
- package/skills/ultra-delegate/scripts/agent-execution-driver.cjs +0 -320
- package/skills/ultra-delegate/scripts/cli-driver.cjs +0 -61
- package/skills/ultra-delegate/scripts/delegate-profiles/kimi-read-only.md +0 -15
- package/skills/ultra-delegate/scripts/delegate-profiles/kimi-write.md +0 -17
- package/skills/ultra-delegate/scripts/delegate-worker.cjs +0 -328
- package/skills/ultra-delegate/scripts/delegate.cjs +0 -690
- package/skills/ultra-delegate/scripts/delegate_wait.py +0 -86
- package/skills/ultra-delegate/scripts/host-profile.cjs +0 -267
- package/skills/ultra-deliver/SKILL.md +0 -194
- package/skills/ultra-deliver/references/baseline-reconciliation.md +0 -41
- package/skills/ultra-dev/SKILL.md +0 -265
- package/skills/ultra-dev/references/debugging.md +0 -22
- package/skills/ultra-domain-modeling/SKILL.md +0 -79
- package/skills/ultra-grilling/SKILL.md +0 -121
- package/skills/ultra-grilling/references/reframing.md +0 -76
- package/skills/ultra-init/SKILL.md +0 -143
- package/skills/ultra-init/references/brownfield-adoption.md +0 -32
- package/skills/ultra-init/scripts/init_project.cjs +0 -1031
- package/skills/ultra-plan/SKILL.md +0 -160
- package/skills/ultra-plan/references/task-evidence-v2.md +0 -300
- package/skills/ultra-plan/scripts/validate_task_evidence.cjs +0 -859
- package/skills/ultra-research/SKILL.md +0 -167
- package/skills/ultra-research/references/00-problem-validation.md +0 -36
- package/skills/ultra-research/references/01-opportunity-discovery.md +0 -33
- package/skills/ultra-research/references/02-market-assessment.md +0 -31
- package/skills/ultra-research/references/03-alternatives.md +0 -31
- package/skills/ultra-research/references/04-product-strategy.md +0 -41
- package/skills/ultra-research/references/05-assumptions-validation.md +0 -31
- package/skills/ultra-research/references/10-user-personas.md +0 -30
- package/skills/ultra-research/references/11-user-scenarios.md +0 -28
- package/skills/ultra-research/references/20-user-stories.md +0 -31
- package/skills/ultra-research/references/21-features-scope.md +0 -38
- package/skills/ultra-research/references/22-success-metrics.md +0 -33
- package/skills/ultra-research/references/30-architecture-context.md +0 -30
- package/skills/ultra-research/references/31-solution-strategy.md +0 -28
- package/skills/ultra-research/references/32-building-blocks.md +0 -28
- package/skills/ultra-research/references/40-deployment.md +0 -29
- package/skills/ultra-research/references/41-quality-risks.md +0 -28
- package/skills/ultra-research/references/99-synthesis.md +0 -59
- package/skills/ultra-research/references/north-star-v2.md +0 -168
- package/skills/ultra-research/references/wayfinding.md +0 -50
- package/skills/ultra-research/scripts/validate_north_star.cjs +0 -1174
- package/skills/ultra-review/SKILL.md +0 -275
- package/skills/ultra-review/references/adversarial-evaluation.md +0 -31
- package/skills/ultra-review/references/code.md +0 -17
- package/skills/ultra-review/references/comments.md +0 -12
- package/skills/ultra-review/references/design.md +0 -12
- package/skills/ultra-review/references/errors.md +0 -13
- package/skills/ultra-review/references/review-modes.md +0 -29
- package/skills/ultra-review/references/spec.md +0 -18
- package/skills/ultra-review/references/tests.md +0 -12
- package/skills/ultra-review/references/unified-schema.md +0 -284
- package/skills/ultra-review/references/worker-packet.md +0 -183
- package/skills/ultra-review/scripts/review_wait.py +0 -2943
- package/skills/ultra-status/SKILL.md +0 -209
- package/skills/ultra-status/references/route-contract.md +0 -100
- package/skills/ultra-tdd/SKILL.md +0 -91
- package/skills/ultra-tdd/references/templates/README.md +0 -20
- package/skills/ultra-tdd/references/templates/feature-flag-default-audit.sh +0 -79
- package/skills/ultra-tdd/references/templates/persistence-real.ts +0 -78
- package/skills/ultra-tdd/references/templates/testcontainer-postgres.py +0 -78
- package/skills/ultra-tdd/references/templates/testcontainer-postgres.ts +0 -73
- package/skills/ultra-tdd/references/templates/vertical-slice.ts +0 -66
- package/skills/ultra-tdd/references/test-execution.md +0 -23
- package/skills/ultra-test/SKILL.md +0 -244
- package/skills/ultra-test/references/export-syntax.md +0 -18
- package/skills/ultra-test/scripts/validate_review_transport.cjs +0 -549
- package/skills/ultra-test/scripts/worktree_digest.cjs +0 -576
- package/skills/ultra-think/SKILL.md +0 -56
- package/skills/ultra-think/references/autonomy-boundary.md +0 -48
- package/skills/ultra-think/references/interaction-boundary.md +0 -42
package/hooks/adapters/zcode.py
DELETED
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""ZCode wire adapter for Ultra's five file-first lifecycle hooks."""
|
|
3
|
-
|
|
4
|
-
from __future__ import annotations
|
|
5
|
-
|
|
6
|
-
import copy
|
|
7
|
-
import json
|
|
8
|
-
import os
|
|
9
|
-
import re
|
|
10
|
-
import subprocess
|
|
11
|
-
import sys
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
from typing import Any
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
HOOK_ROOT = Path(__file__).resolve().parent.parent
|
|
17
|
-
ALLOWED_FEATURES = {
|
|
18
|
-
"block_dangerous_commands.py",
|
|
19
|
-
"compact_context.py",
|
|
20
|
-
"mid_workflow_recall.py",
|
|
21
|
-
"post_edit_guard.py",
|
|
22
|
-
"session_context.py",
|
|
23
|
-
}
|
|
24
|
-
PATCH_FILE_RE = re.compile(r"^\*\*\* (?:Add|Update|Delete) File: (.+?)\s*$", re.MULTILINE)
|
|
25
|
-
PATCH_MOVE_RE = re.compile(r"^\*\*\* Move to: (.+?)\s*$", re.MULTILINE)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def normalize_input(payload: dict[str, Any]) -> dict[str, Any]:
|
|
29
|
-
"""Translate ZCode's camelCase hook wire names to the shared contract."""
|
|
30
|
-
normalized = copy.deepcopy(payload)
|
|
31
|
-
aliases = {
|
|
32
|
-
"hookEventName": "hook_event_name",
|
|
33
|
-
"sessionId": "session_id",
|
|
34
|
-
"toolName": "tool_name",
|
|
35
|
-
"toolInput": "tool_input",
|
|
36
|
-
"toolResult": "tool_output",
|
|
37
|
-
"toolResponse": "tool_output",
|
|
38
|
-
"workspaceRoot": "workspace_root",
|
|
39
|
-
}
|
|
40
|
-
for source, target in aliases.items():
|
|
41
|
-
if source in normalized and target not in normalized:
|
|
42
|
-
normalized[target] = normalized[source]
|
|
43
|
-
if "cwd" not in normalized and isinstance(normalized.get("workspace_root"), str):
|
|
44
|
-
normalized["cwd"] = normalized["workspace_root"]
|
|
45
|
-
return normalized
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def _patch_paths(tool_input: Any) -> list[str]:
|
|
49
|
-
if not isinstance(tool_input, dict):
|
|
50
|
-
return []
|
|
51
|
-
patch = tool_input.get("command") or tool_input.get("patch") or ""
|
|
52
|
-
if not isinstance(patch, str):
|
|
53
|
-
return []
|
|
54
|
-
paths: list[str] = []
|
|
55
|
-
for match in [*PATCH_FILE_RE.findall(patch), *PATCH_MOVE_RE.findall(patch)]:
|
|
56
|
-
candidate = match.strip()
|
|
57
|
-
if candidate and candidate not in paths:
|
|
58
|
-
paths.append(candidate)
|
|
59
|
-
return paths
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def normalize_inputs(payload: dict[str, Any]) -> list[dict[str, Any]]:
|
|
63
|
-
normalized = normalize_input(payload)
|
|
64
|
-
if normalized.get("tool_name") != "ApplyPatch":
|
|
65
|
-
return [normalized]
|
|
66
|
-
paths = _patch_paths(normalized.get("tool_input"))
|
|
67
|
-
if not paths:
|
|
68
|
-
normalized["tool_name"] = "Edit"
|
|
69
|
-
return [normalized]
|
|
70
|
-
results: list[dict[str, Any]] = []
|
|
71
|
-
for file_path in paths:
|
|
72
|
-
item = copy.deepcopy(normalized)
|
|
73
|
-
item["tool_name"] = "Edit"
|
|
74
|
-
tool_input = item.get("tool_input")
|
|
75
|
-
item["tool_input"] = dict(tool_input) if isinstance(tool_input, dict) else {}
|
|
76
|
-
item["tool_input"]["file_path"] = file_path
|
|
77
|
-
results.append(item)
|
|
78
|
-
return results
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def _parse_output(stdout: str) -> dict[str, Any]:
|
|
82
|
-
text = stdout.strip()
|
|
83
|
-
if not text:
|
|
84
|
-
return {}
|
|
85
|
-
try:
|
|
86
|
-
value = json.loads(text)
|
|
87
|
-
except json.JSONDecodeError:
|
|
88
|
-
return {"systemMessage": f"Ultra hook returned non-JSON output: {text[:500]}"}
|
|
89
|
-
return value if isinstance(value, dict) else {}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def adapt_output(output: dict[str, Any], event: str) -> dict[str, Any]:
|
|
93
|
-
"""Project shared output onto ZCode's strict event-specific schema."""
|
|
94
|
-
if not isinstance(output, dict):
|
|
95
|
-
return {}
|
|
96
|
-
specific = output.get("hookSpecificOutput")
|
|
97
|
-
specific = specific if isinstance(specific, dict) else {}
|
|
98
|
-
context = specific.get("additionalContext") or output.get("additionalContext")
|
|
99
|
-
|
|
100
|
-
if event == "PreToolUse":
|
|
101
|
-
decision = specific.get("permissionDecision")
|
|
102
|
-
if decision in {"allow", "ask", "deny"}:
|
|
103
|
-
projected: dict[str, Any] = {
|
|
104
|
-
"hookEventName": "PreToolUse",
|
|
105
|
-
"permissionDecision": decision,
|
|
106
|
-
}
|
|
107
|
-
reason = specific.get("permissionDecisionReason") or output.get("reason")
|
|
108
|
-
if reason:
|
|
109
|
-
projected["permissionDecisionReason"] = str(reason)
|
|
110
|
-
if context:
|
|
111
|
-
projected["additionalContext"] = str(context)
|
|
112
|
-
return {"hookSpecificOutput": projected}
|
|
113
|
-
if output.get("decision") == "block":
|
|
114
|
-
reason = str(output.get("reason") or "Ultra blocked this tool call")
|
|
115
|
-
return {
|
|
116
|
-
"hookSpecificOutput": {
|
|
117
|
-
"hookEventName": "PreToolUse",
|
|
118
|
-
"permissionDecision": "deny",
|
|
119
|
-
"permissionDecisionReason": reason,
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
return {}
|
|
123
|
-
|
|
124
|
-
if event in {
|
|
125
|
-
"SessionStart", "UserPromptSubmit", "PostToolUse",
|
|
126
|
-
"PostToolUseFailure", "Stop",
|
|
127
|
-
} and context:
|
|
128
|
-
return {
|
|
129
|
-
"hookSpecificOutput": {
|
|
130
|
-
"hookEventName": event,
|
|
131
|
-
"additionalContext": str(context),
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return {}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def _merge_outputs(outputs: list[dict[str, Any]], event: str) -> dict[str, Any]:
|
|
138
|
-
if not outputs:
|
|
139
|
-
return {}
|
|
140
|
-
for output in outputs:
|
|
141
|
-
specific = output.get("hookSpecificOutput")
|
|
142
|
-
if isinstance(specific, dict) and specific.get("permissionDecision") == "deny":
|
|
143
|
-
return output
|
|
144
|
-
contexts = []
|
|
145
|
-
for output in outputs:
|
|
146
|
-
specific = output.get("hookSpecificOutput")
|
|
147
|
-
if isinstance(specific, dict) and specific.get("additionalContext"):
|
|
148
|
-
contexts.append(str(specific["additionalContext"]))
|
|
149
|
-
if contexts:
|
|
150
|
-
return {
|
|
151
|
-
"hookSpecificOutput": {
|
|
152
|
-
"hookEventName": event,
|
|
153
|
-
"additionalContext": "\n\n".join(contexts),
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
return outputs[-1]
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
def run_feature(feature: str, payload: dict[str, Any], feature_args: list[str]) -> dict[str, Any]:
|
|
160
|
-
if feature not in ALLOWED_FEATURES or Path(feature).name != feature:
|
|
161
|
-
return {"systemMessage": f"Ultra hook adapter refused unknown feature: {feature}"}
|
|
162
|
-
script = HOOK_ROOT / feature
|
|
163
|
-
if not script.is_file():
|
|
164
|
-
return {"systemMessage": f"Ultra hook feature is missing: {feature}"}
|
|
165
|
-
|
|
166
|
-
event = str(payload.get("hookEventName") or payload.get("hook_event_name") or "")
|
|
167
|
-
normalized_items = normalize_inputs(payload)
|
|
168
|
-
env = os.environ.copy()
|
|
169
|
-
env["UBP_HOOK_RUNTIME"] = "zcode"
|
|
170
|
-
plugin_data = env.get("ZCODE_PLUGIN_DATA") or env.get("CLAUDE_PLUGIN_DATA")
|
|
171
|
-
if plugin_data:
|
|
172
|
-
env["UBP_HOOK_DATA"] = plugin_data
|
|
173
|
-
cwd = normalized_items[0].get("cwd") if normalized_items else None
|
|
174
|
-
run_cwd = str(cwd) if isinstance(cwd, str) and Path(cwd).is_dir() else os.getcwd()
|
|
175
|
-
|
|
176
|
-
outputs: list[dict[str, Any]] = []
|
|
177
|
-
for normalized in normalized_items:
|
|
178
|
-
proc = subprocess.run(
|
|
179
|
-
[sys.executable, str(script), *feature_args],
|
|
180
|
-
input=json.dumps(normalized),
|
|
181
|
-
text=True,
|
|
182
|
-
capture_output=True,
|
|
183
|
-
cwd=run_cwd,
|
|
184
|
-
env=env,
|
|
185
|
-
timeout=30,
|
|
186
|
-
check=False,
|
|
187
|
-
)
|
|
188
|
-
if proc.stderr:
|
|
189
|
-
print(proc.stderr.rstrip(), file=sys.stderr)
|
|
190
|
-
if proc.returncode != 0:
|
|
191
|
-
outputs.append({
|
|
192
|
-
"systemMessage": (
|
|
193
|
-
f"Ultra hook {feature} failed with exit {proc.returncode}: "
|
|
194
|
-
f"{proc.stderr.strip()[:500]}"
|
|
195
|
-
)
|
|
196
|
-
})
|
|
197
|
-
continue
|
|
198
|
-
outputs.append(adapt_output(_parse_output(proc.stdout), event))
|
|
199
|
-
return _merge_outputs(outputs, event)
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
def main() -> int:
|
|
203
|
-
if len(sys.argv) < 2:
|
|
204
|
-
print(json.dumps({"systemMessage": "Ultra ZCode hook adapter requires a feature name"}))
|
|
205
|
-
return 0
|
|
206
|
-
try:
|
|
207
|
-
payload = json.loads(sys.stdin.read() or "{}")
|
|
208
|
-
if not isinstance(payload, dict):
|
|
209
|
-
payload = {}
|
|
210
|
-
except json.JSONDecodeError as exc:
|
|
211
|
-
print(json.dumps({"systemMessage": f"Ultra hook input parse error: {exc}"}))
|
|
212
|
-
return 0
|
|
213
|
-
try:
|
|
214
|
-
output = run_feature(sys.argv[1], payload, sys.argv[2:])
|
|
215
|
-
except subprocess.TimeoutExpired:
|
|
216
|
-
output = {"systemMessage": f"Ultra hook {sys.argv[1]} timed out"}
|
|
217
|
-
except Exception as exc:
|
|
218
|
-
output = {"systemMessage": f"Ultra hook adapter error in {sys.argv[1]}: {exc}"}
|
|
219
|
-
# ZCode's hook wire schema is strict. Its official contract explicitly accepts an
|
|
220
|
-
# empty stdout stream as a successful no-op, so do not serialize an empty object.
|
|
221
|
-
if output:
|
|
222
|
-
print(json.dumps(output, ensure_ascii=False))
|
|
223
|
-
return 0
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if __name__ == "__main__":
|
|
227
|
-
raise SystemExit(main())
|
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Block a narrow set of externally destructive shell effects."""
|
|
3
|
-
|
|
4
|
-
from __future__ import annotations
|
|
5
|
-
|
|
6
|
-
import hashlib
|
|
7
|
-
import json
|
|
8
|
-
import os
|
|
9
|
-
import re
|
|
10
|
-
import shlex
|
|
11
|
-
import subprocess
|
|
12
|
-
|
|
13
|
-
from _common import emit_context, project_root, read_payload
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
APPROVAL_VAR = "UBP_DANGEROUS_COMMAND_APPROVED"
|
|
17
|
-
APPROVAL_PREFIX = re.compile(rf"^\s*{APPROVAL_VAR}=\S*\s+")
|
|
18
|
-
|
|
19
|
-
HEREDOC = re.compile(
|
|
20
|
-
r"<<-?\s*(['\"]?)(?P<tag>[A-Za-z_]\w*)\1(?P<rest>[^\n]*)\n(?P<body>.*?)^\s*(?P=tag)\s*$",
|
|
21
|
-
re.DOTALL | re.MULTILINE,
|
|
22
|
-
)
|
|
23
|
-
# Commands whose heredoc is a payload rather than a script. Deliberately short: an
|
|
24
|
-
# unlisted sink keeps its body in scope, so the cost of an omission is a false positive,
|
|
25
|
-
# never a missed effect.
|
|
26
|
-
DATA_SINK = re.compile(r"\bgit\s+(?:commit|tag|notes)\b", re.IGNORECASE)
|
|
27
|
-
SEARCH_COMMANDS = {"egrep", "fgrep", "grep", "rg"}
|
|
28
|
-
SHELL_CONTROL = {";", ";;", "&", "&&", "|", "||", "(", ")", "\n"}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def strip_data_heredocs(command: str) -> str:
|
|
32
|
-
"""Drop heredoc bodies that are data for a known sink.
|
|
33
|
-
|
|
34
|
-
Describing an effect inside a commit message is not performing it, and a message
|
|
35
|
-
that mentions one used to be blocked as though it did.
|
|
36
|
-
|
|
37
|
-
Bodies are kept whenever they might be executed: an interpreter reading a heredoc
|
|
38
|
-
runs it, so stripping there would make quoting style a way around this gate. The
|
|
39
|
-
body survives unless the delimiter is quoted, the segment introducing it is a
|
|
40
|
-
listed data sink, and nothing on the heredoc's own line pipes it somewhere else.
|
|
41
|
-
An unquoted delimiter performs parameter, command and arithmetic expansion before
|
|
42
|
-
the sink receives the data, so its body remains executable shell input.
|
|
43
|
-
"""
|
|
44
|
-
def resolve(match: re.Match) -> str:
|
|
45
|
-
segment = re.split(r"\|\||&&|[;&|\n]", command[:match.start()])[-1]
|
|
46
|
-
if (
|
|
47
|
-
not match.group(1)
|
|
48
|
-
or "|" in match.group("rest")
|
|
49
|
-
or not DATA_SINK.search(segment)
|
|
50
|
-
):
|
|
51
|
-
return match.group(0)
|
|
52
|
-
tag = match.group("tag")
|
|
53
|
-
return f"<<{tag}\n{tag}"
|
|
54
|
-
|
|
55
|
-
return HEREDOC.sub(resolve, command)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def effect_of(command: str) -> str:
|
|
59
|
-
"""The command reduced to what it actually does.
|
|
60
|
-
|
|
61
|
-
Strips an inline approval assignment, then any heredoc body that is data for a
|
|
62
|
-
known sink. What remains is the effect: the part worth classifying and hashing.
|
|
63
|
-
|
|
64
|
-
The digest must identify the *effect*, not the spelling of the retry. Without this,
|
|
65
|
-
prepending the variable changes the command text, which changes the digest, which
|
|
66
|
-
means the quoted digest is never the one that matches -- the repair path is a loop.
|
|
67
|
-
|
|
68
|
-
The assigned value is discarded on purpose. Only the process environment authorizes,
|
|
69
|
-
because the command string is composed by the model and honouring an inline value
|
|
70
|
-
would let it approve itself.
|
|
71
|
-
"""
|
|
72
|
-
return strip_data_heredocs(APPROVAL_PREFIX.sub("", command, count=1))
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def is_inert_search(command: str) -> bool:
|
|
76
|
-
"""Return true when a search query is data, not executable shell input."""
|
|
77
|
-
if any(marker in command for marker in ("$(", "<(", ">(", "`")):
|
|
78
|
-
return False
|
|
79
|
-
try:
|
|
80
|
-
lexer = shlex.shlex(command, posix=True, punctuation_chars=";&|()\n")
|
|
81
|
-
lexer.whitespace = " \t\r"
|
|
82
|
-
lexer.whitespace_split = True
|
|
83
|
-
lexer.commenters = ""
|
|
84
|
-
tokens = list(lexer)
|
|
85
|
-
except ValueError:
|
|
86
|
-
return False
|
|
87
|
-
if not tokens or any(token in SHELL_CONTROL for token in tokens):
|
|
88
|
-
return False
|
|
89
|
-
return os.path.basename(tokens[0]) in SEARCH_COMMANDS
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def protected_push(command: str, root) -> bool:
|
|
93
|
-
if not re.search(r"\bgit\s+push\b", command, re.IGNORECASE):
|
|
94
|
-
return False
|
|
95
|
-
if re.search(r"\b(?:main|master|production|prod)\b", command, re.IGNORECASE):
|
|
96
|
-
return True
|
|
97
|
-
result = subprocess.run(
|
|
98
|
-
["git", "branch", "--show-current"], cwd=root, text=True,
|
|
99
|
-
capture_output=True, check=False, timeout=3,
|
|
100
|
-
)
|
|
101
|
-
return result.stdout.strip() in {"main", "master", "production", "prod"}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def destructive_protected_push(command: str, root) -> bool:
|
|
105
|
-
"""Detect history rewrites or deletion, not ordinary additive publication."""
|
|
106
|
-
if not protected_push(command, root):
|
|
107
|
-
return False
|
|
108
|
-
match = re.search(r"\bgit\s+push\b", command, re.IGNORECASE)
|
|
109
|
-
if match is None:
|
|
110
|
-
return False
|
|
111
|
-
segment = re.split(r"\|\||&&|[;&|\n]", command[match.end():], maxsplit=1)[0]
|
|
112
|
-
force_or_delete = re.search(
|
|
113
|
-
r"(?:^|\s)(?:-f|--force(?:-with-lease(?:=\S+)?|-if-includes)?|--delete)(?=\s|$)",
|
|
114
|
-
segment,
|
|
115
|
-
re.IGNORECASE,
|
|
116
|
-
)
|
|
117
|
-
forced_refspec = re.search(r"(?:^|\s)(?:\+\S+|:\S+)(?=\s|$)", segment)
|
|
118
|
-
return force_or_delete is not None or forced_refspec is not None
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def classify(command: str, root) -> str | None:
|
|
122
|
-
if destructive_protected_push(command, root):
|
|
123
|
-
return "protected branch history rewrite or deletion"
|
|
124
|
-
if re.search(r"\b(?:drop\s+(?:table|database|schema)|truncate\s+(?:table\s+)?)\b", command, re.IGNORECASE):
|
|
125
|
-
return "destructive database operation"
|
|
126
|
-
if re.search(r"\b(?:cast\s+send|solana\s+transfer|bitcoin-cli\s+send|near\s+send|aptos\s+move\s+run)\b", command, re.IGNORECASE):
|
|
127
|
-
return "funds or on-chain transaction"
|
|
128
|
-
if re.search(
|
|
129
|
-
r"\b(?:API_KEY|ACCESS_TOKEN|AUTH_TOKEN|SECRET|PASSWORD)\s*=\s*['\"]?(?!\$|\$\{)[^\s'\"]{8,}",
|
|
130
|
-
command,
|
|
131
|
-
re.IGNORECASE,
|
|
132
|
-
):
|
|
133
|
-
return "hard-coded credential"
|
|
134
|
-
if re.search(r"\beval\s+['\"]?\$|\bexec\s*\([^)]*\binput\s*\(", command, re.IGNORECASE):
|
|
135
|
-
return "user-controlled code execution"
|
|
136
|
-
return None
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def advisory(command: str, root) -> str | None:
|
|
140
|
-
if protected_push(command, root):
|
|
141
|
-
return (
|
|
142
|
-
"Ultra observed a protected branch push. This portable hook cannot receive "
|
|
143
|
-
"the host's trusted owner-approval receipt, so it does not replace the host's "
|
|
144
|
-
"native authority flow. Proceed only after explicit owner authorization and "
|
|
145
|
-
"verify the remote result."
|
|
146
|
-
)
|
|
147
|
-
if re.search(
|
|
148
|
-
r"\b(?:alembic\s+upgrade|prisma\s+migrate|knex\s+migrate|sequelize\s+db:migrate|rails\s+db:migrate|manage\.py\s+migrate)\b",
|
|
149
|
-
command,
|
|
150
|
-
re.IGNORECASE,
|
|
151
|
-
):
|
|
152
|
-
return (
|
|
153
|
-
"Ultra observed a database migration command. Confirm the target environment, "
|
|
154
|
-
"backup or rollback path, and owner authorization before any irreversible effect."
|
|
155
|
-
)
|
|
156
|
-
return None
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
def main() -> int:
|
|
160
|
-
payload = read_payload()
|
|
161
|
-
root = project_root(payload) # Idle guard: no `.ultra/` means silent exit.
|
|
162
|
-
if root is None:
|
|
163
|
-
return 0
|
|
164
|
-
tool_input = payload.get("tool_input")
|
|
165
|
-
tool_input = tool_input if isinstance(tool_input, dict) else {}
|
|
166
|
-
command = tool_input.get("command")
|
|
167
|
-
if not isinstance(command, str) or not command.strip():
|
|
168
|
-
return 0
|
|
169
|
-
effect = effect_of(command)
|
|
170
|
-
if is_inert_search(effect):
|
|
171
|
-
return 0
|
|
172
|
-
threat = classify(effect, root)
|
|
173
|
-
if threat is None:
|
|
174
|
-
note = advisory(effect, root)
|
|
175
|
-
if note:
|
|
176
|
-
emit_context("PreToolUse", note)
|
|
177
|
-
return 0
|
|
178
|
-
digest = hashlib.sha256(effect.encode()).hexdigest()
|
|
179
|
-
if os.environ.get(APPROVAL_VAR) == digest:
|
|
180
|
-
return 0
|
|
181
|
-
reason = (
|
|
182
|
-
f"Ultra blocked {threat}. Protected effect: the exact shell command, "
|
|
183
|
-
f"digest {digest}. Authorization comes from the owner's environment, never from "
|
|
184
|
-
f"the command text. Either run it yourself, or export "
|
|
185
|
-
f"{APPROVAL_VAR}={digest} in the environment that launches the agent and ask "
|
|
186
|
-
f"again. Prefixing the assignment onto this command does not authorize it."
|
|
187
|
-
)
|
|
188
|
-
print(json.dumps({
|
|
189
|
-
"decision": "block",
|
|
190
|
-
"reason": reason,
|
|
191
|
-
"hookSpecificOutput": {
|
|
192
|
-
"hookEventName": "PreToolUse",
|
|
193
|
-
"permissionDecision": "deny",
|
|
194
|
-
"permissionDecisionReason": reason,
|
|
195
|
-
},
|
|
196
|
-
}))
|
|
197
|
-
return 0
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if __name__ == "__main__":
|
|
201
|
-
raise SystemExit(main())
|
package/hooks/compact_context.py
DELETED
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Save and restore a disposable compact snapshot from canonical files."""
|
|
3
|
-
|
|
4
|
-
from __future__ import annotations
|
|
5
|
-
|
|
6
|
-
import subprocess
|
|
7
|
-
from datetime import datetime, timezone
|
|
8
|
-
|
|
9
|
-
from _common import (
|
|
10
|
-
RESUME_NAVIGATION_LIMITATION,
|
|
11
|
-
current_task_selection,
|
|
12
|
-
emit_context,
|
|
13
|
-
project_root,
|
|
14
|
-
read_derived_project_file_snapshot,
|
|
15
|
-
read_payload,
|
|
16
|
-
render_task_diagnostics,
|
|
17
|
-
task_sections,
|
|
18
|
-
write_derived_project_file_atomic,
|
|
19
|
-
)
|
|
20
|
-
from session_context import _read_bounded_process_output
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
COMPACT_SNAPSHOT_RELATIVE = ".ultra/.runtime/compact-snapshot.md"
|
|
24
|
-
COMPACT_SNAPSHOT_MAX_BYTES = 1024 * 1024
|
|
25
|
-
COMPACT_GIT_TIMEOUT_SECONDS = 5
|
|
26
|
-
COMPACT_GIT_MAX_OUTPUT_BYTES = 256 * 1024
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def git_output(root, *args: str):
|
|
30
|
-
command = ["git", *args]
|
|
31
|
-
rendered = " ".join(command)
|
|
32
|
-
try:
|
|
33
|
-
process = subprocess.Popen(
|
|
34
|
-
command,
|
|
35
|
-
cwd=root,
|
|
36
|
-
stdin=subprocess.PIPE,
|
|
37
|
-
stdout=subprocess.PIPE,
|
|
38
|
-
stderr=subprocess.PIPE,
|
|
39
|
-
shell=False,
|
|
40
|
-
)
|
|
41
|
-
except FileNotFoundError:
|
|
42
|
-
return "unavailable", {
|
|
43
|
-
"code": "compact_git_missing",
|
|
44
|
-
"message": f"Compact snapshot could not run `{rendered}` because Git is unavailable.",
|
|
45
|
-
"path": str(root),
|
|
46
|
-
"command": command,
|
|
47
|
-
"repair": (
|
|
48
|
-
"Restore a working Git executable and retry PreCompact; canonical task "
|
|
49
|
-
"Acceptance Criteria and the navigational Resume Note remain recoverable."
|
|
50
|
-
),
|
|
51
|
-
}
|
|
52
|
-
except OSError as error:
|
|
53
|
-
return "unavailable", {
|
|
54
|
-
"code": "compact_git_execution_error",
|
|
55
|
-
"message": f"Compact snapshot could not execute `{rendered}`: {error}",
|
|
56
|
-
"path": str(root),
|
|
57
|
-
"command": command,
|
|
58
|
-
"repair": (
|
|
59
|
-
"Repair local Git execution and retry PreCompact; canonical task Acceptance "
|
|
60
|
-
"Criteria and the navigational Resume Note remain recoverable."
|
|
61
|
-
),
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
stdout, _stderr, failure = _read_bounded_process_output(
|
|
65
|
-
process,
|
|
66
|
-
COMPACT_GIT_TIMEOUT_SECONDS,
|
|
67
|
-
COMPACT_GIT_MAX_OUTPUT_BYTES,
|
|
68
|
-
b"",
|
|
69
|
-
)
|
|
70
|
-
if failure == "timeout":
|
|
71
|
-
return "unavailable", {
|
|
72
|
-
"code": "compact_git_timeout",
|
|
73
|
-
"message": (
|
|
74
|
-
f"Compact snapshot stopped `{rendered}` after the "
|
|
75
|
-
f"{COMPACT_GIT_TIMEOUT_SECONDS}-second execution ceiling."
|
|
76
|
-
),
|
|
77
|
-
"path": str(root),
|
|
78
|
-
"command": command,
|
|
79
|
-
"repair": (
|
|
80
|
-
"Retry PreCompact after Git becomes responsive; canonical task Acceptance "
|
|
81
|
-
"Criteria and the navigational Resume Note remain recoverable."
|
|
82
|
-
),
|
|
83
|
-
}
|
|
84
|
-
if failure == "output_too_large":
|
|
85
|
-
return "unavailable", {
|
|
86
|
-
"code": "compact_git_output_too_large",
|
|
87
|
-
"message": (
|
|
88
|
-
f"Compact snapshot stopped `{rendered}` after combined stdout and stderr "
|
|
89
|
-
f"exceeded the {COMPACT_GIT_MAX_OUTPUT_BYTES}-byte ceiling."
|
|
90
|
-
),
|
|
91
|
-
"path": str(root),
|
|
92
|
-
"command": command,
|
|
93
|
-
"repair": (
|
|
94
|
-
f"Retry PreCompact after reducing Git command output below the "
|
|
95
|
-
f"{COMPACT_GIT_MAX_OUTPUT_BYTES}-byte combined stdout/stderr ceiling; "
|
|
96
|
-
"canonical task Acceptance Criteria and the navigational Resume Note remain recoverable."
|
|
97
|
-
),
|
|
98
|
-
}
|
|
99
|
-
if process.returncode != 0:
|
|
100
|
-
return "unavailable", {
|
|
101
|
-
"code": "compact_git_nonzero",
|
|
102
|
-
"message": (
|
|
103
|
-
f"Compact snapshot observed `{rendered}` exit with code "
|
|
104
|
-
f"{process.returncode}."
|
|
105
|
-
),
|
|
106
|
-
"path": str(root),
|
|
107
|
-
"command": command,
|
|
108
|
-
"returncode": process.returncode,
|
|
109
|
-
"repair": (
|
|
110
|
-
"Repair the local Git worktree or repository state and retry PreCompact; "
|
|
111
|
-
"canonical task Acceptance Criteria and the navigational Resume Note remain recoverable."
|
|
112
|
-
),
|
|
113
|
-
}
|
|
114
|
-
return stdout.decode("utf-8", errors="replace").strip(), None
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def save_snapshot(root):
|
|
118
|
-
selection = current_task_selection(root)
|
|
119
|
-
task = selection["task"]
|
|
120
|
-
sections = (
|
|
121
|
-
task_sections(root, task)
|
|
122
|
-
if task
|
|
123
|
-
else {
|
|
124
|
-
"acceptance": "",
|
|
125
|
-
"resume": "",
|
|
126
|
-
"diagnostics": selection["diagnostics"],
|
|
127
|
-
}
|
|
128
|
-
)
|
|
129
|
-
head, head_failure = git_output(root, "rev-parse", "HEAD")
|
|
130
|
-
worktree, worktree_failure = git_output(root, "status", "--short")
|
|
131
|
-
git_diagnostics = [
|
|
132
|
-
failure
|
|
133
|
-
for failure in (head_failure, worktree_failure)
|
|
134
|
-
if failure is not None
|
|
135
|
-
]
|
|
136
|
-
snapshot_diagnostics = [*sections["diagnostics"], *git_diagnostics]
|
|
137
|
-
diagnostics = render_task_diagnostics(snapshot_diagnostics)
|
|
138
|
-
text = [
|
|
139
|
-
"# Ultra Compact Snapshot",
|
|
140
|
-
"",
|
|
141
|
-
f"Generated: {datetime.now(timezone.utc).isoformat()}",
|
|
142
|
-
f"HEAD: {head}",
|
|
143
|
-
f"Task: {task.get('id') if task else 'none'}",
|
|
144
|
-
"",
|
|
145
|
-
"## Acceptance Criteria",
|
|
146
|
-
sections["acceptance"] or "_(none)_",
|
|
147
|
-
"",
|
|
148
|
-
"## Resume Note",
|
|
149
|
-
sections["resume"] or "_(none)_",
|
|
150
|
-
"",
|
|
151
|
-
RESUME_NAVIGATION_LIMITATION,
|
|
152
|
-
"",
|
|
153
|
-
"## Task Diagnostics",
|
|
154
|
-
diagnostics or "_(none)_",
|
|
155
|
-
"",
|
|
156
|
-
"## Worktree",
|
|
157
|
-
"```text",
|
|
158
|
-
worktree,
|
|
159
|
-
"```",
|
|
160
|
-
"",
|
|
161
|
-
]
|
|
162
|
-
write_failure = write_derived_project_file_atomic(
|
|
163
|
-
root,
|
|
164
|
-
COMPACT_SNAPSHOT_RELATIVE,
|
|
165
|
-
"\n".join(text).encode("utf-8"),
|
|
166
|
-
max_bytes=COMPACT_SNAPSHOT_MAX_BYTES,
|
|
167
|
-
code_prefix="compact_snapshot_write",
|
|
168
|
-
label="Ultra compact snapshot",
|
|
169
|
-
)
|
|
170
|
-
published_diagnostics = list(sections["diagnostics"])
|
|
171
|
-
published_diagnostics.extend(git_diagnostics)
|
|
172
|
-
if write_failure is not None:
|
|
173
|
-
published_diagnostics.append(write_failure)
|
|
174
|
-
return published_diagnostics
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
def main() -> int:
|
|
178
|
-
payload = read_payload()
|
|
179
|
-
root = project_root(payload) # Idle guard: no `.ultra/` means silent exit.
|
|
180
|
-
if root is None:
|
|
181
|
-
return 0
|
|
182
|
-
event = str(payload.get("hook_event_name") or payload.get("hookEventName") or "")
|
|
183
|
-
if event == "PreCompact":
|
|
184
|
-
diagnostics = save_snapshot(root)
|
|
185
|
-
if diagnostics:
|
|
186
|
-
emit_context(
|
|
187
|
-
"PreCompact",
|
|
188
|
-
"Ultra compact snapshot diagnostic:\n"
|
|
189
|
-
+ render_task_diagnostics(diagnostics),
|
|
190
|
-
)
|
|
191
|
-
return 0
|
|
192
|
-
if event in {"PostCompact", "SessionStart"} and (
|
|
193
|
-
event == "PostCompact" or payload.get("source") == "compact"
|
|
194
|
-
):
|
|
195
|
-
snapshot, failure = read_derived_project_file_snapshot(
|
|
196
|
-
root,
|
|
197
|
-
COMPACT_SNAPSHOT_RELATIVE,
|
|
198
|
-
max_bytes=COMPACT_SNAPSHOT_MAX_BYTES,
|
|
199
|
-
code_prefix="compact_snapshot_read",
|
|
200
|
-
label="Ultra compact snapshot",
|
|
201
|
-
)
|
|
202
|
-
if snapshot is not None:
|
|
203
|
-
try:
|
|
204
|
-
text = snapshot["bytes"].decode("utf-8")
|
|
205
|
-
except UnicodeDecodeError:
|
|
206
|
-
failure = {
|
|
207
|
-
"code": "compact_snapshot_read_invalid_utf8",
|
|
208
|
-
"message": "Ultra compact snapshot must be valid UTF-8 Markdown.",
|
|
209
|
-
"path": snapshot["path"],
|
|
210
|
-
}
|
|
211
|
-
else:
|
|
212
|
-
emit_context("SessionStart", text)
|
|
213
|
-
return 0
|
|
214
|
-
if failure is not None and not failure.get("code", "").endswith("_missing"):
|
|
215
|
-
emit_context(
|
|
216
|
-
"SessionStart",
|
|
217
|
-
"Ultra compact snapshot diagnostic:\n"
|
|
218
|
-
+ render_task_diagnostics([failure]),
|
|
219
|
-
)
|
|
220
|
-
return 0
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if __name__ == "__main__":
|
|
224
|
-
raise SystemExit(main())
|