simplicio-loop 1.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. simplicio_loop/__init__.py +8 -0
  2. simplicio_loop/_bundle/hooks/hooks.claude.json +20 -0
  3. simplicio_loop/_bundle/hooks/hooks.json +12 -0
  4. simplicio_loop/_bundle/hooks/learn_stop.py +38 -0
  5. simplicio_loop/_bundle/hooks/loop_capture.py +67 -0
  6. simplicio_loop/_bundle/hooks/loop_stop.py +205 -0
  7. simplicio_loop/_bundle/hooks/orient_clamp.py +167 -0
  8. simplicio_loop/_bundle/hooks/orient_rewrite.py +96 -0
  9. simplicio_loop/_bundle/skills/simplicio-compress/SKILL.md +86 -0
  10. simplicio_loop/_bundle/skills/simplicio-learn/SKILL.md +70 -0
  11. simplicio_loop/_bundle/skills/simplicio-loop/SKILL.md +108 -0
  12. simplicio_loop/_bundle/skills/simplicio-orient/SKILL.md +188 -0
  13. simplicio_loop/_bundle/skills/simplicio-review/SKILL.md +94 -0
  14. simplicio_loop/_bundle/skills/simplicio-tasks/SKILL.md +213 -0
  15. simplicio_loop/_bundle/skills/simplicio-tasks/references/azure-devops-adapter.md +69 -0
  16. simplicio_loop/_bundle/skills/simplicio-tasks/references/extension-points.md +60 -0
  17. simplicio_loop/_bundle/skills/simplicio-tasks/references/orchestration.md +131 -0
  18. simplicio_loop/_bundle/skills/simplicio-tasks/references/quality-safety-delivery.md +121 -0
  19. simplicio_loop/_bundle/skills/simplicio-tasks/references/standing-loop-247.md +117 -0
  20. simplicio_loop/_bundle/skills/simplicio-tasks/references/token-economy.md +175 -0
  21. simplicio_loop/_bundle/skills/simplicio-tasks/references/web-evidence.md +93 -0
  22. simplicio_loop/cli.py +76 -0
  23. simplicio_loop-1.0.2.dist-info/METADATA +75 -0
  24. simplicio_loop-1.0.2.dist-info/RECORD +28 -0
  25. simplicio_loop-1.0.2.dist-info/WHEEL +5 -0
  26. simplicio_loop-1.0.2.dist-info/entry_points.txt +2 -0
  27. simplicio_loop-1.0.2.dist-info/licenses/LICENSE +21 -0
  28. simplicio_loop-1.0.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,8 @@
1
+ """simplicio-loop — The Universal Looping AI Orchestrator.
2
+
3
+ A runtime-agnostic super-plugin (6 skills + loop/token hooks) that drains any
4
+ queue of work end-to-end on any LLM/runtime. This package ships the skills and
5
+ hooks and installs them into a runtime's skills location.
6
+ """
7
+
8
+ __version__ = "1.0.2"
@@ -0,0 +1,20 @@
1
+ {
2
+ "hooks": {
3
+ "Stop": [
4
+ {
5
+ "hooks": [
6
+ { "type": "command", "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/loop_stop.py\"" },
7
+ { "type": "command", "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/learn_stop.py\"" }
8
+ ]
9
+ }
10
+ ],
11
+ "PreToolUse": [
12
+ {
13
+ "matcher": "Bash",
14
+ "hooks": [
15
+ { "type": "command", "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/orient_rewrite.py\"" }
16
+ ]
17
+ }
18
+ ]
19
+ }
20
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "version": 1,
3
+ "hooks": {
4
+ "afterAgentResponse": [
5
+ { "command": "python3 ./hooks/loop_capture.py" }
6
+ ],
7
+ "stop": [
8
+ { "command": "python3 ./hooks/loop_stop.py", "loop_limit": null },
9
+ { "command": "python3 ./hooks/learn_stop.py" }
10
+ ]
11
+ }
12
+ }
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env python3
2
+ """simplicio-learn — session-end trigger (Stop / SubagentStop, fail-open).
3
+
4
+ Drops a lightweight marker so the next simplicio-tasks tick (or the human) runs the
5
+ `simplicio-learn` retrospective on the just-finished run. It does NOT itself mine the
6
+ transcript (that's the skill's job, where the model can judge signal) — it only signals
7
+ "there is a finished run worth learning from", with the transcript path if the host gave one.
8
+
9
+ Fail-open and silent: never blocks stopping, never errors out loud.
10
+ """
11
+ import json
12
+ import os
13
+ import sys
14
+ import time
15
+
16
+ LEARN_DIR = os.path.join(".orchestrator", "learn")
17
+ QUEUE = os.path.join(LEARN_DIR, "pending.jsonl")
18
+
19
+
20
+ def main():
21
+ try:
22
+ raw = sys.stdin.read()
23
+ data = json.loads(raw) if raw.strip() else {}
24
+ os.makedirs(LEARN_DIR, exist_ok=True)
25
+ rec = {
26
+ "at": int(time.time()),
27
+ "transcript": data.get("transcript_path") or data.get("transcriptPath"),
28
+ "session": data.get("session_id") or data.get("sessionId"),
29
+ }
30
+ with open(QUEUE, "a", encoding="utf-8") as f:
31
+ f.write(json.dumps(rec) + "\n")
32
+ except Exception:
33
+ pass
34
+ sys.exit(0) # never block the stop
35
+
36
+
37
+ if __name__ == "__main__":
38
+ main()
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env python3
2
+ """simplicio-loop — capture hook (Cursor `afterAgentResponse`).
3
+
4
+ Runs after every agent turn. Detects an evidence-backed completion-promise and
5
+ raises the `done` flag for the stop hook to act on. Detection and termination are
6
+ split on purpose (the ralph-loop two-hook pattern): this script NEVER stops the
7
+ loop, it only raises the flag. Fire-and-forget, always exit 0.
8
+
9
+ On Claude Code (which has no separate post-response hook) this script is unused —
10
+ `loop_stop.py` folds capture in by reading the transcript. Harmless if wired anyway.
11
+ """
12
+ import json
13
+ import os
14
+ import re
15
+ import sys
16
+
17
+ LOOP_DIR = os.path.join(".orchestrator", "loop")
18
+ SCRATCHPAD = os.path.join(LOOP_DIR, "scratchpad.md")
19
+ DONE_FLAG = os.path.join(LOOP_DIR, "done")
20
+ LAST_RESP = os.path.join(LOOP_DIR, "last_response.txt")
21
+
22
+ PROMISE_RE = re.compile(r"<promise>\s*(.*?)\s*</promise>", re.IGNORECASE | re.DOTALL)
23
+ EVIDENCE_RE = re.compile(
24
+ r"(https?://\S+/pull/\d+)|(\b(pass|passed|passing|green|ok)\b)|([\w./-]+:\d+)|([✓✅])",
25
+ re.IGNORECASE,
26
+ )
27
+
28
+
29
+ def main():
30
+ try:
31
+ raw = sys.stdin.read()
32
+ data = json.loads(raw) if raw.strip() else {}
33
+ resp = data.get("text", "") or ""
34
+ if not resp:
35
+ sys.exit(0)
36
+ # Stash the response so a unified stop hook can read it cross-runtime.
37
+ try:
38
+ os.makedirs(LOOP_DIR, exist_ok=True)
39
+ with open(LAST_RESP, "w", encoding="utf-8") as f:
40
+ f.write(resp)
41
+ except OSError:
42
+ pass
43
+ if not os.path.exists(SCRATCHPAD):
44
+ sys.exit(0)
45
+ with open(SCRATCHPAD, encoding="utf-8") as f:
46
+ content = f.read()
47
+ m_prom = re.search(r"^completion_promise:\s*(.*)$", content, re.M)
48
+ if not m_prom:
49
+ sys.exit(0)
50
+ promise = m_prom.group(1).strip().strip('"')
51
+ if promise in ("", "null"):
52
+ sys.exit(0)
53
+ evidence_required = "evidence_required: false" not in content.lower()
54
+ m = PROMISE_RE.search(resp)
55
+ if m and m.group(1).strip() == promise:
56
+ if (not evidence_required) or EVIDENCE_RE.search(resp):
57
+ try:
58
+ open(DONE_FLAG, "w").close() # raise the flag; stop hook acts
59
+ except OSError:
60
+ pass
61
+ except Exception:
62
+ pass
63
+ sys.exit(0)
64
+
65
+
66
+ if __name__ == "__main__":
67
+ main()
@@ -0,0 +1,205 @@
1
+ #!/usr/bin/env python3
2
+ """simplicio-loop — stop hook (cross-runtime, cross-platform).
3
+
4
+ Fires when an agent turn ends. Decides whether to RE-FEED the goal (continue the
5
+ Ralph loop) or let the agent STOP. Works under Claude Code (Stop hook) and Cursor
6
+ (stop hook); detects the runtime from env and emits the matching control object.
7
+
8
+ SAFETY: fail-open. On ANY error, ambiguity, or missing state, ALLOW STOP — a buggy
9
+ hook must never trap the agent in an endless loop. The real guards are the
10
+ `max_iterations` cap and the $ budget kill-switch, never this script's cleverness.
11
+
12
+ State (single source of truth): .orchestrator/loop/scratchpad.md (+ sibling `done` flag)
13
+ Reads stdin JSON from the host (Claude: {transcript_path,...}; Cursor: {text,...}).
14
+ """
15
+ import json
16
+ import os
17
+ import re
18
+ import sys
19
+
20
+ LOOP_DIR = os.path.join(".orchestrator", "loop")
21
+ SCRATCHPAD = os.path.join(LOOP_DIR, "scratchpad.md")
22
+ DONE_FLAG = os.path.join(LOOP_DIR, "done")
23
+ LAST_RESP = os.path.join(LOOP_DIR, "last_response.txt")
24
+ STOP_SIGNAL = os.path.join(".orchestrator", "STOP")
25
+ BUDGET = os.path.join(".orchestrator", "loop-budget.json")
26
+
27
+ EVIDENCE_RE = re.compile(
28
+ r"(https?://\S+/pull/\d+)" # a PR URL
29
+ r"|(\b(pass|passed|passing|green|ok)\b)" # a gate verdict
30
+ r"|([\w./-]+:\d+)" # a file:line receipt
31
+ r"|([✓✅])",
32
+ re.IGNORECASE,
33
+ )
34
+ PROMISE_RE = re.compile(r"<promise>\s*(.*?)\s*</promise>", re.IGNORECASE | re.DOTALL)
35
+
36
+
37
+ def allow_stop():
38
+ """Emit nothing actionable → the agent is allowed to stop. Always exit 0."""
39
+ sys.exit(0)
40
+
41
+
42
+ def cleanup_and_stop():
43
+ for p in (SCRATCHPAD, DONE_FLAG, LAST_RESP):
44
+ try:
45
+ if os.path.exists(p):
46
+ os.remove(p)
47
+ except OSError:
48
+ pass
49
+ allow_stop()
50
+
51
+
52
+ def read_stdin_json():
53
+ try:
54
+ raw = sys.stdin.read()
55
+ return json.loads(raw) if raw.strip() else {}
56
+ except Exception:
57
+ return {}
58
+
59
+
60
+ def parse_frontmatter(text):
61
+ """Return (meta dict, body str) or (None, None) on corruption."""
62
+ if not text.startswith("---"):
63
+ return None, None
64
+ parts = text.split("---", 2)
65
+ if len(parts) < 3:
66
+ return None, None
67
+ meta = {}
68
+ for line in parts[1].splitlines():
69
+ if ":" in line:
70
+ k, _, v = line.partition(":")
71
+ meta[k.strip()] = v.strip().strip('"')
72
+ return meta, parts[2].strip()
73
+
74
+
75
+ def last_assistant_text(stdin):
76
+ # Cursor passes the response text inline.
77
+ if isinstance(stdin.get("text"), str):
78
+ return stdin["text"]
79
+ # Cursor capture hook may have stashed it.
80
+ if os.path.exists(LAST_RESP):
81
+ try:
82
+ with open(LAST_RESP, encoding="utf-8") as f:
83
+ return f.read()
84
+ except OSError:
85
+ pass
86
+ # Claude passes a transcript path (JSONL); read the last assistant message.
87
+ tp = stdin.get("transcript_path")
88
+ if tp and os.path.exists(tp):
89
+ try:
90
+ txt = ""
91
+ with open(tp, encoding="utf-8") as f:
92
+ for line in f:
93
+ try:
94
+ ev = json.loads(line)
95
+ except Exception:
96
+ continue
97
+ if ev.get("role") == "assistant" or ev.get("type") == "assistant":
98
+ msg = ev.get("message", ev)
99
+ content = msg.get("content", "")
100
+ if isinstance(content, list):
101
+ content = " ".join(
102
+ c.get("text", "") for c in content if isinstance(c, dict)
103
+ )
104
+ txt = content or txt
105
+ return txt
106
+ except OSError:
107
+ return ""
108
+ return ""
109
+
110
+
111
+ def budget_halted():
112
+ try:
113
+ if not os.path.exists(BUDGET):
114
+ return False
115
+ with open(BUDGET, encoding="utf-8") as f:
116
+ b = json.load(f)
117
+ if str(b.get("state", "")).lower() == "halted":
118
+ return True
119
+ ceiling = float(b.get("daily_usd_ceiling", 0) or 0)
120
+ spent = float(b.get("spent_usd_today", 0) or 0)
121
+ return ceiling > 0 and spent >= ceiling
122
+ except Exception:
123
+ return False # fail-open: budget unreadable ≠ trap
124
+
125
+
126
+ def emit_refeed(followup):
127
+ """Emit the re-feed in BOTH schemas; each runtime reads its own key."""
128
+ out = {
129
+ "followup_message": followup, # Cursor
130
+ "decision": "block", # Claude Code Stop hook
131
+ "reason": followup,
132
+ }
133
+ sys.stdout.write(json.dumps(out))
134
+ sys.exit(0)
135
+
136
+
137
+ def main():
138
+ try:
139
+ # Explicit STOP signal beats everything.
140
+ if os.path.exists(STOP_SIGNAL):
141
+ cleanup_and_stop()
142
+ # (1) No active loop.
143
+ if not os.path.exists(SCRATCHPAD):
144
+ allow_stop()
145
+ with open(SCRATCHPAD, encoding="utf-8") as f:
146
+ content = f.read()
147
+ meta, body = parse_frontmatter(content)
148
+ # (2) Corrupt state.
149
+ if meta is None:
150
+ cleanup_and_stop()
151
+ try:
152
+ iteration = int(meta.get("iteration", "1"))
153
+ max_iter = int(meta.get("max_iterations", "0"))
154
+ except ValueError:
155
+ cleanup_and_stop()
156
+ promise = meta.get("completion_promise", "null")
157
+ promise = None if promise in (None, "null", "") else promise
158
+ evidence_required = str(meta.get("evidence_required", "true")).lower() != "false"
159
+
160
+ stdin = read_stdin_json()
161
+ resp = last_assistant_text(stdin)
162
+
163
+ # Completion detection (capture folded in for single-hook runtimes like Claude).
164
+ if promise and resp:
165
+ m = PROMISE_RE.search(resp)
166
+ if m and m.group(1).strip() == promise.strip():
167
+ has_evidence = bool(EVIDENCE_RE.search(resp))
168
+ if (not evidence_required) or has_evidence:
169
+ cleanup_and_stop() # (3) promise fulfilled → stop
170
+ # promise without evidence → ignore, keep looping
171
+ # (3') Cursor capture may have raised the flag.
172
+ if os.path.exists(DONE_FLAG):
173
+ cleanup_and_stop()
174
+ # (4) Iteration cap.
175
+ if max_iter > 0 and iteration >= max_iter:
176
+ cleanup_and_stop()
177
+ # (5) Budget halted.
178
+ if budget_halted():
179
+ cleanup_and_stop()
180
+ # (6) Continue: bump iteration in place, re-feed the goal body.
181
+ nxt = iteration + 1
182
+ new_content = re.sub(
183
+ r"^iteration:\s*\d+", "iteration: %d" % nxt, content, count=1, flags=re.M
184
+ )
185
+ try:
186
+ tmp = SCRATCHPAD + ".tmp"
187
+ with open(tmp, "w", encoding="utf-8") as f:
188
+ f.write(new_content)
189
+ os.replace(tmp, SCRATCHPAD)
190
+ except OSError:
191
+ allow_stop() # can't persist progress → don't risk an unbounded loop
192
+ promise_hint = (
193
+ " To finish: output <promise>%s</promise> ONLY when genuinely true AND "
194
+ "backed by a passing gate." % promise
195
+ if promise
196
+ else ""
197
+ )
198
+ header = "[simplicio-loop iteration %d.%s]" % (nxt, promise_hint)
199
+ emit_refeed(header + "\n\n" + (body or ""))
200
+ except Exception:
201
+ allow_stop() # fail-open, always
202
+
203
+
204
+ if __name__ == "__main__":
205
+ main()
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env python3
2
+ """simplicio-orient — command clamp wrapper (rtk core, portable).
3
+
4
+ Run a dev command and return its output REDUCED before it reaches the model context:
5
+ success-collapse · dedup-with-counts · signal-tiered caps · tee-cache on failure.
6
+
7
+ Usage:
8
+ python3 hooks/orient_clamp.py -- <command> [args...]
9
+ python3 hooks/orient_clamp.py --json -- <command> [args...] # machine summary
10
+
11
+ Works on Windows/macOS/Linux (pure Python, no shell-specific syntax). Safe and
12
+ fail-open: on ANY internal error it prints the raw output and propagates the REAL
13
+ exit code — it can never turn "task works" into "task dead".
14
+
15
+ Config (optional): .orchestrator/orient.toml
16
+ [tee] mode = "failures" | "always" | "never" (default failures)
17
+ [hooks] exclude_commands = ["curl", "wget", "playwright", "ssh", "vim", "less"]
18
+ Excluded commands run RAW (streaming/interactive/binary must not be filtered).
19
+ """
20
+ import os
21
+ import re
22
+ import subprocess
23
+ import sys
24
+ import time
25
+
26
+ try: # Windows consoles default to cp1252 — force UTF-8 so arbitrary output never crashes.
27
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
28
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
29
+ except Exception:
30
+ pass
31
+
32
+ CAP_ERRORS = 20
33
+ CAP_WARNINGS = 10
34
+ CAP_LIST = 20
35
+ TEE_DIR = os.path.join(".orchestrator", "tee")
36
+ CONFIG = os.path.join(".orchestrator", "orient.toml")
37
+ DEFAULT_EXCLUDES = ["curl", "wget", "playwright", "ssh", "vim", "less", "top", "htop"]
38
+
39
+ ERR_RE = re.compile(r"\b(error|fail(ed|ure)?|panic|exception|fatal|traceback)\b", re.I)
40
+ WARN_RE = re.compile(r"\bwarn(ing)?\b", re.I)
41
+ CLEAN_RE = re.compile(
42
+ r"^(ok|done|success|up.to.date|no changes|nothing to commit|pass(ed|ing)?)", re.I
43
+ )
44
+
45
+
46
+ def load_config():
47
+ mode, excludes = "failures", list(DEFAULT_EXCLUDES)
48
+ try:
49
+ if os.path.exists(CONFIG):
50
+ txt = open(CONFIG, encoding="utf-8").read()
51
+ m = re.search(r'mode\s*=\s*"(\w+)"', txt)
52
+ if m:
53
+ mode = m.group(1)
54
+ m = re.search(r"exclude_commands\s*=\s*\[(.*?)\]", txt, re.S)
55
+ if m:
56
+ excludes = re.findall(r'"([^"]+)"', m.group(1)) or excludes
57
+ except Exception:
58
+ pass
59
+ return mode, excludes
60
+
61
+
62
+ def is_excluded(cmd, excludes):
63
+ joined = " ".join(cmd).lower()
64
+ return any(joined.startswith(x.lower()) or (" " + x.lower() + " ") in (" " + joined + " ")
65
+ for x in excludes)
66
+
67
+
68
+ def tee_write(cmd, raw):
69
+ try:
70
+ os.makedirs(TEE_DIR, exist_ok=True)
71
+ slug = re.sub(r"[^a-z0-9]+", "_", " ".join(cmd).lower())[:40].strip("_")
72
+ path = os.path.join(TEE_DIR, "%d_%s.log" % (int(time.time()), slug or "cmd"))
73
+ with open(path, "w", encoding="utf-8", errors="replace") as f:
74
+ f.write(raw)
75
+ return path
76
+ except Exception:
77
+ return None
78
+
79
+
80
+ def dedup_counts(lines):
81
+ out, prev, n = [], None, 0
82
+ for ln in lines:
83
+ if ln == prev:
84
+ n += 1
85
+ else:
86
+ if prev is not None:
87
+ out.append(prev if n == 1 else "%s x%d" % (prev, n))
88
+ prev, n = ln, 1
89
+ if prev is not None:
90
+ out.append(prev if n == 1 else "%s ×%d" % (prev, n))
91
+ return out
92
+
93
+
94
+ def clamp(raw, exit_code):
95
+ lines = raw.splitlines()
96
+ err = [l for l in lines if ERR_RE.search(l)]
97
+ warn = [l for l in lines if WARN_RE.search(l) and l not in err]
98
+ # success-collapse: clean exit, no error/warning → one line
99
+ if exit_code == 0 and not err and not warn:
100
+ first = next((l for l in lines if l.strip()), "")
101
+ if not lines or CLEAN_RE.search(first.strip()) or len(lines) <= 1:
102
+ return (first.strip() or "ok"), False
103
+ kept = dedup_counts([l for l in lines if l.strip()])[:CAP_LIST]
104
+ clipped = len(kept) < len([l for l in lines if l.strip()])
105
+ return "\n".join(kept), clipped
106
+ # has errors/warnings → keep signal, dedup
107
+ body = dedup_counts(err)[:CAP_ERRORS] + dedup_counts(warn)[:CAP_WARNINGS]
108
+ clipped = len(err) > CAP_ERRORS or len(warn) > CAP_WARNINGS
109
+ if not body: # underflow-safe: fall back to a tail of raw
110
+ body = [l for l in lines if l.strip()][-CAP_ERRORS:]
111
+ clipped = clipped or len(lines) > CAP_ERRORS
112
+ return "\n".join(body), clipped
113
+
114
+
115
+ def main():
116
+ argv = sys.argv[1:]
117
+ as_json = False
118
+ if argv and argv[0] == "--json":
119
+ as_json, argv = True, argv[1:]
120
+ if not argv or argv[0] != "--" or len(argv) < 2:
121
+ sys.stderr.write("usage: orient_clamp.py [--json] -- <command> [args...]\n")
122
+ sys.exit(2)
123
+ cmd = argv[1:]
124
+ mode, excludes = load_config()
125
+
126
+ # Excluded → run raw, stream through unchanged.
127
+ if is_excluded(cmd, excludes):
128
+ try:
129
+ return sys.exit(subprocess.call(cmd))
130
+ except Exception as e:
131
+ sys.stderr.write("orient_clamp passthrough error: %s\n" % e)
132
+ sys.exit(1)
133
+
134
+ try:
135
+ proc = subprocess.run(cmd, capture_output=True, text=True, errors="replace")
136
+ raw = (proc.stdout or "") + (proc.stderr or "")
137
+ code = proc.returncode
138
+ except FileNotFoundError:
139
+ sys.stderr.write("orient_clamp: command not found: %s\n" % cmd[0])
140
+ sys.exit(127)
141
+ except Exception:
142
+ # fail-open: re-run inheriting stdio so the user still gets the real result
143
+ try:
144
+ sys.exit(subprocess.call(cmd))
145
+ except Exception:
146
+ sys.exit(1)
147
+
148
+ reduced, clipped = clamp(raw, code)
149
+ tee_path = None
150
+ if mode == "always" or (mode == "failures" and (code != 0 or (clipped and code != 0))):
151
+ tee_path = tee_write(cmd, raw)
152
+
153
+ if as_json:
154
+ import json
155
+ print(json.dumps({
156
+ "exit": code, "reduced": reduced, "tee": tee_path,
157
+ "raw_chars": len(raw), "reduced_chars": len(reduced),
158
+ }))
159
+ else:
160
+ print(reduced)
161
+ if tee_path:
162
+ print("[full output: %s]" % tee_path)
163
+ sys.exit(code) # propagate the REAL exit code
164
+
165
+
166
+ if __name__ == "__main__":
167
+ main()
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env python3
2
+ """simplicio-orient — auto-rewrite hook (PreToolUse, best-effort, fail-open).
3
+
4
+ Transparently routes a bare heavy READ-ONLY command through `orient_clamp.py` so its
5
+ output is reduced before it reaches context — rtk's `init -g` idea. Guarantees adoption
6
+ across the main agent AND subagents at zero token overhead, where the host supports
7
+ PreToolUse input rewriting (newer Claude Code; Cursor).
8
+
9
+ CONSERVATIVE BY DESIGN (safety > savings):
10
+ • Only wraps a small allowlist of read-only commands (git status/log/diff/show, ls,
11
+ rg, cat/type, and known test/build runners).
12
+ • NEVER touches writes, excluded commands, or compound commands (&&, ||, |, ;, >, <,
13
+ backticks, $()) — those run unchanged.
14
+ • Fail-open: on ANY error or unknown protocol, allow the command unchanged. If the host
15
+ ignores the rewrite, the command simply runs raw — never broken.
16
+
17
+ Disabled unless wired explicitly (see hooks/README.md). Honest note: where a runtime
18
+ cannot rewrite tool input, this no-ops; use `orient_clamp.py` as a manual wrapper instead.
19
+ """
20
+ import json
21
+ import os
22
+ import re
23
+ import sys
24
+
25
+ CLAMP = os.path.join(os.path.dirname(os.path.abspath(__file__)), "orient_clamp.py")
26
+ CONFIG = os.path.join(".orchestrator", "orient.toml")
27
+ DEFAULT_EXCLUDES = ["curl", "wget", "playwright", "ssh", "vim", "less", "top", "htop"]
28
+
29
+ # read-only, output-heavy commands worth clamping (prefix match on the first token(s))
30
+ ALLOW = [
31
+ "git status", "git log", "git diff", "git show", "git branch",
32
+ "ls", "ll", "dir", "rg ", "grep ", "cat ", "type ", "tree",
33
+ "cargo check", "cargo test", "cargo clippy", "cargo build",
34
+ "npm test", "npm run", "pnpm test", "yarn test", "jest", "vitest",
35
+ "go test", "go build", "go vet", "pytest", "python -m pytest",
36
+ "mvn ", "gradle ", "tsc", "eslint", "ruff", "golangci-lint",
37
+ ]
38
+ UNSAFE = re.compile(r"[|&;><`]|\$\(|>>|<<") # compound / redirect / substitution
39
+
40
+
41
+ def allow_unchanged():
42
+ # Emit a permissive decision that does not modify anything; host runs the command raw.
43
+ print(json.dumps({
44
+ "hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "allow"}
45
+ }))
46
+ sys.exit(0)
47
+
48
+
49
+ def load_excludes():
50
+ excludes = list(DEFAULT_EXCLUDES)
51
+ try:
52
+ if os.path.exists(CONFIG):
53
+ m = re.search(r"exclude_commands\s*=\s*\[(.*?)\]",
54
+ open(CONFIG, encoding="utf-8").read(), re.S)
55
+ if m:
56
+ excludes = re.findall(r'"([^"]+)"', m.group(1)) or excludes
57
+ except Exception:
58
+ pass
59
+ return excludes
60
+
61
+
62
+ def main():
63
+ try:
64
+ raw = sys.stdin.read()
65
+ data = json.loads(raw) if raw.strip() else {}
66
+ ti = data.get("tool_input", data.get("toolInput", {})) or {}
67
+ cmd = (ti.get("command") or ti.get("cmd") or "").strip()
68
+ if not cmd:
69
+ allow_unchanged()
70
+ low = cmd.lower()
71
+ # already wrapped, unsafe-shape, or excluded → leave raw
72
+ if "orient_clamp" in low or UNSAFE.search(cmd):
73
+ allow_unchanged()
74
+ if any(low.startswith(x.lower()) or (" " + x.lower()) in (" " + low)
75
+ for x in load_excludes()):
76
+ allow_unchanged()
77
+ if not any(low.startswith(a.lower()) for a in ALLOW):
78
+ allow_unchanged()
79
+ # eligible → rewrite to route through the clamp wrapper
80
+ new_cmd = 'python3 "%s" -- %s' % (CLAMP, cmd)
81
+ out = {
82
+ "hookSpecificOutput": {
83
+ "hookEventName": "PreToolUse",
84
+ "permissionDecision": "allow",
85
+ "updatedInput": {"command": new_cmd}, # newer Claude Code
86
+ },
87
+ "updatedInput": {"command": new_cmd}, # alt schema; ignored if unknown
88
+ }
89
+ print(json.dumps(out))
90
+ sys.exit(0)
91
+ except Exception:
92
+ allow_unchanged() # fail-open, always
93
+
94
+
95
+ if __name__ == "__main__":
96
+ main()