riskrails 3.0.0 → 3.1.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -2
- package/README.md +259 -812
- package/agents/ai-reviewer.md.tmpl +6 -4
- package/ai/policies/model-routing.md +7 -7
- package/ai/policies/production.md +3 -3
- package/ai/policies/release.md +2 -2
- package/ai/policies/review-economy.md +34 -32
- package/ai/using-riskrails.md +15 -0
- package/bin/claude-1m +15 -14
- package/bin/riskrails.js +113 -37
- package/codex/hooks.json +25 -2
- package/hooks/AGENTS.md +2 -2
- package/hooks/CLAUDE.md +2 -2
- package/hooks/ai-guard.py +29 -8
- package/hooks/ai-path-guard-defaults.json +3 -1
- package/hooks/codex-hooks.json +25 -2
- package/hooks/context-guard.py +252 -36
- package/hooks/dialog-gate.py +105 -0
- package/hooks/guard/common.py +7 -0
- package/hooks/guard/decision.py +123 -0
- package/hooks/guard/dialog.py +598 -0
- package/hooks/guard/git.py +49 -3
- package/hooks/guard/paths.py +89 -19
- package/hooks/hooks.json +37 -2
- package/hooks/memory-pre-push.py +28 -0
- package/hooks/project-scaffold.py +1 -5
- package/hooks/runtime-gate.py +65 -18
- package/hooks/update_notice.py +141 -0
- package/instructions/routing.md +4 -4
- package/instructions/stub.md +3 -3
- package/package.json +1 -1
- package/profiles/max.json +3 -1
- package/profiles/pro.json +3 -1
- package/scripts/merge-codex-config.py +134 -0
- package/scripts/resolve-profile.py +15 -3
- package/settings.common.json +39 -2
- package/skills/AGENTS.md +3 -3
- package/skills/CLAUDE.md +3 -3
- package/skills/ai-init/templates/.ai/AGENTS.md +3 -3
- package/skills/ai-init/templates/.ai/VERSION +1 -1
- package/skills/ai-init/templates/.ai/policies/dialog.json +1 -0
- package/skills/ai-init/templates/.ai/policies/safety.md +22 -2
- package/skills/ai-init/templates/.ai/policies/security.md +7 -1
- package/skills/ai-init/templates/.ai/policies/testing.md +19 -6
- package/skills/ai-init/templates/.ai/policies/weight.json +1 -0
- package/skills/ai-init/templates/.ai/policies/weight.md +2 -2
- package/skills/ai-init/templates/.ai/state/README.md +9 -6
- package/skills/ai-init/templates/AGENTS.block.md +2 -2
- package/skills/ai-init/templates/CLAUDE.block.md +2 -2
- package/skills/ai-init/templates/GEMINI.block.md +2 -2
- package/skills/ai-init/templates/gitignore.snippet +1 -0
- package/skills/ai-init/templates/junie-guidelines.block.md +2 -2
- package/skills/ai-task/memory_compact.py +968 -0
- package/skills/ai-task/memory_format.py +775 -0
- package/skills/ai-task/memory_store.py +528 -0
- package/skills/ai-task/memory_sync.py +1571 -0
- package/skills/ai-task/sensors.py +25 -5
- package/skills/ai-task/state.py +999 -79
- package/skills/ai-task/weight.py +3 -3
- package/skills/project-init/templates/constitution.md +1 -1
- package/skills/project-init/templates/sdlc-README.md +20 -14
- package/skills/project-update/history/blobs/00289f98434fed230eebe768dfb3425f83d737866a3aebc97aa95167c4fa0c6e +131 -0
- package/skills/project-update/history/blobs/079a91f976ad69b5cf49ccb9a28849576e4735ddd9485fc26599e528a69fd2f4 +30 -0
- package/skills/project-update/history/blobs/07ff7fa6f93d471ff533f553b4e523e4105ccde1f3dab70132c5237728f68422 +13 -0
- package/skills/project-update/history/blobs/0ef4a360c76da290173dea0f8073949cb0f8113710a38df95acae5d96bdd1286 +123 -0
- package/skills/project-update/history/blobs/164cd34c182de88c33de7df35beb29200d792419e1c13a5f0b2104f944e57be7 +1 -0
- package/skills/project-update/history/blobs/18c7a2753ce9cb6f6d3d1e9d5d1125d75950b879d56b7325225b200869631b29 +15 -0
- package/skills/project-update/history/blobs/1d1d907449ec74594d0ee923ac142ae746b9aedf0a1eda9469f075a44d23e3d7 +13 -0
- package/skills/project-update/history/blobs/23a42febbacaa7b52a802d428033a99c0694391c81d51fe3b6e1dff608a641d5 +67 -0
- package/skills/project-update/history/blobs/25d4f2a86deb5e2574bb3210b67bb24fcc4afb19f93a7b65a057daa874a9d18e +1 -0
- package/skills/project-update/history/blobs/48d9950f16e31d55475f566a80dc95f64b35f1c804d2f51729e313a4f29bac8d +13 -0
- package/skills/project-update/history/blobs/5353ca529c46f2443e12947ec205a6887fb594a4de1f87f0eb1c0e6c0f826133 +49 -0
- package/skills/project-update/history/blobs/5a93f78b4ed4d416c171139b2ce610a117f9c1dd46fbb56ca86729cb4c53d10c +6 -0
- package/skills/project-update/history/blobs/5f65bc7ab3fc3c4e75ae05d49eaf4331fafc398c661abe33c69f7380a720344c +32 -0
- package/skills/project-update/history/blobs/6a3be0e624e5ad91e44d08857b7183ee53b7903e1e8d56ff196dbc9dde271e4a +75 -0
- package/skills/project-update/history/blobs/7170553410bba904ffb7edb4b231e3b3579b85542d84de717299980938f82c9d +13 -0
- package/skills/project-update/history/blobs/774b1e3a04ef0d49d33c44a2cfecd92eb6f6b738a12b6a681103ba7fc13e7b86 +67 -0
- package/skills/project-update/history/blobs/7be527cd648b27cca6e267de30f8e33a2ce17d5631a82694f23ae5c0623efe1f +15 -0
- package/skills/project-update/history/blobs/7ee99edafe7cec45ff577376c0d0178fdb2690134d4656488f46e129aba3cc5a +67 -0
- package/skills/project-update/history/blobs/7f063e79195a5c85c37b7260dd20c184d3b9bea85e37202e0bbca73ad84bba49 +13 -0
- package/skills/project-update/history/blobs/8b3ef9586491a426767c01da5d01bdd6b0655c576c68c4cc6dfb093367817dc0 +67 -0
- package/skills/project-update/history/blobs/917df3320d778ddbaa5c5c7742bc4046bf803c36ed2b050f30844ed206783469 +1 -0
- package/skills/project-update/history/blobs/935af1019931058ec3a0b834fe230e127ec914f75edcc4a6c589cfe733b0b979 +13 -0
- package/skills/project-update/history/blobs/ae5ce131d6bae401199ff32d8c3dc4d8aec188010a9111624ac64506e2116ff4 +84 -0
- package/skills/project-update/history/blobs/c8573ca1f810ce6c4ac49456a4c1725b93e0d3b74c54ce95d46d8747777895f0 +13 -0
- package/skills/project-update/history/blobs/ca4499d829268c8766fb3b825a515e88dc55c237e9d202816b50959b4dac031f +13 -0
- package/skills/project-update/history/blobs/d6e5b34d475e7187d2a8cb2aac7e7f1a427142baf6461bda387d70ceb4d8a8c1 +30 -0
- package/skills/project-update/history/blobs/e120ff4e32b2e950fc94ada4e787d57dc9c790da37223e5a202d74f7e3aadf80 +13 -0
- package/skills/project-update/history/blobs/e9adb7a9905eb719ac78ab96671f055da53e5001810f5ad6d6c94306b0323fe7 +74 -0
- package/skills/project-update/history/blobs/fae3064e648cdc55be4aeb1177525b302a5e34ebd752e4a904f47dc0f4557b96 +71 -0
- package/skills/project-update/history/index.json +60 -20
- package/skills/project-update/migrations/0009_state_file.py +25 -0
- package/skills/project-update/migrations/0010_dialog_switch.py +30 -0
- package/skills/project-update/migrations/0011_retire_risk_tiers.py +37 -0
- package/skills/project-update/migrations/README.md +2 -1
- package/skills/project-update/migrations/__init__.py +6 -1
- package/skills/project-update/render_instructions.py +1 -1
- package/skills/project-update/update.py +385 -89
- package/skills/rr-build/SKILL.md +14 -5
- package/skills/rr-build-all/SKILL.md +190 -0
- package/skills/rr-plan/SKILL.md +16 -4
- package/skills/rr-review/SKILL.md +24 -9
- package/skills/rr-setup/SKILL.md +15 -2
- package/skills/rr-setup/setup.py +7 -1
- package/skills/rr-ship/SKILL.md +16 -8
- package/skills/rr-spec/SKILL.md +4 -2
- package/skills/rr-status/SKILL.md +2 -2
- package/skills/usage-report/usage-report.py +45 -10
- package/skills/ai-init/templates/.ai/policies/risk-tiers.json +0 -628
- package/skills/ai-init/templates/.ai/policies/risk-tiers.md +0 -180
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""R36.12: the gate while a dialog decision is open — the first check of
|
|
2
|
+
hooks/ai-guard.py, on every tool (the Claude matcher is `*`).
|
|
3
|
+
|
|
4
|
+
With no .ai/state/decision.json the check costs one stat per ancestor
|
|
5
|
+
directory and allows. With the dialog on and a decision of this session
|
|
6
|
+
pending, every tool is denied (DECISION_PENDING and the JSON to send) except:
|
|
7
|
+
|
|
8
|
+
1. the dialog tool, when its questions equal what state.py prepared (R36.5);
|
|
9
|
+
the call is appended as an `offer`;
|
|
10
|
+
2. a Bash command equal to the next live pick (R36.7); a git pick's `use` is
|
|
11
|
+
appended here and handed to the git check on ctx.consent, a state.py pick
|
|
12
|
+
is spent by state.py itself after its argv check (R36.10);
|
|
13
|
+
3. `state.py decision show|settle|fallback`.
|
|
14
|
+
|
|
15
|
+
A guarded command (git commit/push, a human-only state.py command) that is not
|
|
16
|
+
the next live pick voids the decision's picks: CONSENT_MISMATCH (R36.8). An
|
|
17
|
+
offer neither picked nor declined by the next tool call means the recorder did
|
|
18
|
+
not run; the decision falls back to the terminal (R36.15).
|
|
19
|
+
|
|
20
|
+
It fails open: it is not the security boundary (R36.9 and R36.10 are).
|
|
21
|
+
"""
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import subprocess
|
|
25
|
+
from datetime import datetime, timezone
|
|
26
|
+
|
|
27
|
+
from guard.common import Allow, Deny, b_bytes, exists, find_ai_root, text_to_b
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def check(ctx):
|
|
31
|
+
# Its own failure is an Allow, never an exception: ai-guard.py ends the
|
|
32
|
+
# run on one, and the git, paths and scope checks after this must still run.
|
|
33
|
+
try:
|
|
34
|
+
root = find_ai_root(ctx.payload.cwd, ctx.pwd)
|
|
35
|
+
if root is None or not exists(root + "/.ai/state/decision.json"):
|
|
36
|
+
raise Allow()
|
|
37
|
+
gate(ctx, os.fsdecode(b_bytes(root)))
|
|
38
|
+
except (Allow, Deny):
|
|
39
|
+
raise
|
|
40
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
41
|
+
raise Allow() from None
|
|
42
|
+
raise Allow()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def gate(ctx, root): # pylint: disable=too-many-locals
|
|
46
|
+
from guard import dialog # pylint: disable=import-outside-toplevel
|
|
47
|
+
values = ctx.payload.values
|
|
48
|
+
raw = values[0] if len(values) == 1 and isinstance(values[0], dict) else None
|
|
49
|
+
hook_file = os.path.join(os.fsdecode(b_bytes(ctx.hook_dir)), "ai-guard.py")
|
|
50
|
+
runtime = dialog.payload_runtime(raw, hook_file)
|
|
51
|
+
if raw is None or not dialog.effective_switch(root, runtime, raw):
|
|
52
|
+
raise Allow()
|
|
53
|
+
session, call = raw.get("session_id"), raw.get("tool_use_id") or ""
|
|
54
|
+
ledger = dialog.read_ledger(root)
|
|
55
|
+
decisions = dialog.pending(dialog.bound(dialog.read_decisions(root), runtime, session), ledger)
|
|
56
|
+
if not decisions:
|
|
57
|
+
raise Allow()
|
|
58
|
+
ids = [d["id"] for d in decisions]
|
|
59
|
+
command = (b_bytes(ctx.payload.bash_command()).decode("utf-8", "replace").strip()
|
|
60
|
+
if ctx.payload.tool == "Bash" else "")
|
|
61
|
+
# The way out stays open even when the automatic fallback cannot run.
|
|
62
|
+
if command and dialog.decision_command(command):
|
|
63
|
+
raise Allow()
|
|
64
|
+
offer = dialog.unanswered_offer(ledger, session, call, ids)
|
|
65
|
+
if offer:
|
|
66
|
+
fall_back(dialog, root, offer["decisions"])
|
|
67
|
+
expected = dialog.dialog_payload(decisions, runtime)
|
|
68
|
+
tool = dialog.DIALOG_TOOL[runtime]
|
|
69
|
+
pending_text = ("DECISION_PENDING %s — a decision is open: call %s with exactly this JSON, then run the "
|
|
70
|
+
"picked command verbatim; or run 'state.py decision settle --id %s' for a recorded answer, or "
|
|
71
|
+
"'state.py decision fallback --id %s --why \"<why>\"': %s"
|
|
72
|
+
% (" ".join(ids), tool, ids[0], ids[0], json.dumps(expected, ensure_ascii=False)))
|
|
73
|
+
if raw.get("tool_name") == tool:
|
|
74
|
+
sent = raw.get("tool_input") if isinstance(raw.get("tool_input"), dict) else {}
|
|
75
|
+
# Only what state.py prepared: no title, no pre-filled answers beside it.
|
|
76
|
+
if dialog.canonical(sent.get("questions")) != dialog.canonical(expected["questions"]) \
|
|
77
|
+
or set(sent) - {"questions", "metadata"} or sent.get("metadata"):
|
|
78
|
+
raise Deny(text_to_b(pending_text))
|
|
79
|
+
with dialog.ledger_lock(root):
|
|
80
|
+
dialog.ledger_append(root, {"type": "offer", "decisions": ids, "session": session, "tool_use_id": call})
|
|
81
|
+
raise Allow()
|
|
82
|
+
if ctx.payload.tool == "Bash":
|
|
83
|
+
pick = dialog.next_live_pick(root, session, datetime.now(timezone.utc))
|
|
84
|
+
if pick and pick.get("decision") in ids and pick.get("label") == command:
|
|
85
|
+
if dialog.is_git_label(command):
|
|
86
|
+
with dialog.ledger_lock(root):
|
|
87
|
+
# Read again under the lock: two calls must not spend one pick.
|
|
88
|
+
again = dialog.next_live_pick(root, session, datetime.now(timezone.utc))
|
|
89
|
+
if not again or again.get("id") != pick["id"]:
|
|
90
|
+
raise Deny(text_to_b(pending_text))
|
|
91
|
+
dialog.ledger_append(root, {"type": "use", "id": pick["id"], "by": "git"})
|
|
92
|
+
ctx.consent = pick
|
|
93
|
+
raise Allow()
|
|
94
|
+
if dialog.guarded(command):
|
|
95
|
+
voided = dialog.unused_picks(ledger, ids, session)
|
|
96
|
+
if voided:
|
|
97
|
+
labels = [e.get("label") for e in ledger if e.get("type") == "pick" and e.get("id") in voided]
|
|
98
|
+
with dialog.ledger_lock(root):
|
|
99
|
+
dialog.ledger_append(root, {"type": "void", "ids": voided, "session": session,
|
|
100
|
+
"why": "mismatch"})
|
|
101
|
+
raise Deny(text_to_b(
|
|
102
|
+
"CONSENT_MISMATCH — this is not the next command the human picked; the picks are void and %s "
|
|
103
|
+
"is open again. Picked, in order: %s. Send the dialog again ('state.py decision show') or "
|
|
104
|
+
"run 'state.py decision fallback --id %s --why \"<why>\"'."
|
|
105
|
+
% (" ".join(ids), "; ".join(labels), ids[0])))
|
|
106
|
+
raise Deny(text_to_b(pending_text))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def fall_back(dialog, root, ids):
|
|
110
|
+
"""R36.15: state.py moves the decisions to the terminal route; the call is
|
|
111
|
+
denied once with the commands the human runs."""
|
|
112
|
+
printed = []
|
|
113
|
+
for decision_id in ids:
|
|
114
|
+
# A fallback that cannot run is the gate's own error: it fails open.
|
|
115
|
+
done = subprocess.run(["python3", dialog.STATE_PY, "--root", root, "decision", "fallback", "--id",
|
|
116
|
+
decision_id, "--why", "the dialog's answer was not recorded (the recorder hook "
|
|
117
|
+
"did not run)"], capture_output=True, text=True, check=False, timeout=5,
|
|
118
|
+
stdin=subprocess.DEVNULL)
|
|
119
|
+
if done.returncode != 0:
|
|
120
|
+
raise Allow()
|
|
121
|
+
printed.append(done.stdout.strip())
|
|
122
|
+
raise Deny(text_to_b("DECISION_FALLBACK %s — the dialog was sent but its answer was not recorded (R36.15); "
|
|
123
|
+
"the decision is back on the terminal route.\n%s" % (" ".join(ids), "\n".join(printed))))
|