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
package/hooks/guard/paths.py
CHANGED
|
@@ -59,21 +59,18 @@ WHY_APPROVE = T(
|
|
|
59
59
|
"and tells the session to run state.py questions --sync. An unattended run\n"
|
|
60
60
|
"exports AI_UNATTENDED=1 in the launcher's environment, and the journal then\n"
|
|
61
61
|
"records the approval as unattended for ever. See .ai/policies/safety.md.")
|
|
62
|
-
WHY_SET_BY = T(
|
|
63
|
-
"\n\nA task gets review_rounds_max review verdicts; after that the human decides,\n"
|
|
64
|
-
"in their own terminal:\n"
|
|
65
|
-
" python3 <plugin>/skills/ai-task/state.py --root . set review_status <value> --by \"<name>\"\n"
|
|
66
|
-
"Show them the findings in .ai/reports/<task-id>/task.md §Review. An unattended\n"
|
|
67
|
-
"run exports AI_UNATTENDED=1 in the launcher's environment. See .ai/policies/safety.md.")
|
|
68
62
|
WHY_WORK_CAP = T(
|
|
69
63
|
"This command names more paths than the guard can check within its timeout; split it into "
|
|
70
64
|
"smaller commands, or pass the list through a file and xargs.")
|
|
71
65
|
|
|
72
66
|
TOKEN_END = "([[:space:]\"';&|)<>]|$)"
|
|
73
67
|
|
|
74
|
-
# The --confirm-delete rule.
|
|
68
|
+
# The --confirm-delete rule, and --confirm-cleanup (the same human gate; any argparse prefix).
|
|
75
69
|
SEP = "(([^|;&]|[<>]&|&>)*[[:space:]])?" + Q
|
|
76
|
-
|
|
70
|
+
CONFIRM_DEL = "d(e(l(e(t(e)?)?)?)?)?"
|
|
71
|
+
CONFIRM_CLEAN = "c(l(e(a(n(u(p)?)?)?)?)?)?"
|
|
72
|
+
CONFIRM = ("--co(n(f(i(r(m(-(" + CONFIRM_DEL + "|" + CONFIRM_CLEAN + ")?)?)?)?)?)?)?"
|
|
73
|
+
"(=|[[:space:]]|['\"]|$)")
|
|
77
74
|
APPLY = "--ap(p(l(y)?)?)?" + END
|
|
78
75
|
HEAD = "(^|[[:space:]'\"/|;&(`])(update\\.py|-[A-Za-z]*m[[:space:]]*" + Q + "update|" + VAR + ")" + Q + "[[:space:]]"
|
|
79
76
|
CONFIRM_RE = HEAD + SEP + CONFIRM + SEP + APPLY + "|" + HEAD + SEP + APPLY + SEP + CONFIRM
|
|
@@ -84,10 +81,49 @@ VERB = "([[:space:]]+--)?[[:space:]]+" + Q + "approve" + END
|
|
|
84
81
|
BOUND = "(^|[|;&([:space:]`])"
|
|
85
82
|
APPROVE_RE = (BOUND + "(python3?[[:space:]]+)?" + Q + "[^[:space:]'\"]*state\\.py" + Q + ARGS + VERB + "|"
|
|
86
83
|
+ BOUND + "(python3?[[:space:]]+)?" + Q + VAR + Q + ARGS + VERB)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
84
|
+
# R36.11: the other human-only state.py commands — `profile set` (dialog on only)
|
|
85
|
+
# and the --by forms of verify, review and reject (dialog on or off) — run from an
|
|
86
|
+
# agent session only as the next live pick, like approve. `start --mode light --by`
|
|
87
|
+
# is denied with the dialog off only: `light` is the one --mode value that can
|
|
88
|
+
# never raise a mode; with the dialog on it reaches state.py, which opens the
|
|
89
|
+
# lowering decision (review H1, T-2026-10-28-010); any other `start --mode X --by`
|
|
90
|
+
# reaches state.py too (T-2026-10-30-0758 Q1, Q2).
|
|
91
|
+
PROFILE_SET_VERB = "([[:space:]]+--)?[[:space:]]+" + Q + "profile" + Q + "[[:space:]]+" + Q + "set" + END
|
|
92
|
+
PROFILE_SET_RE = (BOUND + "(python3?[[:space:]]+)?" + Q + "[^[:space:]'\"]*state\\.py" + Q + ARGS + PROFILE_SET_VERB
|
|
93
|
+
+ "|" + BOUND + "(python3?[[:space:]]+)?" + Q + VAR + Q + ARGS + PROFILE_SET_VERB)
|
|
94
|
+
BY_TOK = Q + "--by?" + Q + "(=|[[:space:]]|$)"
|
|
95
|
+
MODE_LIGHT = Q + "--m(o(d(e)?)?)?" + Q + "(=|[[:space:]]+)" + Q + "light" + END
|
|
96
|
+
SKIP = "([^|;&`]*[[:space:]])?"
|
|
97
|
+
HUMAN_BY_VERB = ("([[:space:]]+--)?[[:space:]]+" + Q + "(verify|review|reject)" + END + SKIP + BY_TOK)
|
|
98
|
+
START_LIGHT_VERB = ("([[:space:]]+--)?[[:space:]]+" + Q + "start" + END + SKIP + "("
|
|
99
|
+
+ MODE_LIGHT + SKIP + BY_TOK + "|" + BY_TOK + SKIP + MODE_LIGHT + ")")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def state_verb_re(verb):
|
|
103
|
+
"""The state.py command whose verb part is `verb`, as an ERE: by path or by a variable."""
|
|
104
|
+
return (BOUND + "(python3?[[:space:]]+)?" + Q + "[^[:space:]'\"]*state\\.py" + Q + ARGS
|
|
105
|
+
+ "(" + verb + ")|" + BOUND + "(python3?[[:space:]]+)?" + Q + VAR + Q + ARGS + "(" + verb + ")")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
HUMAN_BY_RE = state_verb_re(HUMAN_BY_VERB)
|
|
109
|
+
START_LIGHT_RE = state_verb_re(START_LIGHT_VERB)
|
|
110
|
+
WHY_HUMAN = T(
|
|
111
|
+
"\n\nThis command is the human's, whether or not the dialog is on. A human runs it in their\n"
|
|
112
|
+
"own terminal, outside the agent session; an unattended run exports AI_UNATTENDED=1 in the\n"
|
|
113
|
+
"launcher's environment. See .ai/policies/safety.md.")
|
|
114
|
+
WHY_PICK = T(
|
|
115
|
+
"\n\nWith the dialog on, this command runs from an agent session only as the human's pick.\n"
|
|
116
|
+
"The command that needs it opened a decision: 'state.py decision show' prints the dialog;\n"
|
|
117
|
+
"send it and run the picked command verbatim, once. Otherwise the human runs it in their\n"
|
|
118
|
+
"own terminal. See .ai/policies/safety.md.")
|
|
119
|
+
# R36.11: running the dialog's recorder by hand would forge the human's answer.
|
|
120
|
+
# dialog-gate.py in command position — the program itself, or the script a
|
|
121
|
+
# python runs — after a separator, a quote, exec/env/command/nohup/time/sudo
|
|
122
|
+
# and their words; naming it as an argument (grep, git diff, chmod) is not running it.
|
|
123
|
+
DIALOG_GATE_RE = ("(^|[|;&(`{'\"]|\\$\\()[[:space:]]*((exec|env|command|nohup|time|sudo)([[:space:]]+[^[:space:]|;&]+)*"
|
|
124
|
+
"[[:space:]]+)?(" + Q + "[^[:space:]'\"|;&]*python[0-9.]*" + Q
|
|
125
|
+
+ "([[:space:]]+-[^[:space:]]+)*[[:space:]]+)?" + Q + "[^[:space:]'\"|;&]*dialog-gate\\.py" + Q
|
|
126
|
+
+ "([[:space:]|;&)<>]|$)")
|
|
91
127
|
VERBS_RE = ("(^|[^A-Za-z0-9_.-])(cp|mv|install|rsync|rm|rmdir|unlink|tee|truncate|shred|touch|ln|chmod|chown|"
|
|
92
128
|
"dd|sed|perl|ruby)([^A-Za-z0-9_.-]|$)")
|
|
93
129
|
SEGMENT = "^[[:space:]]*"
|
|
@@ -112,7 +148,7 @@ LN_SRC_RE = ".*(^|[|;&[:space:]])ln([[:space:]]+-[^[:space:]]+)*[[:space:]]+([^[
|
|
|
112
148
|
LONG_CMD = 4096
|
|
113
149
|
# Whole words only, so --cov, --config and approve_invoice do not trip it.
|
|
114
150
|
LONG_TRIGGER_RE = ("(^|[^A-Za-z0-9_])approve([^A-Za-z0-9_]|$)|--by(=|[[:space:]]|$)|"
|
|
115
|
-
"--con?f?i?r?m?-?d?e?l?e?t?e?(=|[[:space:]]|$)|state\\.py|update\\.py")
|
|
151
|
+
"--con?f?i?r?m?-?(d?e?l?e?t?e?|c?l?e?a?n?u?p?)(=|[[:space:]]|$)|state\\.py|update\\.py")
|
|
116
152
|
# Tools that write a file named on their command line without a redirect; the
|
|
117
153
|
# movers do not parse them, so any protected path they name is refused (H1).
|
|
118
154
|
FETCHERS = "(^|[|;&[:space:]])(scp|tar|zip|unzip|curl|wget)([[:space:]]+[^;&|[:space:]]+)*[[:space:]]+"
|
|
@@ -206,7 +242,7 @@ def realpath_m(path):
|
|
|
206
242
|
|
|
207
243
|
def find_print(start, maxdepth, prune):
|
|
208
244
|
"""`find START -maxdepth N \\( -name A -o … \\) -prune -o -print`, in
|
|
209
|
-
readdir order, links not followed, errors silent."""
|
|
245
|
+
name order (not find's readdir order), links not followed, errors silent."""
|
|
210
246
|
def walk(path, name, depth):
|
|
211
247
|
if name in prune:
|
|
212
248
|
return
|
|
@@ -218,7 +254,9 @@ def find_print(start, maxdepth, prune):
|
|
|
218
254
|
entries = list(it)
|
|
219
255
|
except OSError:
|
|
220
256
|
return
|
|
221
|
-
|
|
257
|
+
# by name, not readdir order: which held file a refusal names must not depend
|
|
258
|
+
# on the filesystem (ext4 here put .ai/policies first, the CI runner .ai/state)
|
|
259
|
+
for e in sorted(entries, key=lambda e: e.name):
|
|
222
260
|
try:
|
|
223
261
|
is_d = e.is_dir(follow_symlinks=False)
|
|
224
262
|
except OSError:
|
|
@@ -1168,6 +1206,27 @@ class PathGuard: # pylint: disable=too-many-instance-
|
|
|
1168
1206
|
return
|
|
1169
1207
|
self.check_bash(unattended)
|
|
1170
1208
|
|
|
1209
|
+
def dialog_consent(self, cmd):
|
|
1210
|
+
"""(the dialog is on for this call, the command is the next live pick
|
|
1211
|
+
of this session) — R36.11's pass-through. state.py spends the pick
|
|
1212
|
+
after its own argv check (R36.10); the guard only lets it reach it."""
|
|
1213
|
+
if not self.root or not exists(self.root + "/.ai/policies/dialog.json"):
|
|
1214
|
+
return False, False
|
|
1215
|
+
from datetime import datetime, timezone # pylint: disable=import-outside-toplevel
|
|
1216
|
+
from guard import dialog # pylint: disable=import-outside-toplevel
|
|
1217
|
+
root = os.fsdecode(b_bytes(self.root))
|
|
1218
|
+
values = self.p.values
|
|
1219
|
+
raw = values[0] if len(values) == 1 and isinstance(values[0], dict) else None
|
|
1220
|
+
hook_file = os.path.join(os.fsdecode(b_bytes(self.ctx.hook_dir)), "ai-guard.py")
|
|
1221
|
+
try:
|
|
1222
|
+
if raw is None or not dialog.effective_switch(root, dialog.payload_runtime(raw, hook_file), raw):
|
|
1223
|
+
return False, False
|
|
1224
|
+
pick = dialog.next_live_pick(root, raw.get("session_id"), datetime.now(timezone.utc))
|
|
1225
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
1226
|
+
return True, False
|
|
1227
|
+
command = b_bytes(cmd).decode("utf-8", "replace").strip()
|
|
1228
|
+
return True, bool(pick and pick.get("label") == command)
|
|
1229
|
+
|
|
1171
1230
|
def interpreter_text(self, cmd):
|
|
1172
1231
|
"""The text of an interpreter one-liner or an eval, scanned whole (H3)."""
|
|
1173
1232
|
m = self.m
|
|
@@ -1199,11 +1258,22 @@ class PathGuard: # pylint: disable=too-many-instance-
|
|
|
1199
1258
|
if length(cmd) > LONG_CMD and (m.search_c(LONG_TRIGGER_RE, cmd) or m.matches_joined(cmd, self.loose)):
|
|
1200
1259
|
raise Deny("Refusing a %d-character shell command that names a guarded path or a state.py or "
|
|
1201
1260
|
"update.py verb: write it to a file and run that, or split it." % length(cmd))
|
|
1202
|
-
if m.ere_match(
|
|
1261
|
+
if m.ere_match(DIALOG_GATE_RE, cmd_joined):
|
|
1262
|
+
raise Deny("Refusing to run hooks/dialog-gate.py from a shell: it records the human's answer to "
|
|
1263
|
+
"the dialog, and only the runtime calls it, after the dialog tool. To answer a decision, "
|
|
1264
|
+
"send the dialog ('state.py decision show'); a human settles one in their own terminal "
|
|
1265
|
+
"('state.py decision settle --id D<n> --pick <n>'). See .ai/policies/safety.md.")
|
|
1266
|
+
on, picked = self.dialog_consent(cmd)
|
|
1267
|
+
if m.ere_match(APPROVE_RE, cmd_joined) and not unattended and self.task_in_flight() and not picked:
|
|
1203
1268
|
raise Deny("Refusing 'state.py approve' from an agent session." + WHY_APPROVE)
|
|
1204
|
-
if
|
|
1205
|
-
raise Deny(
|
|
1206
|
-
|
|
1269
|
+
if on and not picked and not unattended and m.ere_match(PROFILE_SET_RE, cmd_joined):
|
|
1270
|
+
raise Deny("Refusing 'state.py profile set' from an agent session." + WHY_PICK)
|
|
1271
|
+
if not picked and not unattended and m.ere_match(HUMAN_BY_RE, cmd_joined):
|
|
1272
|
+
raise Deny(T("Refusing a verdict with 'state.py … --by' from an agent session.")
|
|
1273
|
+
+ (WHY_PICK if on else WHY_HUMAN))
|
|
1274
|
+
if not on and not picked and not unattended and m.ere_match(START_LIGHT_RE, cmd_joined):
|
|
1275
|
+
raise Deny(T("Refusing 'state.py start --mode light … --by' from an agent session: lowering the "
|
|
1276
|
+
"mode is the human's.") + WHY_HUMAN)
|
|
1207
1277
|
self.interpreter_text(cmd)
|
|
1208
1278
|
|
|
1209
1279
|
# Top-level splitting only: the string a sh -c or an eval runs is not
|
package/hooks/hooks.json
CHANGED
|
@@ -14,13 +14,13 @@
|
|
|
14
14
|
]
|
|
15
15
|
},
|
|
16
16
|
{
|
|
17
|
-
"matcher": "
|
|
17
|
+
"matcher": "*",
|
|
18
18
|
"hooks": [
|
|
19
19
|
{
|
|
20
20
|
"type": "command",
|
|
21
21
|
"command": "python3 -I -S \"${CLAUDE_PLUGIN_ROOT}/hooks/ai-guard.py\"",
|
|
22
22
|
"timeout": 15,
|
|
23
|
-
"statusMessage": "Checking git, paths and
|
|
23
|
+
"statusMessage": "Checking git, paths, scope and open decisions"
|
|
24
24
|
}
|
|
25
25
|
]
|
|
26
26
|
},
|
|
@@ -84,7 +84,30 @@
|
|
|
84
84
|
]
|
|
85
85
|
}
|
|
86
86
|
],
|
|
87
|
+
"Notification": [
|
|
88
|
+
{
|
|
89
|
+
"matcher": "auth_success",
|
|
90
|
+
"hooks": [
|
|
91
|
+
{
|
|
92
|
+
"type": "command",
|
|
93
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/context-guard.py\"",
|
|
94
|
+
"timeout": 15
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
],
|
|
87
99
|
"PostToolUse": [
|
|
100
|
+
{
|
|
101
|
+
"matcher": "AskUserQuestion",
|
|
102
|
+
"hooks": [
|
|
103
|
+
{
|
|
104
|
+
"type": "command",
|
|
105
|
+
"command": "python3 -I -S \"${CLAUDE_PLUGIN_ROOT}/hooks/dialog-gate.py\"",
|
|
106
|
+
"timeout": 5,
|
|
107
|
+
"statusMessage": "Recording the dialog's answer"
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
},
|
|
88
111
|
{
|
|
89
112
|
"matcher": "Agent",
|
|
90
113
|
"hooks": [
|
|
@@ -117,6 +140,18 @@
|
|
|
117
140
|
}
|
|
118
141
|
]
|
|
119
142
|
}
|
|
143
|
+
],
|
|
144
|
+
"Stop": [
|
|
145
|
+
{
|
|
146
|
+
"hooks": [
|
|
147
|
+
{
|
|
148
|
+
"type": "command",
|
|
149
|
+
"command": "python3 -I -S \"${CLAUDE_PLUGIN_ROOT}/hooks/dialog-gate.py\"",
|
|
150
|
+
"timeout": 5,
|
|
151
|
+
"statusMessage": "Checking for an open decision"
|
|
152
|
+
}
|
|
153
|
+
]
|
|
154
|
+
}
|
|
120
155
|
]
|
|
121
156
|
}
|
|
122
157
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""riskrails memory pre-push validator (spec compact-project-memory §D10, §I5).
|
|
3
|
+
|
|
4
|
+
memory-pre-push.py <remote> <url> < "<lref> <lsha> <rref> <rsha>"… git hook
|
|
5
|
+
memory-pre-push.py --range OLD..NEW [--repo DIR] CI or server
|
|
6
|
+
memory-pre-push.py --tree REV [--repo DIR] structure only
|
|
7
|
+
|
|
8
|
+
Checks the pushed commits' blobs, never the working tree. stderr carries one
|
|
9
|
+
line per finding; exit 0 clean, 1 findings (the push stops), 3 the validator
|
|
10
|
+
itself failed (fail closed). It writes no file, uses no network and imports
|
|
11
|
+
no state.py. A git hook, not a runtime hook: no tool call ever runs it. The
|
|
12
|
+
logic is memory_sync.pre_push, which the shim also runs directly where an
|
|
13
|
+
install copied skills/ai-task/ but not this file.
|
|
14
|
+
"""
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
19
|
+
sys.path.insert(0, os.path.join(os.path.dirname(HERE), "skills", "ai-task"))
|
|
20
|
+
|
|
21
|
+
if __name__ == "__main__":
|
|
22
|
+
try:
|
|
23
|
+
import memory_sync # pylint: disable=import-outside-toplevel
|
|
24
|
+
except ImportError as exc:
|
|
25
|
+
print("memory-pre-push: validator error: %s — run 'state.py memory verify --all'; a human may push "
|
|
26
|
+
"with 'git push --no-verify'" % exc, file=sys.stderr)
|
|
27
|
+
sys.exit(3)
|
|
28
|
+
sys.exit(memory_sync.pre_push_cli(sys.argv[1:]))
|
|
@@ -236,12 +236,8 @@ def main(argv):
|
|
|
236
236
|
on = runtimes(runtime, root)
|
|
237
237
|
s = Scaffold(tpl, root)
|
|
238
238
|
|
|
239
|
-
# shared SDLC
|
|
239
|
+
# shared SDLC (the intent/spec/plan/adr templates ship with the plugin; a project TEMPLATE.md is only an override)
|
|
240
240
|
s.put("sdlc-README.md", "docs/sdlc/README.md")
|
|
241
|
-
s.put("intent.md", "docs/sdlc/intent/TEMPLATE.md")
|
|
242
|
-
s.put("spec.md", "docs/sdlc/specs/TEMPLATE.md")
|
|
243
|
-
s.put("plan.md", "docs/sdlc/plans/TEMPLATE.md")
|
|
244
|
-
s.put("adr.md", "docs/sdlc/adr/TEMPLATE.md")
|
|
245
241
|
s.put("constitution.md", "docs/sdlc/constitution.md", render=True)
|
|
246
242
|
|
|
247
243
|
# per runtime
|
package/hooks/runtime-gate.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Runtime gate: one hook for both runtimes
|
|
3
|
-
codex-model-gate.py
|
|
2
|
+
"""Runtime gate: one hook for both runtimes (Claude Code and Codex). The older
|
|
3
|
+
fable-gate.py and codex-model-gate.py remain only as shims that exec this file.
|
|
4
4
|
|
|
5
5
|
The most capable model of a plan can stop serving an account — a rate limit, a
|
|
6
6
|
used-up usage limit, a model the account cannot reach — and the runtime's own
|
|
@@ -12,8 +12,9 @@ Claude Code (model: fable -> opus):
|
|
|
12
12
|
StopFailure (rate_limit|model_not_found) records that Fable is unavailable
|
|
13
13
|
PostToolUse:Agent records a Fable agent that fell back (overload), briefly
|
|
14
14
|
PreToolUse:Agent while a record is live, rewrites model fable -> opus
|
|
15
|
-
statusline [--then <command>]
|
|
16
|
-
|
|
15
|
+
statusline [--context] [--then <command>] records it when the weekly limit is nearly used, prints
|
|
16
|
+
the model and the context use with --context, then runs
|
|
17
|
+
the user's own statusline command on the same input
|
|
17
18
|
|
|
18
19
|
Codex (EXPERT model -> STRONG model); an agent launch is `spawn_agent`, or
|
|
19
20
|
`collaborationspawn_agent` under multi-agent v2:
|
|
@@ -43,7 +44,7 @@ A record expires on its own. The gate never denies, always exits 0, and any
|
|
|
43
44
|
error inside it fails open: the call goes through unchanged.
|
|
44
45
|
|
|
45
46
|
runtime-gate.py status | quota [--json] | clear | set <seconds> [reason]
|
|
46
|
-
| statusline [--then <cmd>]
|
|
47
|
+
| statusline [--context] [--then <cmd>]
|
|
47
48
|
|
|
48
49
|
Quota (advice only, it never refuses anything): Claude Code's comes from the
|
|
49
50
|
statusline payload, Codex's from the newest rollout's token_count event. Both
|
|
@@ -414,10 +415,10 @@ def claude_pre_tool_use(payload):
|
|
|
414
415
|
"hookSpecificOutput": {
|
|
415
416
|
"hookEventName": "PreToolUse",
|
|
416
417
|
"permissionDecision": "allow",
|
|
417
|
-
"permissionDecisionReason": f"
|
|
418
|
+
"permissionDecisionReason": f"runtime-gate: {why}; {name} runs on {FALLBACK}",
|
|
418
419
|
"updatedInput": {**tool_input, "model": FALLBACK},
|
|
419
420
|
"additionalContext": (
|
|
420
|
-
f"
|
|
421
|
+
f"runtime-gate routed this {name} agent to {FALLBACK}: {why}. "
|
|
421
422
|
f"Its answer is {FALLBACK}'s, not Fable's. "
|
|
422
423
|
"`~/.claude/hooks/runtime-gate.py clear` re-enables Fable early."
|
|
423
424
|
),
|
|
@@ -800,10 +801,10 @@ def codex_pre_tool_use(payload):
|
|
|
800
801
|
journal_fallback(payload, label, model, to, rec)
|
|
801
802
|
out["hookSpecificOutput"]["permissionDecision"] = "allow"
|
|
802
803
|
out["hookSpecificOutput"]["updatedInput"] = updated
|
|
803
|
-
context = (f"
|
|
804
|
+
context = (f"runtime-gate: {why}. {label} runs on {to}{via} instead, so its answer is "
|
|
804
805
|
f"{to}'s, not {EXPERT}'s. Say so when you report it. ")
|
|
805
806
|
else:
|
|
806
|
-
context = (f"
|
|
807
|
+
context = (f"runtime-gate: {why}. {label} is not rerouted ({not_why}): it runs on {EXPERT} "
|
|
807
808
|
"and may fail the same way. Say so when you report it. ")
|
|
808
809
|
out["hookSpecificOutput"]["additionalContext"] = context + "`runtime-gate.py clear` re-enables the expert model early."
|
|
809
810
|
respond(out)
|
|
@@ -1102,7 +1103,7 @@ def cli(argv):
|
|
|
1102
1103
|
if cmd == "set" and len(argv) >= 2 and argv[1].isdigit():
|
|
1103
1104
|
mark(time.time() + int(argv[1]), " ".join(argv[2:]) or "manual", "cli")
|
|
1104
1105
|
return cli(["status"])
|
|
1105
|
-
print("usage: runtime-gate.py status | quota [--json] | clear | set <seconds> [reason] | statusline [--then <cmd>] < json",
|
|
1106
|
+
print("usage: runtime-gate.py status | quota [--json] | clear | set <seconds> [reason] | statusline [--context] [--then <cmd>] < json",
|
|
1106
1107
|
file=sys.stderr)
|
|
1107
1108
|
return 2
|
|
1108
1109
|
|
|
@@ -1115,17 +1116,63 @@ def disabled():
|
|
|
1115
1116
|
return False
|
|
1116
1117
|
|
|
1117
1118
|
|
|
1119
|
+
def context_line(payload):
|
|
1120
|
+
"""`Opus 5.5 [1m] · 123k/1000k (12%)` from a Claude Code statusline payload, or ""
|
|
1121
|
+
when it names neither the model nor the context. The context_window fields differ
|
|
1122
|
+
between Claude Code versions: the tokens come from current_usage (input plus cache
|
|
1123
|
+
reads and writes, what the next turn re-reads), else from used_percentage."""
|
|
1124
|
+
model = payload.get("model")
|
|
1125
|
+
name = str(model.get("display_name") or model.get("id") or "") if isinstance(model, dict) else ""
|
|
1126
|
+
window = payload.get("context_window")
|
|
1127
|
+
window = window if isinstance(window, dict) else {}
|
|
1128
|
+
|
|
1129
|
+
def number(value):
|
|
1130
|
+
return value if isinstance(value, (int, float)) and not isinstance(value, bool) and value >= 0 else None
|
|
1131
|
+
|
|
1132
|
+
size = number(window.get("context_window_size"))
|
|
1133
|
+
pct = number(window.get("used_percentage"))
|
|
1134
|
+
usage = window.get("current_usage")
|
|
1135
|
+
used = None
|
|
1136
|
+
if isinstance(usage, dict):
|
|
1137
|
+
parts = [number(usage.get(k)) for k in
|
|
1138
|
+
("input_tokens", "cache_creation_input_tokens", "cache_read_input_tokens")]
|
|
1139
|
+
if any(p is not None for p in parts):
|
|
1140
|
+
used = sum(p or 0 for p in parts)
|
|
1141
|
+
if used is None and pct is not None and size:
|
|
1142
|
+
used = size * pct / 100
|
|
1143
|
+
if pct is None and used is not None and size:
|
|
1144
|
+
pct = used * 100 / size
|
|
1145
|
+
context = ""
|
|
1146
|
+
if used is not None and size:
|
|
1147
|
+
context = f"{round(used / 1000)}k/{round(size / 1000)}k ({round(pct)}%)"
|
|
1148
|
+
return " · ".join(part for part in (name, context) if part)
|
|
1149
|
+
|
|
1150
|
+
|
|
1118
1151
|
def statusline_main(argv):
|
|
1119
|
-
"""`statusline [--then <command>]`: check the weekly limit,
|
|
1120
|
-
|
|
1121
|
-
|
|
1152
|
+
"""`statusline [--context] [--then <command>]`: check the weekly limit, print the
|
|
1153
|
+
model and the context use with --context, then hand the same input to the user's
|
|
1154
|
+
own statusline command and pass its output and exit code through. Neither the
|
|
1155
|
+
check nor the line ever gets in the way of the statusline."""
|
|
1122
1156
|
data = sys.stdin.buffer.read()
|
|
1123
|
-
|
|
1157
|
+
show_context = argv[1:2] == ["--context"]
|
|
1158
|
+
if show_context:
|
|
1159
|
+
argv = argv[:1] + argv[2:]
|
|
1160
|
+
payload = None
|
|
1161
|
+
try:
|
|
1162
|
+
payload = json.loads(data.decode("utf-8", "replace"))
|
|
1163
|
+
except Exception: # pylint: disable=broad-exception-caught # fail open: a bad payload never breaks the statusline
|
|
1164
|
+
pass
|
|
1165
|
+
if isinstance(payload, dict) and not disabled() and RUNTIME == "claude":
|
|
1166
|
+
try:
|
|
1167
|
+
statusline(payload)
|
|
1168
|
+
except Exception: # pylint: disable=broad-exception-caught # fail open, as above
|
|
1169
|
+
pass
|
|
1170
|
+
if show_context and isinstance(payload, dict):
|
|
1124
1171
|
try:
|
|
1125
|
-
|
|
1126
|
-
if
|
|
1127
|
-
|
|
1128
|
-
except Exception: # pylint: disable=broad-exception-caught # fail open
|
|
1172
|
+
line = context_line(payload)
|
|
1173
|
+
if line:
|
|
1174
|
+
print(line)
|
|
1175
|
+
except Exception: # pylint: disable=broad-exception-caught # fail open, as above
|
|
1129
1176
|
pass
|
|
1130
1177
|
if len(argv) >= 3 and argv[1] == "--then" and argv[2].strip():
|
|
1131
1178
|
shell = shutil.which("bash") or "/bin/sh"
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Update notice: tell the user at session start when npm has a newer `riskrails`.
|
|
3
|
+
|
|
4
|
+
Imported by context-guard.py (SessionStart) and run as a script for the refresh:
|
|
5
|
+
|
|
6
|
+
update_notice.py --refresh <cache path>
|
|
7
|
+
|
|
8
|
+
The hook itself never touches the network. It reads <home>/riskrails/update-check.json
|
|
9
|
+
({"checked_at": epoch, "latest": "x.y.z"}); when that file is missing or older than a
|
|
10
|
+
day it starts the refresh as a detached child and answers from the cache as it is, so
|
|
11
|
+
the first session after an install says nothing and a later one does. The child asks
|
|
12
|
+
the registry (3 s timeout) and writes the cache atomically; any error is silent, and a
|
|
13
|
+
failed fetch only moves checked_at, so the next try waits a day.
|
|
14
|
+
|
|
15
|
+
Environment:
|
|
16
|
+
RISKRAILS_NO_UPDATE_CHECK non-empty turns the check off (no read, no spawn)
|
|
17
|
+
CI non-empty turns it off as well
|
|
18
|
+
RISKRAILS_REGISTRY_URL instead of https://registry.npmjs.org/riskrails/latest (file:// works)
|
|
19
|
+
"""
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import re
|
|
23
|
+
import subprocess
|
|
24
|
+
import sys
|
|
25
|
+
import time
|
|
26
|
+
import urllib.request
|
|
27
|
+
|
|
28
|
+
REGISTRY_URL = "https://registry.npmjs.org/riskrails/latest"
|
|
29
|
+
MAX_AGE = 24 * 3600
|
|
30
|
+
TIMEOUT = 3
|
|
31
|
+
MAX_BODY = 65536
|
|
32
|
+
COMMAND = "npx -y riskrails@latest --yes"
|
|
33
|
+
VERSION_RE = re.compile(r"^(\d+)\.(\d+)\.(\d+)$") # no prerelease, no build metadata
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def parse(version):
|
|
37
|
+
"""(major, minor, patch) of a plain X.Y.Z, else None."""
|
|
38
|
+
match = VERSION_RE.match(version.strip()) if isinstance(version, str) else None
|
|
39
|
+
return tuple(int(n) for n in match.groups()) if match else None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def installed_version(runtime, plugin_root, home=None):
|
|
43
|
+
"""The installed release: profile.json's plugin_version (written by the installer into
|
|
44
|
+
<home>/riskrails/), else the plugin manifest beside the hooks (plugin-cache layout).
|
|
45
|
+
Only a plain X.Y.Z counts; anything else is None."""
|
|
46
|
+
manifest = ".codex-plugin" if runtime == "codex" else ".claude-plugin"
|
|
47
|
+
sources = [(os.path.join(plugin_root, manifest, "plugin.json"), "version")]
|
|
48
|
+
if home:
|
|
49
|
+
sources.insert(0, (os.path.join(home, "riskrails", "profile.json"), "plugin_version"))
|
|
50
|
+
for path, key in sources:
|
|
51
|
+
try:
|
|
52
|
+
with open(path, encoding="utf-8") as fh:
|
|
53
|
+
version = json.load(fh).get(key)
|
|
54
|
+
except (OSError, ValueError, AttributeError):
|
|
55
|
+
continue
|
|
56
|
+
if parse(version):
|
|
57
|
+
return version
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def read_cache(path):
|
|
62
|
+
try:
|
|
63
|
+
with open(path, encoding="utf-8") as fh:
|
|
64
|
+
data = json.load(fh)
|
|
65
|
+
return data if isinstance(data, dict) else {}
|
|
66
|
+
except (OSError, ValueError):
|
|
67
|
+
return {}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def disabled():
|
|
71
|
+
return bool(os.environ.get("RISKRAILS_NO_UPDATE_CHECK") or os.environ.get("CI"))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def spawn_refresh(path):
|
|
75
|
+
try:
|
|
76
|
+
subprocess.Popen([sys.executable, "-I", os.path.abspath(__file__), "--refresh", path], # pylint: disable=consider-using-with
|
|
77
|
+
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
|
|
78
|
+
stderr=subprocess.DEVNULL, start_new_session=True)
|
|
79
|
+
except OSError:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def notice(runtime, home, plugin_root, now):
|
|
84
|
+
"""(systemMessage, additionalContext) when the cache knows a newer release, else ("", "")."""
|
|
85
|
+
if disabled():
|
|
86
|
+
return "", ""
|
|
87
|
+
riskrails = os.path.join(home, "riskrails")
|
|
88
|
+
path = os.path.join(riskrails, "update-check.json")
|
|
89
|
+
cache = read_cache(path)
|
|
90
|
+
checked = cache.get("checked_at")
|
|
91
|
+
fresh = isinstance(checked, (int, float)) and 0 <= now - checked < MAX_AGE
|
|
92
|
+
if not fresh and os.path.isdir(riskrails):
|
|
93
|
+
spawn_refresh(path)
|
|
94
|
+
installed, latest = installed_version(runtime, plugin_root, home), cache.get("latest")
|
|
95
|
+
have, new = parse(installed), parse(latest)
|
|
96
|
+
if not have or not new or new <= have:
|
|
97
|
+
return "", ""
|
|
98
|
+
message = (f"RiskRails {latest} is available (installed {installed}). Update: {COMMAND} "
|
|
99
|
+
f"— inside Claude Code or Codex CLI: ! {COMMAND} — then /rr-setup --update in each project.")
|
|
100
|
+
context = (f"RiskRails {latest} is available on npm; {installed} is installed. The user updates it "
|
|
101
|
+
f"with `{COMMAND}` in a terminal, or `! {COMMAND}` inside Claude Code or Codex CLI, "
|
|
102
|
+
"then `/rr-setup --update` in each project. Mention this once to the user if it is "
|
|
103
|
+
"relevant; never run the update yourself — installing is the user's act.")
|
|
104
|
+
return message, context
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def write_cache(path, latest):
|
|
108
|
+
tmp = f"{path}.{os.getpid()}.tmp"
|
|
109
|
+
try:
|
|
110
|
+
with open(tmp, "w", encoding="utf-8") as fh:
|
|
111
|
+
json.dump({"checked_at": int(time.time()), "latest": latest}, fh)
|
|
112
|
+
os.replace(tmp, path)
|
|
113
|
+
except OSError:
|
|
114
|
+
try:
|
|
115
|
+
os.remove(tmp)
|
|
116
|
+
except OSError:
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def refresh(path):
|
|
121
|
+
"""Ask the registry and write the cache; checked_at moves first, so a fetch that hangs
|
|
122
|
+
or is killed still costs one spawn a day, and a failed fetch keeps the previous latest."""
|
|
123
|
+
previous = read_cache(path).get("latest")
|
|
124
|
+
previous = previous if isinstance(previous, str) else None
|
|
125
|
+
write_cache(path, previous)
|
|
126
|
+
latest = None
|
|
127
|
+
try:
|
|
128
|
+
url = os.environ.get("RISKRAILS_REGISTRY_URL") or REGISTRY_URL
|
|
129
|
+
with urllib.request.urlopen(url, timeout=TIMEOUT) as resp: # nosec: the registry, or a test's file URL
|
|
130
|
+
version = json.loads(resp.read(MAX_BODY).decode("utf-8")).get("version")
|
|
131
|
+
if parse(version):
|
|
132
|
+
latest = version
|
|
133
|
+
except (OSError, ValueError, AttributeError):
|
|
134
|
+
pass
|
|
135
|
+
write_cache(path, latest if latest is not None else previous)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
if __name__ == "__main__":
|
|
139
|
+
if len(sys.argv) == 3 and sys.argv[1] == "--refresh" and not disabled():
|
|
140
|
+
refresh(sys.argv[2])
|
|
141
|
+
sys.exit(0)
|
package/instructions/routing.md
CHANGED
|
@@ -38,7 +38,7 @@ Facts are collected cheaply; thinking is paid for. Session model {{SESSION_MODEL
|
|
|
38
38
|
- One subagent returning twenty lines beats five tool calls whose output stays in context all session.
|
|
39
39
|
- Deterministic tools first: `rg`, `git`, `jq`, the framework CLI, the test runner.
|
|
40
40
|
- `/clear` between tasks, and after every task that ran a review. Compaction near {{COMPACT_WINDOW}} tokens is a summary of the old task, not a clean start; until then every turn re-reads the whole history.
|
|
41
|
-
- `context-guard` measures the context on every prompt: a one-line warning from {{CONTEXT_WARN}} tokens,
|
|
41
|
+
- `context-guard` measures the context on every prompt: a one-line warning from {{CONTEXT_WARN}} tokens, {{CONTEXT_BLOCK}}. When its note says the prompt starts a different task, suggest `/clear` before doing the work. Before a compaction it saves the edited files, the latest instructions verbatim, the todo list and the git state, and hands them back afterwards as "Session state before compaction": trust that block over the summary where they differ, and re-read a file before editing it rather than relying on what the summary says it contains.
|
|
42
42
|
- Run tests once, to the end, and fix every failure as one batch. Never one failure, one fix, one run.
|
|
43
43
|
- `/usage-report` shows where the tokens went, at zero model cost; `--provider both` puts Codex next to Claude Code.
|
|
44
44
|
|
|
@@ -46,8 +46,8 @@ Facts are collected cheaply; thinking is paid for. Session model {{SESSION_MODEL
|
|
|
46
46
|
# Guards
|
|
47
47
|
|
|
48
48
|
<!-- stub: runtime=claude -->
|
|
49
|
-
- `ai-git-guard` runs in every repository: no force push, history rewrite, push or merge to a protected branch, `--no-verify`, staged secret or production deploy. `ai-path-guard` and `ai-scope-guard` arm only where `.ai/` exists. No agent
|
|
50
|
-
- `context-guard` measures the context on every prompt and holds a prompt back once at the block threshold above; its "Session state before compaction" block and `.ai/state/handoff.md` are what a resumed session trusts over the summary.
|
|
49
|
+
- `ai-git-guard` runs in every repository: no force push, history rewrite, push or merge to a protected branch, `--no-verify`, staged secret or production deploy. `ai-path-guard` and `ai-scope-guard` arm only where `.ai/` exists. No agent merges or deploys; commit, push and approval run only as the command the human picked in the dialog, as the harness recorded — verbatim, once — or the human runs them outside the agent.
|
|
50
|
+
- `context-guard` measures the context on every prompt and holds a prompt back once at the block threshold above, when there is one; its "Session state before compaction" block and `.ai/state/handoff.md` are what a resumed session trusts over the summary.
|
|
51
51
|
|
|
52
52
|
<!-- stub: runtime=codex -->
|
|
53
53
|
# Model allocation by task and scope (host-wide, ChatGPT {{CODEX_PLAN}} plan)
|
|
@@ -130,7 +130,7 @@ The main session re-reads its whole context every turn, so context length — mo
|
|
|
130
130
|
# Guards
|
|
131
131
|
|
|
132
132
|
<!-- stub: runtime=codex -->
|
|
133
|
-
- `ai-git-guard` runs in every repository: no force push, history rewrite, push or merge to a protected branch, `--no-verify`, staged secret or production deploy. `ai-path-guard` and `ai-scope-guard` arm only where `.ai/` exists. All three see `Bash` and `apply_patch`; run `/hooks` once after installing to review and trust them, or they are skipped. No agent
|
|
133
|
+
- `ai-git-guard` runs in every repository: no force push, history rewrite, push or merge to a protected branch, `--no-verify`, staged secret or production deploy. `ai-path-guard` and `ai-scope-guard` arm only where `.ai/` exists. All three see `Bash` and `apply_patch`; run `/hooks` once after installing to review and trust them, or they are skipped. No agent merges or deploys; commit, push and approval run only as the command the human picked in the dialog — verbatim, once — or the human runs them outside the agent.
|
|
134
134
|
- `architect` is for design questions outside a task, in a repository without `.ai/`, and in a full-mode task whose design is open (`/rr-plan`). Otherwise the session plans, and an EXPERT spawn is the escalation.
|
|
135
135
|
|
|
136
136
|
<!-- stub: runtime=claude,codex -->
|
package/instructions/stub.md
CHANGED
|
@@ -24,7 +24,7 @@ Rules for every repository; everything else loads on demand. `{{HOME}}/riskrails
|
|
|
24
24
|
- With `.ai/`: read `.ai/AGENTS.md` first, run every change through `/rr-build <request>`, treat `.ai/policies/` as binding; `/rr-setup --update` after a plugin reinstall. Without `.ai/`: `/rr-setup` first.
|
|
25
25
|
- Production behaviour is the source of truth: document problems outside the task, do not fix them. Never mix a refactoring with a feature change.
|
|
26
26
|
- Each step names the files it may touch; an edit outside them is refused — answer `SCOPE_CHANGE_REQUIRED`, do not widen the step.{{MULTI_EDIT}}
|
|
27
|
-
- No agent
|
|
27
|
+
- No agent merges or deploys. Commit, push and approval run only as the command the human picked in the dialog, as the harness recorded — verbatim, once — or the human runs them outside the agent. A guard's refusal is not a hint to reword.{{TRUST}}
|
|
28
28
|
- Verify before reporting done: the verification command once, to the end, every failure fixed as one batch, output shown.
|
|
29
29
|
- Context: `grep -n`, then read the hits; never `cat` logs, lockfiles or generated code; a file over ~{{READ_LINES}} lines is read by a FAST reader (`Explore`, `log-reader`) that returns `file:line` excerpts, never the file. Deterministic tools first. {{NEW_CONTEXT}} between tasks. `/rr-status --cost` shows where tokens went.
|
|
30
30
|
- Models are tiers, not names: runners and verbatim extraction on FAST at `low`, readers that judge on BALANCED; the session implements; STRONG and EXPERT only on a named trigger, one agent at a time, never a fleet on the top tier; a failed thinking task is never retried on a cheaper model.
|
|
@@ -47,8 +47,8 @@ This repository runs an agentic pipeline under `.ai/`. Read `.ai/AGENTS.md` firs
|
|
|
47
47
|
|
|
48
48
|
<!-- stub: scope=project runtime=* -->
|
|
49
49
|
- Verify before reporting done: `verify_command` from `.ai/policies/testing.md` once, to the end, then `e2e_command` once; every failure fixed as one batch; show the output.
|
|
50
|
-
- No agent
|
|
51
|
-
- Files, not chat, carry decisions: `.ai/reports/<task-id>/task.md` (answer by filling `[Answer]:`), `.ai/state/handoff.md` (read first when resuming), `docs/sdlc/constitution.md
|
|
50
|
+
- No agent merges or deploys; commit, push, approval only as the dialog picks and the harness records, else a human outside. When a review flags the same mistake twice, the correction goes into this file.
|
|
51
|
+
- Files, not chat, carry decisions: `.ai/reports/<task-id>/task.md` (answer by filling `[Answer]:`), `.ai/state/handoff.md` (read first when resuming), `docs/sdlc/constitution.md`.
|
|
52
52
|
|
|
53
53
|
<!-- stub: scope=skeleton,skeleton-sdlc runtime=* -->
|
|
54
54
|
# {{PROJECT}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "riskrails",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "RiskRails: a weight-based engineering pipeline for Claude Code and Codex — scoped steps, enforcement hooks, adversarial review, model routing.",
|
|
5
5
|
"keywords": ["claude-code", "codex", "sdlc", "agents", "guardrails", "code-review"],
|
|
6
6
|
"homepage": "https://github.com/vanssata/claude-agentic-sdlc-module#readme",
|
package/profiles/max.json
CHANGED
|
@@ -10,12 +10,14 @@
|
|
|
10
10
|
"claude-sonnet-5": { "effortLevel": "medium" },
|
|
11
11
|
"claude-haiku-4-5-20251001": { "effortLevel": "low" }
|
|
12
12
|
},
|
|
13
|
-
"autoCompactWindow": 800000,
|
|
14
13
|
"riskrails": {
|
|
15
14
|
"inherits": null,
|
|
16
15
|
"plan": "max",
|
|
17
16
|
"label": "Max",
|
|
18
17
|
"runtime": "claude",
|
|
18
|
+
"economy": {
|
|
19
|
+
"autoCompactWindow": 800000
|
|
20
|
+
},
|
|
19
21
|
"tiers": {
|
|
20
22
|
"FAST": {
|
|
21
23
|
"model": "haiku",
|