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
package/profiles/pro.json CHANGED
@@ -8,12 +8,14 @@
8
8
  "claude-sonnet-5": { "effortLevel": "medium" },
9
9
  "claude-haiku-4-5-20251001": { "effortLevel": "high" }
10
10
  },
11
- "autoCompactWindow": 300000,
12
11
  "riskrails": {
13
12
  "inherits": null,
14
13
  "plan": "pro",
15
14
  "label": "Pro",
16
15
  "runtime": "claude",
16
+ "economy": {
17
+ "autoCompactWindow": 300000
18
+ },
17
19
  "tiers": {
18
20
  "FAST": {
19
21
  "model": "haiku",
@@ -16,12 +16,19 @@ Exactly five keys are managed:
16
16
  agents.default_subagent_reasoning_effort
17
17
  agents.max_concurrent_threads_per_session
18
18
 
19
+ and, with ``--status-context yes``, ``tui.status_line``: ``model-with-reasoning``
20
+ and ``context-used`` are appended to a list the user set, when missing; the user's
21
+ items are never removed or reordered. An unset key (Codex's own default items) and
22
+ an empty list are left alone, with a note: writing ours would replace either.
23
+ ``--status-context no`` (the default) leaves the list alone.
24
+
19
25
  Safety contract: the file is parsed before and after the edit, and the write is
20
26
  abandoned unless the *only* difference between the two parses is inside that
21
27
  managed set. A prose key name inside a multi-line string cannot slip through
22
28
  that check, because it would show up as an unexpected difference.
23
29
 
24
30
  merge-codex-config.py <config.toml> --profile <codex-plus.json|codex-pro.json> [--dry-run] [--no-backup]
31
+ [--status-context yes|no]
25
32
  """
26
33
  import argparse
27
34
  import json
@@ -37,6 +44,7 @@ MANAGED_AGENTS = (
37
44
  "default_subagent_reasoning_effort",
38
45
  "max_concurrent_threads_per_session",
39
46
  )
47
+ STATUS_ITEMS = ("model-with-reasoning", "context-used") # Codex 0.165 [tui] status_line ids
40
48
  TABLE_HEADER = re.compile(r"^\s*\[")
41
49
 
42
50
 
@@ -46,6 +54,8 @@ def die(message):
46
54
 
47
55
 
48
56
  def toml_value(value):
57
+ if isinstance(value, list):
58
+ return "[%s]" % ", ".join(toml_value(v) for v in value)
49
59
  if isinstance(value, bool):
50
60
  return "true" if value else "false"
51
61
  if isinstance(value, (int, float)):
@@ -100,6 +110,121 @@ def set_in_span(lines, start, end, key, value, changes):
100
110
  return end + 1
101
111
 
102
112
 
113
+ def value_end(lines, at):
114
+ """One past the last line of the value assigned on line `at` (an array may span lines)."""
115
+ chunk = lines[at].split("=", 1)[1]
116
+ for j in range(at, len(lines)):
117
+ if j > at:
118
+ chunk += "\n" + lines[j]
119
+ try:
120
+ tomllib.loads("v = " + chunk)
121
+ return j + 1
122
+ except tomllib.TOMLDecodeError:
123
+ continue
124
+ return None
125
+
126
+
127
+ def status_items(lines, before, changes, notes):
128
+ """Append STATUS_ITEMS to the user's [tui] status_line where missing; the user's
129
+ items keep their order. Returns the wanted list, or None when nothing is to change,
130
+ the key is unset or empty, or it lives somewhere this edit does not reach."""
131
+ current = before.get("tui.status_line")
132
+ if current is None or current == []:
133
+ # Unset is Codex's own default set, and [] a deliberate empty line: writing
134
+ # our two items would replace either.
135
+ notes.append("left [tui] status_line alone (%s; add %s to show them)"
136
+ % ("not set: Codex's default items" if current is None else "empty on purpose",
137
+ " and ".join(STATUS_ITEMS)))
138
+ return None
139
+ if not isinstance(current, list):
140
+ return None
141
+ missing = [item for item in STATUS_ITEMS if item not in current]
142
+ if not missing:
143
+ return None
144
+ value = current + missing
145
+ rendered = "status_line = %s" % toml_value(value)
146
+ span = table_span(lines, "tui")
147
+ at = key_line(lines, span[0] + 1, span[1], "status_line") if span else None
148
+ if at is None:
149
+ notes.append("left [tui] status_line alone (not a status_line key in a [tui] table)")
150
+ return None
151
+ end = value_end(lines, at)
152
+ if end is None:
153
+ return None
154
+ was = " ".join(line.strip() for line in lines[at:end])
155
+ kept = append_items(lines[at:end], missing)
156
+ try:
157
+ ok = kept is not None and tomllib.loads("\n".join(kept))["status_line"] == value
158
+ except (tomllib.TOMLDecodeError, KeyError):
159
+ ok = False
160
+ # The user's own lines and comments stay; one rendered line only when they cannot.
161
+ lines[at:end] = kept if ok else [rendered]
162
+ changes.append("set [tui] %s (was: %s)" % (rendered, was))
163
+ return value
164
+
165
+
166
+ def array_marks(block):
167
+ """(line, col) of the closing ] of the array assigned on block[0], and of the last
168
+ character before it outside strings, comments and blanks; None for an array this
169
+ scan does not read (nested, or a multi-line string)."""
170
+ row, col = 0, block[0].index("=") + 1
171
+ depth, last, quote = 0, None, None
172
+ while row < len(block):
173
+ line = block[row]
174
+ while col < len(line):
175
+ ch = line[col]
176
+ if quote:
177
+ if quote == '"' and ch == "\\":
178
+ col += 2
179
+ continue
180
+ if ch == quote:
181
+ quote = None
182
+ last = (row, col)
183
+ elif ch in "\"'":
184
+ if line.startswith(ch * 3, col):
185
+ return None
186
+ quote = ch
187
+ elif ch == "#":
188
+ break
189
+ elif ch == "[":
190
+ depth += 1
191
+ if depth > 1:
192
+ return None
193
+ last = (row, col)
194
+ elif ch == "]":
195
+ return (row, col), last
196
+ elif not ch.isspace():
197
+ last = (row, col)
198
+ col += 1
199
+ if quote:
200
+ return None
201
+ row, col = row + 1, 0
202
+ return None
203
+
204
+
205
+ def append_items(block, items):
206
+ """The lines of `block` (one `status_line = [...]` assignment) with `items` added
207
+ before its closing ], comments and layout kept; None when the scan cannot place them."""
208
+ marks = array_marks(block)
209
+ if marks is None or marks[1] is None:
210
+ return None
211
+ (row, col), (lrow, lcol) = marks
212
+ out = list(block)
213
+ comma = block[lrow][lcol] not in ",["
214
+ if block[row][:col].strip():
215
+ # ] shares its line with an item: `["a"] # c` → `["a", "x", "y"] # c`.
216
+ joined = ", ".join(toml_value(item) for item in items)
217
+ lead = ", " if comma else ("" if block[lrow][lcol] == "[" else " ")
218
+ out[row] = out[row][:col] + lead + joined + out[row][col:]
219
+ else:
220
+ # ] on its own line: one `"item",` line each, at the last item's indentation.
221
+ indent = re.match(r"\s*", block[lrow]).group(0) if lrow > 0 else re.match(r"\s*", block[row]).group(0) + " "
222
+ out[row:row] = ["%s%s," % (indent, toml_value(item)) for item in items]
223
+ if comma:
224
+ out[lrow] = out[lrow][:lcol + 1] + "," + out[lrow][lcol + 1:]
225
+ return out
226
+
227
+
103
228
  def flatten(data, prefix=""):
104
229
  out = {}
105
230
  for key, value in data.items():
@@ -117,6 +242,7 @@ def main():
117
242
  parser.add_argument("--profile", required=True)
118
243
  parser.add_argument("--dry-run", action="store_true")
119
244
  parser.add_argument("--no-backup", action="store_true")
245
+ parser.add_argument("--status-context", choices=("yes", "no"), default="no")
120
246
  args = parser.parse_args()
121
247
 
122
248
  try:
@@ -182,6 +308,12 @@ def main():
182
308
  continue
183
309
  stop = set_in_span(lines, start + 1, stop, key, wanted["agents.%s" % key], changes)
184
310
 
311
+ notes = []
312
+ if args.status_context == "yes":
313
+ status = status_items(lines, before, changes, notes)
314
+ if status is not None:
315
+ wanted["tui.status_line"] = status
316
+
185
317
  text = "\n".join(lines)
186
318
  if trailing_newline or text:
187
319
  text = text.rstrip("\n") + "\n"
@@ -201,6 +333,8 @@ def main():
201
333
  if after.get(key) != value:
202
334
  die(f"refusing to write: {key} did not end up as {value!r}")
203
335
 
336
+ for note in notes:
337
+ print("codex config: %s" % note)
204
338
  if not changes:
205
339
  print("codex config: already set (model=%s, effort=%s)" % (wanted["model"], wanted["model_reasoning_effort"]))
206
340
  return 0
@@ -21,9 +21,14 @@ Resolution:
21
21
  ``pro.json``, ``team-max`` resolves ``max.json``).
22
22
  * ``--fable yes|no`` records ``fable`` and drops ``architect_model_with_fable``
23
23
  when Fable is off, so a reader never has to know the flag.
24
+ * ``--context default|economy`` (default ``default``) records ``context: {mode,
25
+ window}``. ``economy`` copies ``riskrails.economy.autoCompactWindow`` to the
26
+ settings' top level and into ``window``; ``default`` leaves the runtime its own
27
+ compaction, so ``window`` is null and settings carry no ``autoCompactWindow``.
24
28
 
25
29
  resolve-profile.py <name|path> [--src DIR] [--plan P] [--label L]
26
- [--fable yes|no] [--print full|settings|agentic]
30
+ [--fable yes|no] [--context default|economy]
31
+ [--print full|settings|agentic]
27
32
 
28
33
  ``settings`` prints the profile without ``riskrails``; ``agentic`` prints
29
34
  the resolved ``riskrails`` object; ``full`` (the default) prints both.
@@ -79,7 +84,7 @@ def codex_tiers(data):
79
84
  return tiers
80
85
 
81
86
 
82
- def resolve(src, name, plan=None, label=None, fable=None):
87
+ def resolve(src, name, plan=None, label=None, fable=None, context="default"):
83
88
  data = load(src, name)
84
89
  agentic = data.get(KEY)
85
90
  if not isinstance(agentic, dict):
@@ -94,6 +99,12 @@ def resolve(src, name, plan=None, label=None, fable=None):
94
99
  agentic["fable"] = fable
95
100
  if not fable:
96
101
  agentic.get("tiers", {}).get("EXPERT", {}).pop("architect_model_with_fable", None)
102
+ window = None
103
+ if context == "economy":
104
+ window = (agentic.get("economy") or {}).get("autoCompactWindow")
105
+ if window:
106
+ data["autoCompactWindow"] = window
107
+ agentic["context"] = {"mode": context, "window": window}
97
108
  return data
98
109
 
99
110
 
@@ -105,12 +116,13 @@ def main(argv=None):
105
116
  ap.add_argument("--plan")
106
117
  ap.add_argument("--label")
107
118
  ap.add_argument("--fable", choices=("yes", "no"))
119
+ ap.add_argument("--context", choices=("default", "economy"), default="default")
108
120
  ap.add_argument("--print", dest="what", choices=("full", "settings", "agentic"), default="full")
109
121
  args = ap.parse_args(argv)
110
122
 
111
123
  fable = None if args.fable is None else args.fable == "yes"
112
124
  try:
113
- data = resolve(args.src, args.profile, args.plan, args.label, fable)
125
+ data = resolve(args.src, args.profile, args.plan, args.label, fable, args.context)
114
126
  except (OSError, ValueError) as exc:
115
127
  print(f"resolve-profile: {exc}", file=sys.stderr)
116
128
  return 2
@@ -26,13 +26,13 @@
26
26
  ]
27
27
  },
28
28
  {
29
- "matcher": "Read|Edit|Write|MultiEdit|NotebookEdit|Bash",
29
+ "matcher": "*",
30
30
  "hooks": [
31
31
  {
32
32
  "type": "command",
33
33
  "command": "python3 -I -S \"$HOME/.claude/hooks/ai-guard.py\"",
34
34
  "timeout": 15,
35
- "statusMessage": "Checking git, paths and scope"
35
+ "statusMessage": "Checking git, paths, scope and open decisions"
36
36
  }
37
37
  ]
38
38
  }
@@ -84,6 +84,43 @@
84
84
  }
85
85
  ]
86
86
  }
87
+ ],
88
+ "Notification": [
89
+ {
90
+ "matcher": "auth_success",
91
+ "hooks": [
92
+ {
93
+ "type": "command",
94
+ "command": "\"$HOME/.claude/hooks/context-guard.py\"",
95
+ "timeout": 15
96
+ }
97
+ ]
98
+ }
99
+ ],
100
+ "PostToolUse": [
101
+ {
102
+ "matcher": "AskUserQuestion",
103
+ "hooks": [
104
+ {
105
+ "type": "command",
106
+ "command": "python3 -I -S \"$HOME/.claude/hooks/dialog-gate.py\"",
107
+ "timeout": 5,
108
+ "statusMessage": "Recording the dialog's answer"
109
+ }
110
+ ]
111
+ }
112
+ ],
113
+ "Stop": [
114
+ {
115
+ "hooks": [
116
+ {
117
+ "type": "command",
118
+ "command": "python3 -I -S \"$HOME/.claude/hooks/dialog-gate.py\"",
119
+ "timeout": 5,
120
+ "statusMessage": "Checking for an open decision"
121
+ }
122
+ ]
123
+ }
87
124
  ]
88
125
  }
89
126
  }
package/skills/AGENTS.md CHANGED
@@ -1,6 +1,6 @@
1
- <!-- claude-agentic:rule:skills:start -->
1
+ <!-- riskrails:rule:skills:start -->
2
2
  # Skills rules
3
3
  - Weigh a change by what it does to a user project's decisions, never by the skill being a process (constitution C6). The mode is what W gives (`state.py weight`); the rows below set the behaviour axis.
4
4
  - behaviour 0: skill or agent text, `usage-report`, no `state.py` refusal moves. behaviour 1: adds a `state.py` check, a sensor or a refusal. behaviour 2: weakens a refusal, changes the approval path (`approve`, `questions --sync`, `AI_UNATTENDED`, the review verdict transitions) or changes install and migration (the `project-update` merge; a migration also raises reversibility through its hint) — declare it, `--declare behaviour=2:"<the effect>"`. Start such a task with `start --mode full --by <name>`: its weight lands in guarded, and these effects keep a STRONG review and the gate.
5
- - A change to a shipped template or to `ai/` runs `tools/build-template-history.py` in the same task, or `tests/test-project-update.sh` goes red.
6
- <!-- claude-agentic:rule:skills:end -->
5
+ - A change to a shipped template or to `ai/` runs `tools/build-template-history.py` in the same task, or `tests/test_project_update.py` (`test_history_covers_every_template`) goes red.
6
+ <!-- riskrails:rule:skills:end -->
package/skills/CLAUDE.md CHANGED
@@ -1,6 +1,6 @@
1
- <!-- claude-agentic:rule:skills:start -->
1
+ <!-- riskrails:rule:skills:start -->
2
2
  # Skills rules
3
3
  - Weigh a change by what it does to a user project's decisions, never by the skill being a process (constitution C6). The mode is what W gives (`state.py weight`); the rows below set the behaviour axis.
4
4
  - behaviour 0: skill or agent text, `usage-report`, no `state.py` refusal moves. behaviour 1: adds a `state.py` check, a sensor or a refusal. behaviour 2: weakens a refusal, changes the approval path (`approve`, `questions --sync`, `AI_UNATTENDED`, the review verdict transitions) or changes install and migration (the `project-update` merge; a migration also raises reversibility through its hint) — declare it, `--declare behaviour=2:"<the effect>"`. Start such a task with `start --mode full --by <name>`: its weight lands in guarded, and these effects keep a STRONG review and the gate.
5
- - A change to a shipped template or to `ai/` runs `tools/build-template-history.py` in the same task, or `tests/test-project-update.sh` goes red.
6
- <!-- claude-agentic:rule:skills:end -->
5
+ - A change to a shipped template or to `ai/` runs `tools/build-template-history.py` in the same task, or `tests/test_project_update.py` (`test_history_covers_every_template`) goes red.
6
+ <!-- riskrails:rule:skills:end -->
@@ -13,8 +13,8 @@ Everything else loads on demand; nothing here is worth reading twice.
13
13
  The task names the files it may touch; an edit outside them is refused —
14
14
  answer `SCOPE_CHANGE_REQUIRED` and widen the task in place with
15
15
  `state.py start --amend --files "+<path>" --why "…"`, never around the guard.
16
- 3. No agent commits, pushes, merges or deploys. Approval is given by a human,
17
- outside the agent.
16
+ 3. No agent merges or deploys; commit, push, approval only as the dialog's picked
17
+ command, or the human runs them outside the agent.
18
18
  4. Verify before reporting done: the verification command once, to the end,
19
19
  every failure fixed as one batch, the output shown. Run it through
20
20
  `state.py verify --scope all` — it caps the runs, keeps the log out of the
@@ -31,7 +31,7 @@ Everything else loads on demand; nothing here is worth reading twice.
31
31
  ## Shared files
32
32
 
33
33
  This `.ai/` holds `project/`, `rules/`, `state/`, `reports/`, `VERSION` and, in
34
- `policies/`, `risk-tiers.json`, `risk-tiers.md`, `path-guard.json`, `testing.md`,
34
+ `policies/`, `weight.json`, `weight.md`, `path-guard.json`, `testing.md`,
35
35
  `coding.md`, `safety.md` and `security.md`. Every other path below ships with the
36
36
  plugin at `<home>/riskrails/ai/<path>`; `<home>` is `~/.claude`, or
37
37
  `$CODEX_HOME` (default `~/.codex`). A file at the same path here overrides it and
@@ -1 +1 @@
1
- 8
1
+ 11
@@ -0,0 +1 @@
1
+ { "version": 1, "enabled": true, "window_seconds": 120, "stop_blocks_max": 2 }
@@ -23,8 +23,15 @@ is evidence; your intuition about how the code "should" look is not.
23
23
  - Read production secrets or customer data into the model context.
24
24
 
25
25
  If you find a problem outside the current task: **write it down, do not fix it.**
26
- Unrelated findings go to `.ai/project/known-risks.md` or into the report's
27
- "observations" section, and the human decides whether they become their own task.
26
+ Unrelated findings are recorded with `state.py memory risk add` (in files mode it
27
+ writes `.ai/project/known-risks.md`) or go into the report's "observations"
28
+ section, and the human decides whether they become their own task.
29
+
30
+ The compact record is the human's to switch: `state.py memory enable`, `disable`,
31
+ `cleanup`, `pack`, `redact` and `migrate --move-logs` take the human route, as
32
+ `approve` does, and `.ai/memory/` and `.ai/local/memory/` are written by
33
+ `state.py` alone — an agent never edits them (`state.py memory --help`; the
34
+ plugin's `docs/local-memory.md`).
28
35
 
29
36
  ## Scope
30
37
 
@@ -43,6 +50,16 @@ Codex has no hookable read tool, so the large-read cap is Claude-only there —
43
50
  an agent that behaves correctly only because a hook is watching is not behaving
44
51
  correctly. The rules above hold whether or not a hook enforces them.
45
52
 
53
+ **The guards' threat model.** The guards defend against an agent's mistakes — a
54
+ careless `rm`, a write to a frozen policy during a task, a commit with a
55
+ secret — not against an agent that constructs a command to exhaust them. A
56
+ command built to time the guard out is visible in the transcript and in the
57
+ journal, and the human gate in full and full_irreversible mode is the boundary
58
+ for anything that matters.
59
+ Approval rests on a guard plus a ledger: a command the agent constructs to pass
60
+ for the human's pick is outside the threat model, and `state.py consent audit`
61
+ detects it afterwards.
62
+
46
63
  ## Evidence labels
47
64
 
48
65
  Anything an agent writes into `.ai/project/` carries one of four labels:
@@ -62,3 +79,6 @@ evidence for each, and let the human decide which is the bug.
62
79
 
63
80
  Stop and ask. An agent that pauses costs a message; an agent that guesses on a
64
81
  payment path costs an incident.
82
+
83
+ Doubt on reversibility or domain is a blocker; other doubt is written to
84
+ `task.md` and the work continues.
@@ -57,12 +57,18 @@ it, add its pattern to `deny_patterns` (a `dependency_instruction_patterns`
57
57
  list from an older `path-guard.json` is read as `deny_patterns`).
58
58
 
59
59
  Always read-only for an agent: the task's state under `.ai/state/`, its
60
- `task.md` and `events.jsonl`, `.ai/policies/*.json`, and the installed guards —
60
+ `task.md` and `events.jsonl`, `.ai/policies/*.json`, the compact
61
+ record under `.ai/memory/` and `.ai/local/memory/` (written by `state.py` alone),
62
+ and the installed guards —
61
63
  their hooks, engine and `skills/ai-task/` under `.claude/` and `.codex/`. While a
62
64
  task is in flight the runtime's settings and this project's `.ai/policies/` are
63
65
  read-only too. Changing the rules a run is judged by, inside that run, is not a
64
66
  change an agent may make on its own.
65
67
 
68
+ A review's evidence for a closed task is its record — `state.py memory show <id>`
69
+ and `recall` — once cleanup has moved the report directory; an approval in the
70
+ record authorises nothing.
71
+
66
72
  Project-specific additions go in `.ai/policies/path-guard.json`
67
73
  (`deny_patterns`, `allow_patterns`, `protected_config_patterns`,
68
74
  `task_protected_patterns`; allow wins over all of them). When a deny is wrong, widen the allow list in that file — do not
@@ -47,13 +47,15 @@ else runs in between.
47
47
  which, in one line; silence is not a verdict.
48
48
  - A failure in a step's own tests is fixed in that step, before `step-done`;
49
49
  the batch rule below is for the two end-of-task runs.
50
+ - After a green suite, a documents-only edit runs `doc_tests` (step scope), not the suite.
50
51
 
51
52
  ## Reading a failure
52
53
 
53
54
  **Run to the end, then fix as one batch.** The verification command runs with
54
55
  no fail-fast flag and is never interrupted at the first failure. Collect every
55
- failure, classify each one, fix all the new regressions in a single remediation
56
- step (`state.py remediate`), and run the command once more. One failure never
56
+ failure, classify each one, fix all the new regressions in one batch
57
+ (`state.py start --amend` widens the task when a fix needs more files), and run the
58
+ command once more. One failure never
57
59
  sends the task back to the start: the plan, the tier and the finished steps
58
60
  stand. At most two remediation rounds; a third means the human decides.
59
61
 
@@ -66,6 +68,9 @@ Classify before fixing. The four answers are:
66
68
  is missing.
67
69
  - **UNKNOWN** — you could not tell. Say what you tried.
68
70
 
71
+ A review's MEDIUM and LOW findings are recorded with `state.py memory risk add`, not
72
+ fixed in the task; only a BLOCKER, a HIGH or a regression buys an `--after-fix` run.
73
+
69
74
  **Never edit a test to make it pass when production behaviour changed
70
75
  unexpectedly.** A test that suddenly disagrees with the code is evidence, and
71
76
  deleting evidence is the worst available option.
@@ -88,6 +93,7 @@ verify_command: # the full fast suite, e2e excluded — e.g. make test |
88
93
  step_test_command: # the same runner, scoped to a path or filter — e.g. vendor/bin/phpunit {files} | pytest {files}
89
94
  e2e_command: # the end-to-end suite, run once at the end of a task — e.g. make e2e | vendor/bin/behat | npx playwright test
90
95
  lint_command: # e.g. vendor/bin/php-cs-fixer fix --dry-run | ruff check . | npm run lint — write `none` if the project has none
96
+ doc_tests: # the suites a documents-only edit runs after a green suite — e.g. tests/test_docs.py; `none` if the project has none
91
97
  typecheck_command: # e.g. vendor/bin/phpstan analyse --no-progress | mypy . | npx tsc --noEmit — `none` if the project has none
92
98
  healthy_output: # two or three lines of what a green run ends with
93
99
  runtime: # roughly, for each command, so a hang is recognisable
@@ -99,12 +105,19 @@ step names; without it the paths are appended. The placeholder is what lets
99
105
  `sensors.py` run a step's own tests for the "test must bite" check, so a runner
100
106
  that only takes a `--filter` gets `bite: unavailable` rather than a wrong answer.
101
107
 
108
+ `bite_codes` (optional) lists the exit codes that mean "a test failed" when a
109
+ test is shown failing before its fix; the default is `1`. Runners that exit
110
+ differently say so here — `bite_codes: 1,2` for pytest (2 is a collection
111
+ error, such as a missing import), `1,2` for phpunit, `101` for cargo. Any other
112
+ code is the runner's own usage error and does not count as a bite.
113
+
102
114
  `lint_command` and `typecheck_command` are read by the deterministic sensors,
103
- never by a model. They decide, together with the test run, whether a T2 change
104
- can skip its model review (`risk-tiers.json`, `sensors`). A **missing** line is
115
+ never by a model. They decide, together with the test run, whether a light-mode change
116
+ can skip its model review (`state.py close` measures them). A **missing** line is
105
117
  `unavailable` and keeps the review; only the literal `none`, written by a human,
106
- says the project has no such tool. `sensors.py detect` proposes the exact line
107
- from the config files it finds, and never runs what it detected.
118
+ says the project has no such tool. Read the exact line from the project's
119
+ config files (`package.json`, `composer.json`, `pyproject.toml`, `Makefile`), and
120
+ never run a command just to find it out.
108
121
 
109
122
  If the project has no e2e suite, write `e2e_command: none` and say so — an
110
123
  empty line is read as "not written down yet" and the next task will go looking
@@ -40,6 +40,7 @@
40
40
  "review_rounds_max": 2,
41
41
  "remediation_rounds": 2,
42
42
  "tests": {"max_suite_runs": 2, "timeout_seconds": 1800, "env_retries": 1, "log_max_bytes": 2097152},
43
+ "batch": {"default": false, "review_rounds_max": 3, "max_suite_runs": 8, "parallel": 3, "tier": "BALANCED"},
43
44
  "examples": {
44
45
  "light": ["a font or copy change in one template", "a test-only change", "a message-text change"],
45
46
  "guarded": ["a regex fix in a hot guard script", "a dev-environment manifest", "a feature with a new screen and an additive migration"],
@@ -1,6 +1,6 @@
1
1
  # Weight table
2
2
 
3
- <!-- generated by `state.py weight --mirror` from weight.json; edit the JSON and regenerate. sha256: 7f3859c34035fe32abf937eebdd7240ac72ab32d254286cff7d939cf0cf6eaef -->
3
+ <!-- generated by `state.py weight --mirror` from weight.json; edit the JSON and regenerate. sha256: 5353ca529c46f2443e12947ec205a6887fb594a4de1f87f0eb1c0e6c0f826133 -->
4
4
 
5
5
  Six axes, 0–2 each; W is their sum. Why each rule is what it is: the plugin's `docs/weight.md`.
6
6
 
@@ -29,4 +29,4 @@ Six axes, 0–2 each; W is their sum. Why each rule is what it is: the plugin's
29
29
 
30
30
  Rules: {"unknown_scores": 1, "hint_raises_by": 1, "hint_raises_once_per_axis": true, "rescore_axes": ["reversibility", "domain"], "rescore_direction": "up", "lower_by": "human", "steps_advice_over": 8}
31
31
 
32
- Budgets: review_rounds_max 2 · remediation_rounds 2 · tests {"max_suite_runs": 2, "timeout_seconds": 1800, "env_retries": 1, "log_max_bytes": 2097152}
32
+ Budgets: review_rounds_max 2 · remediation_rounds 2 · tests {"max_suite_runs": 2, "timeout_seconds": 1800, "env_retries": 1, "log_max_bytes": 2097152} · batch {"default": false, "review_rounds_max": 3, "max_suite_runs": 8, "parallel": 3, "tier": "BALANCED"}
@@ -1,6 +1,6 @@
1
1
  # Task state
2
2
 
3
- `current.json` is the machine-readable record of the task in flight. It is
3
+ `state.json` is the machine-readable record of the task in flight. It is
4
4
  written by `skills/ai-task/state.py` (atomically), read by `/ai-status`, and
5
5
  enforced by `ai-scope-guard`, which derives the files the current step may touch
6
6
  from it.
@@ -10,15 +10,18 @@ Two other writers exist, and only these two. A schema migration in
10
10
  same atomic write and a `schema_migrated` history entry — that is how a task in
11
11
  flight survives a change to the shape of this file. And `hooks/context-guard.py`
12
12
  writes `session.json`, the sidecar beside this file, and nothing else: it never
13
- touches `current.json`. Nothing else writes any of it — an agent never edits
13
+ touches `state.json`. Nothing else writes any of it — an agent never edits
14
14
  them by hand, and the path guard refuses when one tries.
15
15
 
16
16
  It is **not committed**: it describes a session, not the repository. The
17
17
  `.gitignore` entry added by `/ai-init` keeps it out of git.
18
18
 
19
- When a task closes, `state.py archive` moves it to
20
- `.ai/reports/<task-id>/state.json`, which **is** committed — that is the audit
21
- trail: which stages ran, in what order, who approved what, and when.
19
+ When a task closes, `state.py close` writes the final state to
20
+ `.ai/state/logs/<task-id>/state.json`, beside the task's verify logs — local,
21
+ **not** committed. What is committed is `.ai/reports/<task-id>/events.jsonl`
22
+ and `task.md`, and in packages mode the task's compact record under
23
+ `.ai/memory/` — that is the audit trail: which stages ran, in what order, who
24
+ approved what, and when (the plugin's `docs/local-memory.md`).
22
25
 
23
26
  It holds facts, never transcripts:
24
27
 
@@ -42,7 +45,7 @@ defaults.
42
45
 
43
46
  | File | Written by | Committed |
44
47
  |---|---|---|
45
- | `current.json` | `state.py`, and `update.py` during a migration | no |
48
+ | `state.json` (legacy `current.json`, still read) | `state.py`, and `update.py` during a migration | no |
46
49
  | `session.json` | `context-guard.py`, on SessionStart and each prompt | no |
47
50
  | `handoff.md` | `state.py handoff` — never a model | no |
48
51
  | `../reports/<task-id>/task.md` §Questions | `state.py ask/answer/questions` | yes |
@@ -8,6 +8,6 @@ This repository runs an agentic pipeline under `.ai/`. Read `.ai/AGENTS.md` firs
8
8
  - A change runs through `/rr-build <request>`; `/rr-status` shows where it stands. Each step names the files it may touch — an edit outside them is refused: answer `SCOPE_CHANGE_REQUIRED`. One `apply_patch` is checked file by file: a patch that reaches outside the step is refused whole.
9
9
 
10
10
  - 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.
11
- - 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.
12
- - 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).
11
+ - 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.
12
+ - 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`.
13
13
  <!-- riskrails:end -->
@@ -8,6 +8,6 @@ This repository runs an agentic pipeline under `.ai/`. Read `.ai/AGENTS.md` firs
8
8
  - A change runs through `/rr-build <request>`; `/rr-status` shows where it stands. Each step names the files it may touch — an edit outside them is refused: answer `SCOPE_CHANGE_REQUIRED`.
9
9
 
10
10
  - 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.
11
- - 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.
12
- - 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).
11
+ - 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.
12
+ - 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`.
13
13
  <!-- riskrails:end -->
@@ -8,6 +8,6 @@ This repository runs an agentic pipeline under `.ai/`. Read `.ai/AGENTS.md` firs
8
8
  - Changes run through the pipeline (`/rr-build` in Claude Code or Codex); without it, follow `.ai/policies/safety.md`, touch only the files the current task names, and answer questions in prose or by filling the file.
9
9
 
10
10
  - 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.
11
- - 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.
12
- - 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).
11
+ - 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.
12
+ - 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`.
13
13
  <!-- riskrails:end -->
@@ -2,4 +2,5 @@
2
2
  .ai/state/*.json
3
3
  .ai/state/handoff.md
4
4
  .ai/state/*.tmp
5
+ .ai/state/*.jsonl
5
6
  .ai/local/
@@ -8,6 +8,6 @@ This repository runs an agentic pipeline under `.ai/`. Read `.ai/AGENTS.md` firs
8
8
  - Changes run through the pipeline (`/rr-build` in Claude Code or Codex); without it, follow `.ai/policies/safety.md`, touch only the files the current task names, and answer questions in prose or by filling the file.
9
9
 
10
10
  - 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.
11
- - 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.
12
- - 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).
11
+ - 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.
12
+ - 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`.
13
13
  <!-- riskrails:end -->