riskrails 3.0.1 → 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 (62) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/README.md +28 -15
  5. package/ai/policies/model-routing.md +7 -7
  6. package/ai/policies/production.md +3 -3
  7. package/ai/policies/release.md +2 -2
  8. package/ai/policies/review-economy.md +27 -30
  9. package/bin/riskrails.js +5 -5
  10. package/hooks/context-guard.py +1 -1
  11. package/hooks/guard/common.py +2 -0
  12. package/hooks/guard/paths.py +41 -30
  13. package/hooks/project-scaffold.py +1 -5
  14. package/hooks/runtime-gate.py +6 -6
  15. package/hooks/update_notice.py +20 -14
  16. package/instructions/routing.md +1 -1
  17. package/instructions/stub.md +3 -3
  18. package/package.json +1 -1
  19. package/skills/AGENTS.md +1 -1
  20. package/skills/CLAUDE.md +1 -1
  21. package/skills/ai-init/templates/.ai/AGENTS.md +1 -1
  22. package/skills/ai-init/templates/.ai/VERSION +1 -1
  23. package/skills/ai-init/templates/.ai/policies/security.md +7 -1
  24. package/skills/ai-init/templates/.ai/policies/testing.md +19 -6
  25. package/skills/ai-init/templates/AGENTS.block.md +2 -2
  26. package/skills/ai-init/templates/CLAUDE.block.md +2 -2
  27. package/skills/ai-init/templates/GEMINI.block.md +2 -2
  28. package/skills/ai-init/templates/gitignore.snippet +1 -0
  29. package/skills/ai-init/templates/junie-guidelines.block.md +2 -2
  30. package/skills/ai-task/sensors.py +24 -4
  31. package/skills/ai-task/state.py +37 -14
  32. package/skills/project-init/templates/constitution.md +1 -1
  33. package/skills/project-init/templates/sdlc-README.md +20 -14
  34. package/skills/project-update/history/blobs/00289f98434fed230eebe768dfb3425f83d737866a3aebc97aa95167c4fa0c6e +131 -0
  35. package/skills/project-update/history/blobs/079a91f976ad69b5cf49ccb9a28849576e4735ddd9485fc26599e528a69fd2f4 +30 -0
  36. package/skills/project-update/history/blobs/07ff7fa6f93d471ff533f553b4e523e4105ccde1f3dab70132c5237728f68422 +13 -0
  37. package/skills/project-update/history/blobs/0ef4a360c76da290173dea0f8073949cb0f8113710a38df95acae5d96bdd1286 +123 -0
  38. package/skills/project-update/history/blobs/18c7a2753ce9cb6f6d3d1e9d5d1125d75950b879d56b7325225b200869631b29 +15 -0
  39. package/skills/project-update/history/blobs/25d4f2a86deb5e2574bb3210b67bb24fcc4afb19f93a7b65a057daa874a9d18e +1 -0
  40. package/skills/project-update/history/blobs/48d9950f16e31d55475f566a80dc95f64b35f1c804d2f51729e313a4f29bac8d +13 -0
  41. package/skills/project-update/history/blobs/5a93f78b4ed4d416c171139b2ce610a117f9c1dd46fbb56ca86729cb4c53d10c +6 -0
  42. package/skills/project-update/history/blobs/6a3be0e624e5ad91e44d08857b7183ee53b7903e1e8d56ff196dbc9dde271e4a +75 -0
  43. package/skills/project-update/history/blobs/7170553410bba904ffb7edb4b231e3b3579b85542d84de717299980938f82c9d +13 -0
  44. package/skills/project-update/history/blobs/774b1e3a04ef0d49d33c44a2cfecd92eb6f6b738a12b6a681103ba7fc13e7b86 +67 -0
  45. package/skills/project-update/history/blobs/7f063e79195a5c85c37b7260dd20c184d3b9bea85e37202e0bbca73ad84bba49 +13 -0
  46. package/skills/project-update/history/blobs/c8573ca1f810ce6c4ac49456a4c1725b93e0d3b74c54ce95d46d8747777895f0 +13 -0
  47. package/skills/project-update/history/blobs/d6e5b34d475e7187d2a8cb2aac7e7f1a427142baf6461bda387d70ceb4d8a8c1 +30 -0
  48. package/skills/project-update/history/blobs/e120ff4e32b2e950fc94ada4e787d57dc9c790da37223e5a202d74f7e3aadf80 +13 -0
  49. package/skills/project-update/history/index.json +36 -15
  50. package/skills/project-update/migrations/0011_retire_risk_tiers.py +37 -0
  51. package/skills/project-update/migrations/__init__.py +6 -1
  52. package/skills/project-update/render_instructions.py +1 -1
  53. package/skills/project-update/update.py +374 -87
  54. package/skills/rr-build-all/SKILL.md +2 -2
  55. package/skills/rr-plan/SKILL.md +3 -1
  56. package/skills/rr-review/SKILL.md +13 -8
  57. package/skills/rr-setup/SKILL.md +15 -2
  58. package/skills/rr-setup/setup.py +7 -1
  59. package/skills/rr-spec/SKILL.md +4 -2
  60. package/skills/usage-report/usage-report.py +4 -2
  61. package/skills/ai-init/templates/.ai/policies/risk-tiers.json +0 -628
  62. package/skills/ai-init/templates/.ai/policies/risk-tiers.md +0 -180
@@ -5,7 +5,7 @@
5
5
  },
6
6
  "metadata": {
7
7
  "description": "A weight-based engineering pipeline for Claude Code and Codex: each change is weighed on six axes, runs light, guarded or full, edits only the files its plan names, and ends at human approval — enforced by hooks, reviewed adversarially, routed to the cheap model tier by default.",
8
- "version": "3.0.1"
8
+ "version": "3.1.0"
9
9
  },
10
10
  "plugins": [
11
11
  {
@@ -14,7 +14,7 @@
14
14
  "source": "url",
15
15
  "url": "https://github.com/vanssata/claude-agentic-sdlc-module.git"
16
16
  },
17
- "version": "3.0.1",
17
+ "version": "3.1.0",
18
18
  "strict": true
19
19
  }
20
20
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "riskrails",
3
- "version": "3.0.1",
3
+ "version": "3.1.0",
4
4
  "description": "A weight-based engineering pipeline for Claude Code and Codex: each change is weighed on six axes, runs light, guarded or full, edits only the files its plan names, and ends at human approval — enforced by hooks, reviewed adversarially, routed to the cheap model tier by default.",
5
5
  "author": {
6
6
  "name": "Ivan Kakurov"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "riskrails",
3
- "version": "3.0.1",
3
+ "version": "3.1.0",
4
4
  "description": "A weight-based engineering pipeline for Claude Code and Codex: each change is weighed on six axes, runs light, guarded or full, edits only the files its plan names, and ends at human approval — enforced by hooks, reviewed adversarially, routed to the cheap model tier by default.",
5
5
  "author": {
6
6
  "name": "Ivan Kakurov"
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # RiskRails
2
2
 
3
- [![Version](https://img.shields.io/badge/version-3.0.1-d2f878?labelColor=101210)](https://github.com/vanssata/claude-agentic-sdlc-module/releases/tag/v3.0.1)
3
+ [![Version](https://img.shields.io/badge/version-3.1.0-d2f878?labelColor=101210)](https://github.com/vanssata/claude-agentic-sdlc-module/releases/tag/v3.1.0)
4
4
  [![Runtimes](https://img.shields.io/badge/runtimes-Claude%20Code%20%7C%20Codex-d2f878?labelColor=101210)](docs/routing.md)
5
5
  [![Known risks](https://img.shields.io/badge/known%20risks-documented-d2f878?labelColor=101210)](docs/known-risks.md)
6
6
  [![License: MIT](https://img.shields.io/badge/license-MIT-d2f878?labelColor=101210)](LICENSE)
@@ -18,7 +18,7 @@ approval. Risky actions are stopped by a hook, not by a polite request in a
18
18
  prompt.
19
19
 
20
20
  ```bash
21
- npx riskrails@3.0.1 # detects Claude Code, Codex or both (Node 18+, python3)
21
+ npx riskrails@3.1.0 # detects Claude Code, Codex or both (Node 18+, Python 3.11+)
22
22
  ```
23
23
 
24
24
  Then, in any project: `/rr-setup` once, and `/rr-build <what you want>` for
@@ -50,9 +50,21 @@ Tuned for **one developer who knows the codebase**, on a Claude Pro, Max or
50
50
  Team plan, or a ChatGPT Plus or Pro plan, following the
51
51
  [AI-native SDLC playbook](https://claude.com/blog/the-ai-native-sdlc-playbook).
52
52
 
53
- **Version 3.0.1.** The guards have limits: read [Known risks](docs/known-risks.md)
53
+ **Version 3.1.0.** The guards have limits: read [Known risks](docs/known-risks.md)
54
54
  before relying on them. What is decided but not built is in [ROADMAP.md](ROADMAP.md).
55
55
 
56
+ ## Upgrade to 3.1
57
+
58
+ From 3.0.x: update the home (`npx riskrails@3.1.0`, restart the runtime), then
59
+ in each project `/rr-setup --update` (dry run first, then `--apply`). Schema 10
60
+ becomes **11**: migration `0011` proposes deleting `.ai/policies/risk-tiers.json`
61
+ and `risk-tiers.md`, which `weight.json` and `weight.md` replaced; confirm with
62
+ `--apply --confirm-delete <name>` in a terminal of your own. The update now
63
+ renders `weight.md` from `weight.json` instead of merging it, reads the
64
+ `docs/sdlc/*/TEMPLATE.md` files from the plugin (an unedited project copy is
65
+ offered for deletion), and proposes cleanup of old run logs and stale tasks.
66
+ See [CHANGELOG.md](CHANGELOG.md).
67
+
56
68
  ## Upgrade to 3.0
57
69
 
58
70
  3.0 renames the plugin from `claude-agentic` to `riskrails`, replaces the risk
@@ -70,7 +82,7 @@ version. Step by step, in this order:
70
82
  3. **Update the home** on every machine, for every runtime:
71
83
 
72
84
  ```bash
73
- npx riskrails@3.0.1 # or: npx -y riskrails@latest --yes
85
+ npx riskrails@3.1.0 # or: npx -y riskrails@latest --yes
74
86
  ```
75
87
 
76
88
  It moves `<home>/claude-agentic/` to `<home>/riskrails/` and leaves a
@@ -85,22 +97,22 @@ version. Step by step, in this order:
85
97
  /rr-setup --update # dry run first; read it; then --apply
86
98
  ```
87
99
 
88
- Four migrations run inside the dry run, in order, before the merge:
100
+ Migrations `0007` to `0010` (and `0011`, see Upgrade to 3.1) run inside the dry run, in order, before the merge:
89
101
  `0007` creates `.ai/policies/weight.json`, carrying over the project's
90
102
  edits to its risk-tier table; `0008` renames the managed-block markers and
91
103
  the router's routes; `0009` moves `current.json` to `state.json`; `0010`
92
104
  creates `.ai/policies/dialog.json` with the dialog on. Edited markdown is
93
105
  three-way merged, JSON policies key by key with a confirmation per key;
94
- `delete?` proposals are left for you to confirm. The schema becomes 10.
106
+ `delete?` proposals are left for you to confirm. The schema becomes 10 with a 3.0 plugin, 11 with 3.1.
95
107
  5. **Decide two things the migrations cannot.** Keep the dialog (then amend
96
108
  constitution C5 as `0010` hints) or set `"enabled": false`; and tune the
97
109
  weight table — `domain.modules`, `customer_visible`, `hot_paths`,
98
110
  `needs_step`, `irreversible` — then `state.py weight --mirror`.
99
- 6. **Verify**: `/rr-status` shows schema 10 and no task in flight; a
111
+ 6. **Verify**: `/rr-status` shows schema 11 and no task in flight; a
100
112
  `/rr-build` on a small change runs light and closes on the two facts.
101
113
  Commit `.ai/`.
102
114
 
103
- Every machine of a team upgrades the plugin before a project at schema 10 is
115
+ Every machine of a team upgrades the plugin before a project at schema 11 is
104
116
  committed; an older plugin refuses such a project. Rollback and the full
105
117
  migration table are in [docs/project-update.md](docs/project-update.md); the
106
118
  home-side details in [docs/installation.md](docs/installation.md).
@@ -108,12 +120,12 @@ home-side details in [docs/installation.md](docs/installation.md).
108
120
  ## Install
109
121
 
110
122
  ```bash
111
- npx riskrails@3.0.1 # detects Claude Code, Codex, or both; proposes the detected plan
112
- npx riskrails@3.0.1 --target codex # auto | claude | codex | both
113
- npx riskrails@3.0.1 --plan max --fable no # pro | team-pro | team-max | max | max20 | balanced-max
114
- npx riskrails@3.0.1 --codex-plan plus # plus | pro | balanced-max
115
- npx riskrails@3.0.1 --context economy # the plan's compaction window; the context guard also blocks
116
- npx riskrails@3.0.1 --dry-run # print what would be written, write nothing
123
+ npx riskrails@3.1.0 # detects Claude Code, Codex, or both; proposes the detected plan
124
+ npx riskrails@3.1.0 --target codex # auto | claude | codex | both
125
+ npx riskrails@3.1.0 --plan max --fable no # pro | team-pro | team-max | max | max20 | balanced-max
126
+ npx riskrails@3.1.0 --codex-plan plus # plus | pro | balanced-max
127
+ npx riskrails@3.1.0 --context economy # the plan's compaction window; the context guard also blocks
128
+ npx riskrails@3.1.0 --dry-run # print what would be written, write nothing
117
129
  npx -y riskrails@latest --yes # update in place, keeping the recorded plan
118
130
  ```
119
131
 
@@ -263,7 +275,7 @@ python3 -I -m unittest discover -s tests -p 'test_*.py' # CI's serial oracl
263
275
  AI_E2E=1 python3 -I -m unittest discover -s tests -p test_end_to_end.py
264
276
  ```
265
277
 
266
- Fifty-six suites, 979 tests, about 75 seconds in parallel. The guards against JSON fixtures for both runtimes and a
278
+ Fifty-nine suites, 1045 tests, about 75 seconds in parallel. The guards against JSON fixtures for both runtimes and a
267
279
  golden of 1 341 recorded decisions, byte for byte, with timed cases that hold
268
280
  the hot path under its budget; the state machine, the weight, the batch loop,
269
281
  the dialog contract and the documents rule; the compact record and its
@@ -293,6 +305,7 @@ the developer's real `~/.claude` or `~/.codex`.
293
305
  | [docs/local-memory.md](docs/local-memory.md) | the compact record: task ids, packages, recall, the pre-push check, migration |
294
306
  | [docs/known-risks.md](docs/known-risks.md) | what the guards do not protect against, and what to do about it |
295
307
  | [docs/faq.md](docs/faq.md) | why something was blocked, and how to change it |
308
+ | [CHANGELOG.md](CHANGELOG.md) | what changed in each release, and the upgrade notes |
296
309
  | [docs/sdlc/README.md](docs/sdlc/README.md) | the spec → plan → build artefacts in a project |
297
310
  | [ROADMAP.md](ROADMAP.md) | what is decided but not built; the open rows |
298
311
 
@@ -19,16 +19,16 @@ not settle — never for fact collection.
19
19
 
20
20
  Which model a tier is depends on the plan and the runtime: the installed profile
21
21
  decides it, `state.py profile --tier <TIER>` prints it for the runtime you are in,
22
- and `/ai-status` shows all four. This file names tiers only, so it is true on
22
+ and `/rr-status` shows the plan's budget. This file names tiers only, so it is true on
23
23
  every plan.
24
24
 
25
25
  The main session runs the model the installed profile sets and does the
26
26
  implementation itself. Planning, the weight, the test run and the release are
27
27
  its work too, not an agent's: `/rr-plan`, `state.py weight`, `state.py verify`,
28
28
  `/rr-ship`. Which stages spawn an agent at all
29
- is decided by `pipeline_profile` in `risk-tiers.json`: in `solo`, T0–T4 run in
30
- direct mode — nothing on STRONG below T4, a BALANCED review at T2/T3 only when
31
- a sensor is red, one STRONG review at T4 — and tests run once
29
+ is decided by the mode `state.py weight` gives: light runs direct — no review
30
+ unless `close` reports a red fact — guarded has one BALANCED review, full one
31
+ STRONG review, and nothing runs on STRONG below full — and tests run once
32
32
  after the last step, to the end, with every failure fixed as one batch.
33
33
 
34
34
  **Reading by difficulty.** A lookup — where X is, one route, service id, config
@@ -65,12 +65,12 @@ depends on a local model; if one becomes available it slots in at the FAST tier.
65
65
 
66
66
  ## EXPERT triggers
67
67
 
68
- 1. The task is T6: migration, infrastructure, production architecture.
68
+ 1. The task is full and irreversible: migration, infrastructure, production architecture.
69
69
  2. STRONG could not settle it: `confidence: uncertain`, or two STRONG results
70
70
  contradict each other.
71
71
  3. An irreversible design with several viable options — core data model, public
72
72
  API or event contract, service split.
73
- 4. The final check of a T6 plan or of a production-incident root cause before a
73
+ 4. The final check of a full-and-irreversible plan or of a production-incident root cause before a
74
74
  human acts on it.
75
75
 
76
76
  ## Escalation
@@ -104,7 +104,7 @@ the model `state.py profile --tier STRONG` printed — because a tier alone does
104
104
  cost or how much weight to give it. Report the model that **ran**, not the one
105
105
  that was requested: a gate may have sent an EXPERT agent down one tier during a
106
106
  rate limit, and a Codex agent file may have overridden the spawn-time model. Both
107
- runtimes expose the current state through `/ai-status`.
107
+ runtimes expose the current state through `/rr-status`.
108
108
 
109
109
  ## What never gets delegated
110
110
 
@@ -7,8 +7,8 @@ is reached by a real customer until you have evidence it is not.
7
7
 
8
8
  1. Find the callers. `grep`/`rg` for the method, the service id, the route, the
9
9
  event name, the template.
10
- 2. Find the tests that pin the current behaviour. If none exist and the tier is
11
- T3 or above, write a characterization test **first** — one that passes against
10
+ 2. Find the tests that pin the current behaviour. If none exist and the mode is
11
+ guarded or full, write a characterization test **first** — one that passes against
12
12
  the code as it is today.
13
13
  3. Check the history for why it looks like this: `git log -L`, `git log -S`,
14
14
  `git blame`. A workaround with a ticket number in the message is a constraint,
@@ -37,6 +37,6 @@ named in the release report. "It worked locally" is not observability.
37
37
 
38
38
  ## Rollback
39
39
 
40
- Every T3+ change states how to undo it. If undoing it is impossible — a data
40
+ Every guarded or full change states how to undo it. If undoing it is impossible — a data
41
41
  migration, an external system call, a fiscal record — say so plainly in the
42
42
  release report, because that changes who has to approve it.
@@ -29,7 +29,7 @@ It exists so that the human approving the change can see, in one page, what
29
29
  changed, what deliberately did not, what was tested, what was reviewed, what
30
30
  could go wrong, and how to undo it.
31
31
 
32
- A report that says "no known risks" for a T4 change is a report that was not
32
+ A report that says "no known risks" for a guarded or full change is a report that was not
33
33
  written carefully.
34
34
 
35
35
  ## What blocks a release
@@ -38,7 +38,7 @@ written carefully.
38
38
  `focus: security` pass;
39
39
  - a new regression in the last `state.py verify` run;
40
40
  - a destructive database operation without its own named approval;
41
- - a T3+ change with no rollback section;
41
+ - a guarded or full change with no rollback section;
42
42
  - an unanswered blocking question in the plan.
43
43
 
44
44
  ## Project specifics
@@ -72,8 +72,8 @@ The ledger is what replaces agent memory. Without it, every pass is pass one.
72
72
  ## 3. Findings are fixed as one batch, and re-review is scoped, not repeated
73
73
 
74
74
  A review returns all its findings at once, and they are fixed at once: one
75
- `state.py remediate` step whose scope is the finished steps plus what the
76
- findings name, every BLOCKER and HIGH addressed in it, then the verification
75
+ fix batch (`state.py start --amend` widens the task to the finished steps plus
76
+ what the findings name), every BLOCKER and HIGH addressed in it, then the verification
77
77
  command once. Never one fix, one run, one re-review per finding — that is the
78
78
  serial loop the measurements below describe, and the remediation of one finding
79
79
  is where the next regression comes from.
@@ -98,13 +98,12 @@ shipped), each after a fix batch — before the same stop: the pass past that
98
98
  number is the human's. The reviewer's verdict says which closing rule applies:
99
99
  `blockers_open` (a BLOCKER) takes the remediation and the scoped re-review;
100
100
  `pass_with_findings` (a HIGH, no BLOCKER) takes the remediation and a green
101
- verification run, no re-review; `pass` closes. `state.py` has no value of its
102
- own for `pass_with_findings` yet: it is recorded as `blockers_open`, stays so
103
- after the green run, and the close summary names the labels that run closed.
101
+ verification run, no re-review; `pass` closes. `state.py review --verdict`
102
+ takes all three values.
104
103
 
105
- ## 3a. The boundary of a direct-mode review
104
+ ## 3a. The boundary of a light or guarded review
106
105
 
107
- Below T5 the review is bounded. Its inputs are the task goal, the diff, the
106
+ In light and guarded the review is bounded. Its inputs are the task goal, the diff, the
108
107
  tests the step names, and the project's own fixture corpus and threat model —
109
108
  `.ai/policies/safety.md`, the guards' `*-defaults.json`, the test fixtures.
110
109
  The reviewer works its whole list inside that corpus; what it may not do is
@@ -117,8 +116,8 @@ it only towards `yes`.
117
116
 
118
117
  A guard whose input is any shell line is a bottomless well for an unbounded
119
118
  reviewer: every pass finds a new input nobody will type, and every fix for one
120
- is the next pass's target. The boundary is what lets a review end. At T5 and
121
- T6 there is none — the reviewer's list applies unbounded, as the cost of being
119
+ is the next pass's target. The boundary is what lets a review end. In full
120
+ mode there is none — the reviewer's list applies unbounded, as the cost of being
122
121
  wrong there warrants.
123
122
 
124
123
  ## 4. Split one pass into parallel dimensions, not serial rounds
@@ -174,37 +173,35 @@ script, or run it yourself:
174
173
  A review pass spent on a self-contradictory risk register is a pass not spent on
175
174
  the code. When a task closes a gap that a register entry calls open, the entry is
176
175
  corrected **in the same step as the fix**, not later. When the plan gains a
177
- remediation, the plan gains a section — at T4 and above the artefact a human
176
+ remediation, the plan gains a section — in guarded and full the artefact a human
178
177
  approves must describe the change that exists, and a diff wider than the plan is
179
178
  invisible to the path guard, because the file set never changed.
180
179
 
181
180
  ## 8. The one thing the sensors may replace
182
181
 
183
- At T3 and below, when every sensor in `risk-tiers.json` `required_for_skip` is
184
- green or explicitly not applicable **on the tree as it is now**, the model
185
- review is replaced by the sensor set and `state.py review-gate` records
186
- `review_status: skipped_green`. This is the intent's decision 6, and it is an
187
- exception to the rule below, written here so the two do not contradict each
188
- other.
182
+ In light mode, when the two facts `close` measures are green **on the tree as
183
+ it is now** — a green whole-suite `verify` and every changed file inside the
184
+ task's files — the model review is replaced by the sensor set and the task
185
+ closes without one. This is the intent's decision 6, and it is an exception to
186
+ the rule below, written here so the two do not contradict each other.
189
187
 
190
188
  It is narrow on purpose. The skip requires that the suite passed on this exact
191
- tree, that the tests the plan named **fail without the change** (`bite` — a
192
- green suite that never reaches the diff is not evidence), that the diff is
193
- inside its budget — the budget is advisory for the tier, never for the skip —
194
- and that the tier re-scored from the real diff is still T3 or lower. A missing
195
- linter is not green: `unavailable` keeps the review, and only a human writing
196
- `lint_command: none` says the project has no linter. No
197
- sensor writes `skipped_green` by asserting it — `review-gate` reaches it from
198
- measurements, and `state.py set review_status skipped_green` is refused.
199
-
200
- From T4 the review always runs. What the sensors change there is where it
201
- starts: their rows are in the ledger, CONFIRMED, before the reviewer plans its
202
- work.
189
+ tree, and that the tests the plan named **fail without the change** (`bite` — a
190
+ green suite that never reaches the diff is not evidence). The mode is the one
191
+ re-scored from the real diff: a diff that raises it to guarded or full keeps the
192
+ review. A missing linter is not green: `unavailable` keeps the review, and only
193
+ a human writing `lint_command: none` says the project has no linter. No
194
+ sensor skips the review by asserting it — `close` reaches it from
195
+ measurements.
196
+
197
+ In guarded and full the review always runs. What the sensors change there is
198
+ where it starts: their rows are in the ledger, CONFIRMED, before the reviewer
199
+ plans its work.
203
200
 
204
201
  ## What this policy will not trade away
205
202
 
206
- - No review stage the tier requires is skipped, shortened, or merged — except
207
- the T2/T3 model review, and only under §8's measured conditions.
203
+ - No review stage the mode requires is skipped, shortened, or merged — except
204
+ the light-mode model review, and only under §8's measured conditions.
208
205
  - No reviewer is moved to a cheaper model to go faster. Narrow the surface, never
209
206
  the mind.
210
207
  - A blocker still returns to implementation, however late it arrives. Past the
package/bin/riskrails.js CHANGED
@@ -1205,9 +1205,9 @@ function claudeSummary() {
1205
1205
  'Restart Claude Code, then:',
1206
1206
  ' /config model and effort match the profile',
1207
1207
  ` /hooks lists cap-large-read, ai-guard, project-scaffold and context-guard, plus runtime-gate on PreToolUse, PostToolUse, SubagentStart, SubagentStop${C.GATE === 'on' ? ' and StopFailure' : ''}`,
1208
- ' /skills lists ai-init, ai-audit, ai-task, ai-status, project-init, sdlc-*, usage-report',
1209
- ' /ai-init in a project, to survey it and build .ai/',
1210
- ' /project-update in a project that already has .ai/ or docs/sdlc/, to pull in these rules',
1208
+ ' /skills lists rr-setup, rr-spec, rr-plan, rr-build, rr-build-all, rr-review, rr-ship, rr-status',
1209
+ ' /rr-setup in a project, to survey it and build .ai/',
1210
+ ' /rr-setup --update in a project that already has .ai/ or docs/sdlc/, to pull in these rules',
1211
1211
  ];
1212
1212
  out(lines.join('\n'));
1213
1213
  }
@@ -1411,8 +1411,8 @@ function codexSummary() {
1411
1411
  '',
1412
1412
  'Restart Codex, then:',
1413
1413
  ' /hooks review and trust the guard, gate and context hooks — until you do, Codex skips them',
1414
- ' /skills lists ai-init, ai-audit, ai-task, ai-status, project-init, sdlc-*, usage-report',
1415
- ' /ai-init in a project, to survey it and build .ai/',
1414
+ ' /skills lists rr-setup, rr-spec, rr-plan, rr-build, rr-build-all, rr-review, rr-ship, rr-status',
1415
+ ' /rr-setup in a project, to survey it and build .ai/',
1416
1416
  '',
1417
1417
  `Your previous default model was replaced. backups/${BACKUP_DATE}/config.toml holds the old one.`,
1418
1418
  ].join('\n'));
@@ -552,7 +552,7 @@ def task_frame(root, reason):
552
552
  tail.append(f"## Pending questions ({count}) — answer with `state.py answer …` "
553
553
  "or fill the file")
554
554
  tail.append(prose or ids)
555
- tail.append("Resume with: /ai-task --resume")
555
+ tail.append("Resume with: /rr-build --resume")
556
556
  if len("\n".join([head] + tail)) > HANDOFF_INJECT_MAX_CHARS and len(tail) > 1:
557
557
  tail[1] = ids # the questions collapse to their ids
558
558
  return head[:HANDOFF_INJECT_MAX_CHARS], "\n".join(tail)
@@ -165,6 +165,8 @@ _WS = " \t\n\r"
165
165
  def parse_stream(text):
166
166
  """(values, ok): every JSON value of a jq input stream, and False when a
167
167
  parse error ended it (the values before the error are kept)."""
168
+ if text.startswith("\ufeff"): # jq skips one leading UTF-8 BOM
169
+ text = text[1:]
168
170
  values, i, n = [], 0, len(text)
169
171
  while True:
170
172
  while i < n and text[i] in _WS:
@@ -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,22 +81,36 @@ 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)
91
- # R36.11, with the dialog on only: the other human-only state.py commands —
92
- # `profile set` and the --by forms of verify, review and reject — run from an
93
- # agent session only as the next live pick, like approve. `start --mode --by`
94
- # reaches state.py: raising a mode needs no human, and a lowering opens its own
95
- # decision there before human_route() refuses it (review H1, T-2026-10-28-010).
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).
96
91
  PROFILE_SET_VERB = "([[:space:]]+--)?[[:space:]]+" + Q + "profile" + Q + "[[:space:]]+" + Q + "set" + END
97
92
  PROFILE_SET_RE = (BOUND + "(python3?[[:space:]]+)?" + Q + "[^[:space:]'\"]*state\\.py" + Q + ARGS + PROFILE_SET_VERB
98
93
  + "|" + BOUND + "(python3?[[:space:]]+)?" + Q + VAR + Q + ARGS + PROFILE_SET_VERB)
99
- HUMAN_BY_VERB = ("([[:space:]]+--)?[[:space:]]+" + Q + "(verify|review|reject)" + END + "([^|;&`]*[[:space:]])?"
100
- + Q + "--by?" + Q + "(=|[[:space:]]|$)")
101
- HUMAN_BY_RE = (BOUND + "(python3?[[:space:]]+)?" + Q + "[^[:space:]'\"]*state\\.py" + Q + ARGS + HUMAN_BY_VERB + "|"
102
- + BOUND + "(python3?[[:space:]]+)?" + Q + VAR + Q + ARGS + HUMAN_BY_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.")
103
114
  WHY_PICK = T(
104
115
  "\n\nWith the dialog on, this command runs from an agent session only as the human's pick.\n"
105
116
  "The command that needs it opened a decision: 'state.py decision show' prints the dialog;\n"
@@ -137,7 +148,7 @@ LN_SRC_RE = ".*(^|[|;&[:space:]])ln([[:space:]]+-[^[:space:]]+)*[[:space:]]+([^[
137
148
  LONG_CMD = 4096
138
149
  # Whole words only, so --cov, --config and approve_invoice do not trip it.
139
150
  LONG_TRIGGER_RE = ("(^|[^A-Za-z0-9_])approve([^A-Za-z0-9_]|$)|--by(=|[[:space:]]|$)|"
140
- "--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")
141
152
  # Tools that write a file named on their command line without a redirect; the
142
153
  # movers do not parse them, so any protected path they name is refused (H1).
143
154
  FETCHERS = "(^|[|;&[:space:]])(scp|tar|zip|unzip|curl|wget)([[:space:]]+[^;&|[:space:]]+)*[[:space:]]+"
@@ -1255,14 +1266,14 @@ class PathGuard: # pylint: disable=too-many-instance-
1255
1266
  on, picked = self.dialog_consent(cmd)
1256
1267
  if m.ere_match(APPROVE_RE, cmd_joined) and not unattended and self.task_in_flight() and not picked:
1257
1268
  raise Deny("Refusing 'state.py approve' from an agent session." + WHY_APPROVE)
1258
- if on and not picked and not unattended:
1259
- if m.ere_match(PROFILE_SET_RE, cmd_joined):
1260
- raise Deny("Refusing 'state.py profile set' from an agent session." + WHY_PICK)
1261
- if m.ere_match(HUMAN_BY_RE, cmd_joined):
1262
- raise Deny(T("Refusing a 'state.py … --by' verdict from an agent session.") + WHY_PICK)
1263
- if m.ere_match(SETBY_RE, cmd_joined) and not unattended and self.task_in_flight():
1264
- raise Deny(T("Refusing 'state.py set \u2026 --by' from an agent session: a verdict with --by is the "
1265
- "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)
1266
1277
  self.interpreter_text(cmd)
1267
1278
 
1268
1279
  # Top-level splitting only: the string a sh -c or an eval runs is not
@@ -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
@@ -415,10 +415,10 @@ def claude_pre_tool_use(payload):
415
415
  "hookSpecificOutput": {
416
416
  "hookEventName": "PreToolUse",
417
417
  "permissionDecision": "allow",
418
- "permissionDecisionReason": f"fable-gate: {why}; {name} runs on {FALLBACK}",
418
+ "permissionDecisionReason": f"runtime-gate: {why}; {name} runs on {FALLBACK}",
419
419
  "updatedInput": {**tool_input, "model": FALLBACK},
420
420
  "additionalContext": (
421
- f"fable-gate routed this {name} agent to {FALLBACK}: {why}. "
421
+ f"runtime-gate routed this {name} agent to {FALLBACK}: {why}. "
422
422
  f"Its answer is {FALLBACK}'s, not Fable's. "
423
423
  "`~/.claude/hooks/runtime-gate.py clear` re-enables Fable early."
424
424
  ),
@@ -801,10 +801,10 @@ def codex_pre_tool_use(payload):
801
801
  journal_fallback(payload, label, model, to, rec)
802
802
  out["hookSpecificOutput"]["permissionDecision"] = "allow"
803
803
  out["hookSpecificOutput"]["updatedInput"] = updated
804
- 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 "
805
805
  f"{to}'s, not {EXPERT}'s. Say so when you report it. ")
806
806
  else:
807
- 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} "
808
808
  "and may fail the same way. Say so when you report it. ")
809
809
  out["hookSpecificOutput"]["additionalContext"] = context + "`runtime-gate.py clear` re-enables the expert model early."
810
810
  respond(out)
@@ -28,6 +28,7 @@ import urllib.request
28
28
  REGISTRY_URL = "https://registry.npmjs.org/riskrails/latest"
29
29
  MAX_AGE = 24 * 3600
30
30
  TIMEOUT = 3
31
+ MAX_BODY = 65536
31
32
  COMMAND = "npx -y riskrails@latest --yes"
32
33
  VERSION_RE = re.compile(r"^(\d+)\.(\d+)\.(\d+)$") # no prerelease, no build metadata
33
34
 
@@ -103,20 +104,7 @@ def notice(runtime, home, plugin_root, now):
103
104
  return message, context
104
105
 
105
106
 
106
- def refresh(path):
107
- """Ask the registry and write the cache; on failure only move checked_at."""
108
- latest = None
109
- try:
110
- url = os.environ.get("RISKRAILS_REGISTRY_URL") or REGISTRY_URL
111
- with urllib.request.urlopen(url, timeout=TIMEOUT) as resp: # nosec: the registry, or a test's file URL
112
- version = json.loads(resp.read().decode("utf-8")).get("version")
113
- if parse(version):
114
- latest = version
115
- except (OSError, ValueError, AttributeError):
116
- pass
117
- if latest is None:
118
- previous = read_cache(path).get("latest")
119
- latest = previous if isinstance(previous, str) else None
107
+ def write_cache(path, latest):
120
108
  tmp = f"{path}.{os.getpid()}.tmp"
121
109
  try:
122
110
  with open(tmp, "w", encoding="utf-8") as fh:
@@ -129,6 +117,24 @@ def refresh(path):
129
117
  pass
130
118
 
131
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
+
132
138
  if __name__ == "__main__":
133
139
  if len(sys.argv) == 3 and sys.argv[1] == "--refresh" and not disabled():
134
140
  refresh(sys.argv[2])
@@ -46,7 +46,7 @@ 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 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.
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
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 -->