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.
Files changed (112) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +2 -2
  4. package/README.md +259 -812
  5. package/agents/ai-reviewer.md.tmpl +6 -4
  6. package/ai/policies/model-routing.md +7 -7
  7. package/ai/policies/production.md +3 -3
  8. package/ai/policies/release.md +2 -2
  9. package/ai/policies/review-economy.md +34 -32
  10. package/ai/using-riskrails.md +15 -0
  11. package/bin/claude-1m +15 -14
  12. package/bin/riskrails.js +113 -37
  13. package/codex/hooks.json +25 -2
  14. package/hooks/AGENTS.md +2 -2
  15. package/hooks/CLAUDE.md +2 -2
  16. package/hooks/ai-guard.py +29 -8
  17. package/hooks/ai-path-guard-defaults.json +3 -1
  18. package/hooks/codex-hooks.json +25 -2
  19. package/hooks/context-guard.py +252 -36
  20. package/hooks/dialog-gate.py +105 -0
  21. package/hooks/guard/common.py +7 -0
  22. package/hooks/guard/decision.py +123 -0
  23. package/hooks/guard/dialog.py +598 -0
  24. package/hooks/guard/git.py +49 -3
  25. package/hooks/guard/paths.py +89 -19
  26. package/hooks/hooks.json +37 -2
  27. package/hooks/memory-pre-push.py +28 -0
  28. package/hooks/project-scaffold.py +1 -5
  29. package/hooks/runtime-gate.py +65 -18
  30. package/hooks/update_notice.py +141 -0
  31. package/instructions/routing.md +4 -4
  32. package/instructions/stub.md +3 -3
  33. package/package.json +1 -1
  34. package/profiles/max.json +3 -1
  35. package/profiles/pro.json +3 -1
  36. package/scripts/merge-codex-config.py +134 -0
  37. package/scripts/resolve-profile.py +15 -3
  38. package/settings.common.json +39 -2
  39. package/skills/AGENTS.md +3 -3
  40. package/skills/CLAUDE.md +3 -3
  41. package/skills/ai-init/templates/.ai/AGENTS.md +3 -3
  42. package/skills/ai-init/templates/.ai/VERSION +1 -1
  43. package/skills/ai-init/templates/.ai/policies/dialog.json +1 -0
  44. package/skills/ai-init/templates/.ai/policies/safety.md +22 -2
  45. package/skills/ai-init/templates/.ai/policies/security.md +7 -1
  46. package/skills/ai-init/templates/.ai/policies/testing.md +19 -6
  47. package/skills/ai-init/templates/.ai/policies/weight.json +1 -0
  48. package/skills/ai-init/templates/.ai/policies/weight.md +2 -2
  49. package/skills/ai-init/templates/.ai/state/README.md +9 -6
  50. package/skills/ai-init/templates/AGENTS.block.md +2 -2
  51. package/skills/ai-init/templates/CLAUDE.block.md +2 -2
  52. package/skills/ai-init/templates/GEMINI.block.md +2 -2
  53. package/skills/ai-init/templates/gitignore.snippet +1 -0
  54. package/skills/ai-init/templates/junie-guidelines.block.md +2 -2
  55. package/skills/ai-task/memory_compact.py +968 -0
  56. package/skills/ai-task/memory_format.py +775 -0
  57. package/skills/ai-task/memory_store.py +528 -0
  58. package/skills/ai-task/memory_sync.py +1571 -0
  59. package/skills/ai-task/sensors.py +25 -5
  60. package/skills/ai-task/state.py +999 -79
  61. package/skills/ai-task/weight.py +3 -3
  62. package/skills/project-init/templates/constitution.md +1 -1
  63. package/skills/project-init/templates/sdlc-README.md +20 -14
  64. package/skills/project-update/history/blobs/00289f98434fed230eebe768dfb3425f83d737866a3aebc97aa95167c4fa0c6e +131 -0
  65. package/skills/project-update/history/blobs/079a91f976ad69b5cf49ccb9a28849576e4735ddd9485fc26599e528a69fd2f4 +30 -0
  66. package/skills/project-update/history/blobs/07ff7fa6f93d471ff533f553b4e523e4105ccde1f3dab70132c5237728f68422 +13 -0
  67. package/skills/project-update/history/blobs/0ef4a360c76da290173dea0f8073949cb0f8113710a38df95acae5d96bdd1286 +123 -0
  68. package/skills/project-update/history/blobs/164cd34c182de88c33de7df35beb29200d792419e1c13a5f0b2104f944e57be7 +1 -0
  69. package/skills/project-update/history/blobs/18c7a2753ce9cb6f6d3d1e9d5d1125d75950b879d56b7325225b200869631b29 +15 -0
  70. package/skills/project-update/history/blobs/1d1d907449ec74594d0ee923ac142ae746b9aedf0a1eda9469f075a44d23e3d7 +13 -0
  71. package/skills/project-update/history/blobs/23a42febbacaa7b52a802d428033a99c0694391c81d51fe3b6e1dff608a641d5 +67 -0
  72. package/skills/project-update/history/blobs/25d4f2a86deb5e2574bb3210b67bb24fcc4afb19f93a7b65a057daa874a9d18e +1 -0
  73. package/skills/project-update/history/blobs/48d9950f16e31d55475f566a80dc95f64b35f1c804d2f51729e313a4f29bac8d +13 -0
  74. package/skills/project-update/history/blobs/5353ca529c46f2443e12947ec205a6887fb594a4de1f87f0eb1c0e6c0f826133 +49 -0
  75. package/skills/project-update/history/blobs/5a93f78b4ed4d416c171139b2ce610a117f9c1dd46fbb56ca86729cb4c53d10c +6 -0
  76. package/skills/project-update/history/blobs/5f65bc7ab3fc3c4e75ae05d49eaf4331fafc398c661abe33c69f7380a720344c +32 -0
  77. package/skills/project-update/history/blobs/6a3be0e624e5ad91e44d08857b7183ee53b7903e1e8d56ff196dbc9dde271e4a +75 -0
  78. package/skills/project-update/history/blobs/7170553410bba904ffb7edb4b231e3b3579b85542d84de717299980938f82c9d +13 -0
  79. package/skills/project-update/history/blobs/774b1e3a04ef0d49d33c44a2cfecd92eb6f6b738a12b6a681103ba7fc13e7b86 +67 -0
  80. package/skills/project-update/history/blobs/7be527cd648b27cca6e267de30f8e33a2ce17d5631a82694f23ae5c0623efe1f +15 -0
  81. package/skills/project-update/history/blobs/7ee99edafe7cec45ff577376c0d0178fdb2690134d4656488f46e129aba3cc5a +67 -0
  82. package/skills/project-update/history/blobs/7f063e79195a5c85c37b7260dd20c184d3b9bea85e37202e0bbca73ad84bba49 +13 -0
  83. package/skills/project-update/history/blobs/8b3ef9586491a426767c01da5d01bdd6b0655c576c68c4cc6dfb093367817dc0 +67 -0
  84. package/skills/project-update/history/blobs/917df3320d778ddbaa5c5c7742bc4046bf803c36ed2b050f30844ed206783469 +1 -0
  85. package/skills/project-update/history/blobs/935af1019931058ec3a0b834fe230e127ec914f75edcc4a6c589cfe733b0b979 +13 -0
  86. package/skills/project-update/history/blobs/ae5ce131d6bae401199ff32d8c3dc4d8aec188010a9111624ac64506e2116ff4 +84 -0
  87. package/skills/project-update/history/blobs/c8573ca1f810ce6c4ac49456a4c1725b93e0d3b74c54ce95d46d8747777895f0 +13 -0
  88. package/skills/project-update/history/blobs/ca4499d829268c8766fb3b825a515e88dc55c237e9d202816b50959b4dac031f +13 -0
  89. package/skills/project-update/history/blobs/d6e5b34d475e7187d2a8cb2aac7e7f1a427142baf6461bda387d70ceb4d8a8c1 +30 -0
  90. package/skills/project-update/history/blobs/e120ff4e32b2e950fc94ada4e787d57dc9c790da37223e5a202d74f7e3aadf80 +13 -0
  91. package/skills/project-update/history/blobs/e9adb7a9905eb719ac78ab96671f055da53e5001810f5ad6d6c94306b0323fe7 +74 -0
  92. package/skills/project-update/history/blobs/fae3064e648cdc55be4aeb1177525b302a5e34ebd752e4a904f47dc0f4557b96 +71 -0
  93. package/skills/project-update/history/index.json +60 -20
  94. package/skills/project-update/migrations/0009_state_file.py +25 -0
  95. package/skills/project-update/migrations/0010_dialog_switch.py +30 -0
  96. package/skills/project-update/migrations/0011_retire_risk_tiers.py +37 -0
  97. package/skills/project-update/migrations/README.md +2 -1
  98. package/skills/project-update/migrations/__init__.py +6 -1
  99. package/skills/project-update/render_instructions.py +1 -1
  100. package/skills/project-update/update.py +385 -89
  101. package/skills/rr-build/SKILL.md +14 -5
  102. package/skills/rr-build-all/SKILL.md +190 -0
  103. package/skills/rr-plan/SKILL.md +16 -4
  104. package/skills/rr-review/SKILL.md +24 -9
  105. package/skills/rr-setup/SKILL.md +15 -2
  106. package/skills/rr-setup/setup.py +7 -1
  107. package/skills/rr-ship/SKILL.md +16 -8
  108. package/skills/rr-spec/SKILL.md +4 -2
  109. package/skills/rr-status/SKILL.md +2 -2
  110. package/skills/usage-report/usage-report.py +45 -10
  111. package/skills/ai-init/templates/.ai/policies/risk-tiers.json +0 -628
  112. package/skills/ai-init/templates/.ai/policies/risk-tiers.md +0 -180
@@ -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
- CONFIRM = "--co(n(f(i(r(m(-(d(e(l(e(t(e)?)?)?)?)?)?)?)?)?)?)?)?(=|[[:space:]]|['\"]|$)"
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
- SETBY_VERB = ("([[:space:]]+--)?[[:space:]]+" + Q + "set" + END + "([^|;&`]*[[:space:]])?" + Q + "--by?"
88
- + Q + "(=|[[:space:]]|$)")
89
- SETBY_RE = (BOUND + "(python3?[[:space:]]+)?" + Q + "[^[:space:]'\"]*state\\.py" + Q + ARGS + SETBY_VERB + "|"
90
- + BOUND + "(python3?[[:space:]]+)?" + Q + VAR + Q + ARGS + SETBY_VERB)
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
- for e in entries:
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(APPROVE_RE, cmd_joined) and not unattended and self.task_in_flight():
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 m.ere_match(SETBY_RE, cmd_joined) and not unattended and self.task_in_flight():
1205
- raise Deny(T("Refusing 'state.py set \u2026 --by' from an agent session: a verdict with --by is the "
1206
- "human's, given when the review budget is spent.") + WHY_SET_BY)
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": "Read|Edit|Write|MultiEdit|NotebookEdit|Bash",
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 scope"
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
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
- """Runtime gate: one hook for both runtimes, replacing fable-gate.py and
3
- codex-model-gate.py (which remain as shims that exec this file).
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>] records it when the weekly limit is nearly used, then
16
- runs the user's own statusline command on the same input
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"fable-gate: {why}; {name} runs on {FALLBACK}",
418
+ "permissionDecisionReason": f"runtime-gate: {why}; {name} runs on {FALLBACK}",
418
419
  "updatedInput": {**tool_input, "model": FALLBACK},
419
420
  "additionalContext": (
420
- f"fable-gate routed this {name} agent to {FALLBACK}: {why}. "
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"codex-model-gate: {why}. {label} runs on {to}{via} instead, so its answer is "
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"codex-model-gate: {why}. {label} is not rerouted ({not_why}): it runs on {EXPERT} "
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, then hand the same
1120
- input to the user's own statusline command and pass its output and exit code
1121
- through. The check never gets in the way of the statusline."""
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
- if not disabled() and RUNTIME == "claude":
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
- payload = json.loads(data.decode("utf-8", "replace"))
1126
- if isinstance(payload, dict):
1127
- statusline(payload)
1128
- except Exception: # pylint: disable=broad-exception-caught # fail open: a bad payload never breaks the statusline
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)
@@ -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, and from {{CONTEXT_BLOCK}} the prompt is held back once — sending it again goes through. 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.
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 commits, merges or deploys on its own; the pipeline ends at human approval.
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 commits, merges or deploys on its own; the pipeline ends at human approval.
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 -->
@@ -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 commits, pushes, merges or deploys; the pipeline ends at human approval, given outside the agent. A guard's refusal is not a hint to reword.{{TRUST}}
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 commits, merges or deploys; approval is given by a human outside the agent. 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` (this project's principles).
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.0.0",
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",