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
@@ -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 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. 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 merges or deploys; commit, push, approval only as the dialog picks, 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` (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.1",
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/skills/AGENTS.md CHANGED
@@ -2,5 +2,5 @@
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.
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
6
  <!-- riskrails:rule:skills:end -->
package/skills/CLAUDE.md CHANGED
@@ -2,5 +2,5 @@
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.
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
6
  <!-- riskrails:rule:skills:end -->
@@ -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
- 10
1
+ 11
@@ -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
@@ -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 merges or deploys; commit, push, approval only as the dialog picks, 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` (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 merges or deploys; commit, push, approval only as the dialog picks, 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` (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 merges or deploys; commit, push, approval only as the dialog picks, 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` (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 merges or deploys; commit, push, approval only as the dialog picks, 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` (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 -->
@@ -135,8 +135,10 @@ def snapshot_tree(root):
135
135
  def numstat(root, from_tree, to_tree):
136
136
  """[{path, added, deleted, binary}] between two trees, NUL-separated so a
137
137
  path with a space, a quote or a newline survives. Renames are off: a rename
138
- is an add plus a delete, as diff_files reports it."""
139
- code, out = git(root, ["diff", "--numstat", "--no-renames", "-z",
138
+ is an add plus a delete, as diff_files reports it. --relative (git runs in
139
+ root): paths are relative to the project root, as task files and policy globs
140
+ are, and a change outside root, below the git top level, is left out."""
141
+ code, out = git(root, ["diff", "--relative", "--numstat", "--no-renames", "-z",
140
142
  from_tree, to_tree])
141
143
  if code != 0:
142
144
  return None
@@ -170,7 +172,8 @@ def diff_files(root, from_tree, to_tree):
170
172
  None when either tree is missing: an unmeasured diff is not an empty one."""
171
173
  if not from_tree or not to_tree:
172
174
  return None
173
- code, out = git(root, ["diff", "--name-status", "--no-renames", "-z", from_tree, to_tree])
175
+ code, out = git(root, ["diff", "--relative", "--name-status", "--no-renames", "-z",
176
+ from_tree, to_tree])
174
177
  if code != 0:
175
178
  return None
176
179
  fields = [f for f in out.split("\0") if f != ""]
@@ -202,7 +205,8 @@ def scope_any(path, patterns):
202
205
  # ------------------------------------------------------------------- running
203
206
 
204
207
  TESTING_FIELDS = ("verify_command", "step_test_command", "e2e_command",
205
- "lint_command", "typecheck_command", "single_test", "doc_tests")
208
+ "lint_command", "typecheck_command", "single_test", "doc_tests",
209
+ "bite_codes")
206
210
 
207
211
  SCOPE_FIELD = {"suite": "verify_command", "step": "step_test_command",
208
212
  "e2e": "e2e_command", "single": "single_test"}
@@ -232,10 +236,26 @@ def read_commands(root):
232
236
  value = match.group(1).strip()
233
237
  if value.startswith("#"):
234
238
  value = ""
239
+ # A trailing ` # comment` is not part of `none` or of the code list; a
240
+ # command value is left exactly as written (a `#` can be part of it).
241
+ bare = re.sub(r"\s+#.*$", "", value)
242
+ if field == "bite_codes" or bare.lower() == "none":
243
+ value = bare
235
244
  commands[field] = value
236
245
  return commands
237
246
 
238
247
 
248
+ def bite_codes(root):
249
+ """The exit codes that count as "the test fails" under --expect fail: the
250
+ testing.md `bite_codes` list of integers, 1 when absent or not a clean list."""
251
+ raw = read_commands(root).get("bite_codes", "")
252
+ try:
253
+ codes = tuple(int(part) for part in raw.split(","))
254
+ except ValueError:
255
+ return (1,)
256
+ return codes or (1,)
257
+
258
+
239
259
  def command_for(root, scope):
240
260
  return (read_commands(root).get(SCOPE_FIELD.get(scope, ""), "") or "").strip()
241
261
 
@@ -194,7 +194,7 @@ def find_root(start):
194
194
  return d
195
195
  parent = os.path.dirname(d)
196
196
  if parent == d:
197
- die("no .ai/ directory found above %s — run /ai-init first" % start)
197
+ die("no .ai/ directory found above %s — run /rr-setup first" % start)
198
198
  d = parent
199
199
 
200
200
 
@@ -405,7 +405,7 @@ def claim_runtime(root, state):
405
405
  "state.py --runtime %s handoff --to %s (this shell is neither runtime; "
406
406
  "name the one taking it)" % ((other_runtime(pending),) * 2))
407
407
  die("RUNTIME_HANDOFF_PENDING: task %s was handed to %s at %s. Resume it there "
408
- "(/ai-task), or take it back here with: %s"
408
+ "(/rr-build), or take it back here with: %s"
409
409
  % (state.get("task_id"), pending, state["handoff"].get("pending_since"), back), 7)
410
410
  # The receiving runtime's first change: the handoff is complete. The
411
411
  # move itself was journaled when it was made, so there is no second event.
@@ -1120,7 +1120,8 @@ def cmd_event(args, root):
1120
1120
  die("--data must be a JSON object")
1121
1121
  state = load(root)
1122
1122
  append_journal(root, state["task_id"], journal_line(
1123
- state["task_id"], args.type, "hook", state.get("current_stage"), args.detail, data))
1123
+ state["task_id"], args.type, "agent" if args.type == "note" else "hook",
1124
+ state.get("current_stage"), args.detail, data))
1124
1125
  print("%s recorded" % args.type)
1125
1126
 
1126
1127
 
@@ -1134,7 +1135,7 @@ def find_docs_root(start):
1134
1135
  return d
1135
1136
  parent = os.path.dirname(d)
1136
1137
  if parent == d:
1137
- die("no docs/sdlc/ directory found above %s — run /sdlc-intent first" % start)
1138
+ die("no docs/sdlc/ directory found above %s — run /rr-spec first" % start)
1138
1139
  d = parent
1139
1140
 
1140
1141
 
@@ -1863,7 +1864,7 @@ def handoff_lines(root, state, reason):
1863
1864
  pending_to = (state.get("handoff") or {}).get("pending_to")
1864
1865
  if pending_to:
1865
1866
  review = state.get("cross_vendor_review") or {}
1866
- out.append("Handed to %s at %s%s — resume there with /ai-task" % (
1867
+ out.append("Handed to %s at %s%s — resume there with /rr-build" % (
1867
1868
  pending_to, (state.get("handoff") or {}).get("pending_since"),
1868
1869
  " for the cross-vendor review" if review.get("status") == "requested" else ""))
1869
1870
  advice = advisory_line(state)
@@ -1967,7 +1968,7 @@ def handoff_to(args, root):
1967
1968
  emit(root, state, "runtime_handoff", "%s -> %s (%s)" % (source, args.to, via), data)
1968
1969
  write_handoff(root, state, "manual")
1969
1970
  save(root, state)
1970
- print("Handed %s to %s (%s). In %s, run /ai-task: it resumes from the state file in .ai/state/."
1971
+ print("Handed %s to %s (%s). In %s, run /rr-build: it resumes from the state file in .ai/state/."
1971
1972
  % (state.get("task_id"), args.to, via, args.to))
1972
1973
  print("headless (not run): cd %s && %s" % (shlex.quote(root), RESUME_COMMAND[args.to]))
1973
1974
 
@@ -3251,7 +3252,10 @@ def rescore_weight(root, base_tree, files, current, policy):
3251
3252
  if line is not None:
3252
3253
  # The operation's keyword (DROP, ALTER …), not the pattern's
3253
3254
  # source: it is what a human names in approve --confirm.
3254
- operations.append(rx.search(line).group(0).split()[0].upper())
3255
+ # The first word of the match; a match of only blanks or punctuation
3256
+ # is named IRREVERSIBLE, never an IndexError.
3257
+ word = re.search(r"[A-Za-z]+", rx.search(line).group(0))
3258
+ operations.append(word.group(0).upper() if word else "IRREVERSIBLE")
3255
3259
  if found.get("reversibility", (0,))[0] < 2:
3256
3260
  found["reversibility"] = (2, "%s adds %r (irreversible_content %r)"
3257
3261
  % (rel, line.strip()[:60], pattern))
@@ -3483,7 +3487,8 @@ def cmd_start(args, root):
3483
3487
  check_steps(steps, implied)
3484
3488
  concrete = [f for f in files if not any(c in f for c in "*?[")]
3485
3489
  try:
3486
- result = weigh(root, concrete, parse_declares(args.declare), args.goal, policy)
3490
+ result = weigh(root, concrete, parse_declares(args.declare), args.goal, policy,
3491
+ [f for f in files if f not in concrete])
3487
3492
  except ValueError as exc:
3488
3493
  die(str(exc))
3489
3494
  except (TypeError, AttributeError) as exc:
@@ -3674,18 +3679,21 @@ def is_document(path, policy):
3674
3679
  DOCUMENTS_ZEROED = ("behaviour", "domain", "novelty")
3675
3680
 
3676
3681
 
3677
- def weigh(root, files, declared, goal, policy):
3682
+ def weigh(root, files, declared, goal, policy, globs=()):
3678
3683
  """weight.compute, plus the documents rule (faster-task-cycle (c), in state.py because
3679
3684
  weight.py is at its line cap). Predicate P: at least one concrete file, every file a
3680
3685
  document (is_document) or a test, at least one a document. When P holds, file text no
3681
3686
  longer scores behaviour (the goal's keyword still does, exactly as in compute), and
3682
3687
  behaviour, domain and novelty still UNKNOWN become 0 with the evidence "documents only"; hints and
3683
3688
  declarations were applied inside compute. `documents_only` is {"zeroed": {axis: what it
3684
- would have scored}}, None when P does not hold or the policy has no `documents` key."""
3689
+ would have scored}}, None when P does not hold or the policy has no `documents` key.
3690
+ `globs` are the task's glob entries, which score nothing themselves: P fails when one is
3691
+ not itself a document or test glob."""
3685
3692
  concrete = [posixpath.normpath(f.strip().replace(os.sep, "/")) for f in files if f.strip()]
3686
3693
  tests = ((policy.get("axes") or {}).get("coverage") or {}).get("test_globs")
3687
3694
  inside = all(not f.startswith(("/", "../")) and f != ".." for f in concrete)
3688
- if not (inside and any(is_document(f, policy) for f in concrete)
3695
+ if not (inside and all(is_document(g, policy) or weight.match(g, tests) for g in globs)
3696
+ and any(is_document(f, policy) for f in concrete)
3689
3697
  and all(is_document(f, policy) or weight.match(f, tests) for f in concrete)):
3690
3698
  result = weight.compute(root, files, declared, goal, policy=policy)
3691
3699
  result.documents_only = None
@@ -3722,6 +3730,9 @@ def documents_cover(root, state, policy, tree):
3722
3730
  g = runs[-1] if runs else None
3723
3731
  if not g or g["exit"] != 0 or any(r["tree"] == g["tree"] and r["exit"] != 0 for r in runs):
3724
3732
  return off # D2
3733
+ # A full-suite run after G that is not green stops the rule, whatever its tree (it may be None).
3734
+ if any(full_suite(r) and r["n"] > g["n"] and r.get("exit") != 0 for r in state["verify"]["runs"]):
3735
+ return off
3725
3736
  diff = safe_sensor(sensors.diff_files, root, g["tree"], tree)
3726
3737
  if not diff or not all(is_document(d["path"], policy) for d in diff):
3727
3738
  return off # D3, D4
@@ -3891,7 +3902,9 @@ def cmd_verify(args, root):
3891
3902
  run = run_v7(root, state, "suite" if scope == "all" else scope, args, tcfg)
3892
3903
  code = run["exit"]
3893
3904
  if args.expect == "fail":
3894
- run["bites"] = code not in (0, None)
3905
+ # exit 1 is a failing test; the runner's own usage error (2) is not, unless
3906
+ # testing.md's bite_codes names it (pytest 1,2 — 2 is a collection error)
3907
+ run["bites"] = code in (safe_sensor(sensors.bite_codes, root) or (1,))
3895
3908
  print("expected fail: %s" % ("the test fails without the fix — it bites" if run["bites"]
3896
3909
  else "the test passes without the fix — it does not bite yet"))
3897
3910
  code = 0 if run["bites"] else 1
@@ -4299,9 +4312,11 @@ def archive_v7(root, state, tree, result, facts=None):
4299
4312
  ensure_logs(root, state["task_id"])
4300
4313
  target = os.path.join(logs_dir(root, state["task_id"]), "state.json")
4301
4314
  state["updated_at"] = now()
4302
- with open(target, "w", encoding="utf-8") as fh:
4315
+ tmp = "%s.%d.tmp" % (target, os.getpid())
4316
+ with open(tmp, "w", encoding="utf-8") as fh:
4303
4317
  json.dump(state, fh, indent=2, ensure_ascii=False)
4304
4318
  fh.write("\n")
4319
+ os.replace(tmp, target)
4305
4320
  with contextlib.suppress(OSError):
4306
4321
  os.remove(handoff_path(root))
4307
4322
  remove_state(root)
@@ -4461,6 +4476,12 @@ def status_lines(root, state):
4461
4476
  events, _ = read_journal(root, state["task_id"])
4462
4477
  last = events[-1] if events else {}
4463
4478
  pending = (state.get("questions") or {}).get("pending") or []
4479
+ if is_v7(state):
4480
+ # The cache lags a hand-filled [Answer]:; the file is the truth, as guard_pending reads it.
4481
+ with contextlib.suppress(OSError, ValueError, KeyError):
4482
+ _, asked = parse_questions(questions_path(root, state, None))
4483
+ gate_id = open_gate(state)
4484
+ pending = [q["id"] for q in asked if (q["kind"] != "G" and is_pending(q)) or q["id"] == gate_id]
4464
4485
  lines.append("task: %s — %s" % (state["task_id"], one_line(state.get("goal"), 140)))
4465
4486
  if not is_v7(state):
4466
4487
  lines += ["tier: %s · workflow %s · stage %s" % (state.get("risk_tier") or "?", state.get("workflow"),
@@ -4684,9 +4705,11 @@ def abandon_old(_args, root):
4684
4705
  ensure_logs(root, state["task_id"])
4685
4706
  target = os.path.join(logs_dir(root, state["task_id"]), "state.json")
4686
4707
  state["updated_at"] = now()
4687
- with open(target, "w", encoding="utf-8") as fh:
4708
+ tmp = "%s.%d.tmp" % (target, os.getpid())
4709
+ with open(tmp, "w", encoding="utf-8") as fh:
4688
4710
  json.dump(state, fh, indent=2, ensure_ascii=False)
4689
4711
  fh.write("\n")
4712
+ os.replace(tmp, target)
4690
4713
  with contextlib.suppress(OSError):
4691
4714
  os.remove(handoff_path(root))
4692
4715
  remove_state(root)
@@ -7,7 +7,7 @@ C1. Production behaviour is the source of truth; what looks wrong may be load-be
7
7
  C2. A bugfix starts with the failing test; legacy behaviour gets a characterization test before it changes.
8
8
  C3. A refactoring changes no behaviour and is never mixed with a feature.
9
9
  C4. Every step names its files; scope grows only by amending the plan.
10
- C5. No agent merges or deploys; an agent commits, pushes or approves only by running, verbatim and once, a command a human picked in the dialog; otherwise a human runs it outside the agent.
10
+ C5. No agent merges or deploys; an agent commits, pushes or approves only by running, verbatim and once, a command a human picked in the dialog, as the harness recorded; else a human runs it outside.
11
11
  C6. <!-- project principle — filled by /ai-init from the survey, confirmed by you -->
12
12
  C7. <!-- project principle -->
13
13
  C8. <!-- project principle -->
@@ -1,24 +1,30 @@
1
1
  # SDLC artefacts
2
2
 
3
- Feature work in this repo follows the AI-Native SDLC flow: **intent → spec → plan → build**.
4
- Each stage has a skill that produces one file here, and the next stage reads it.
3
+ Work that needs a written intent and design before any code follows
4
+ **spec → plan → build**. Each stage has a skill that produces one file here,
5
+ and the next stage reads it.
5
6
 
6
7
  ```
7
- /sdlc-intent <topic> → docs/sdlc/intent/<slug>.md (problem, outcome, constraints)
8
+ /rr-spec <idea | intent/<slug>.md> → docs/sdlc/specs/<slug>.md (## Intent, requirements, design, Flagged concerns)
9
+ │ docs/sdlc/intent/<slug>.questions.md (the brainstorm, recorded by state.py)
10
+ /rr-plan specs/<slug>.md → docs/sdlc/plans/<slug>.md (files that change, order, risks, proof, rollback)
8
11
  │
9
- /sdlc-spec intent/<slug>.md → docs/sdlc/specs/<slug>.md (requirements, design, flagged concerns)
10
- │
11
- /sdlc-plan specs/<slug>.md → docs/sdlc/plans/<slug>.md (files, order, risks, proof)
12
- │
13
- build (main session, one plan step at a time; `ai-reviewer` before commit)
12
+ /rr-build — one row at a time (each row is one task, weighed and started by /rr-plan)
13
+ /rr-build-all plans/<slug>.md — the whole plan as one task: parallel implementers, one review loop
14
14
  ```
15
15
 
16
16
  | Folder | Produced by | Contains |
17
17
  |---|---|---|
18
- | `intent/` | `/sdlc-intent` | one file per idea; what and why, not how |
19
- | `specs/` | `/sdlc-spec` | requirements + design; ends with **Flagged concerns** |
20
- | `plans/` | `/sdlc-plan` | ordered implementation steps with verification |
21
- | `adr/` | you / `architect` | Architecture Decision Records (MADR style) |
18
+ | `intent/` | `/rr-spec`'s brainstorm (`state.py ask --topic`) | `<slug>.questions.md`, one question at a time with the answer on record; an `intent/<slug>.md` written by hand is accepted instead of the brainstorm |
19
+ | `specs/` | `/rr-spec`, `architect` on STRONG | `## Intent` (problem, outcome, affected users and systems, constraints, decisions taken), requirements, design; ends with **Flagged concerns**, which a human resolves or accepts before planning |
20
+ | `plans/` | `/rr-plan` | Files that change / Order of work / Risks / Proof (every requirement to a test or command) / Rollback; each row carries its files and its *Consumes* line |
21
+ | `adr/` | you, or `architect` | Architecture Decision Records (MADR style) |
22
+ | `constitution.md` | `/rr-setup`, then you | the principles the project does not negotiate, cited as `C<n>` in specs, plans and reviews; at most 15 lines |
22
23
 
23
- Each folder holds a `TEMPLATE.md`; the skills copy it and fill it in. Commit the artefacts with the
24
- code they describe so the reasoning survives the pull request.
24
+ The skills fill in the plugin's templates
25
+ (`$AI_HOME/skills/project-init/templates/{intent,spec,plan,adr}.md`, in the
26
+ installed plugin); to override one, copy it to `docs/sdlc/<folder>/TEMPLATE.md`
27
+ and edit it — the skills read that one first. Commit the artefacts with the code they describe, so the
28
+ reasoning survives the pull request. A task's own record — weight, plan, questions, verify runs, review,
29
+ verdict — is not here but in `.ai/reports/<task-id>/task.md`, written by
30
+ `state.py`.
@@ -0,0 +1,131 @@
1
+ # Testing policy
2
+
3
+ Tests describe behaviour. Coverage percentage is a side effect, not a goal.
4
+
5
+ ## Characterization first
6
+
7
+ Before changing behaviour that is not covered by a test — which is most legacy
8
+ behaviour — write a test that passes against the code **as it is today**. That
9
+ test is the definition of what must not break. Only then change the code.
10
+
11
+ ```
12
+ characterization test → refactor → verify identical behaviour → feature change
13
+ ```
14
+
15
+ Never do those in one step, and never in one commit.
16
+
17
+ ## What to consider, by kind of change
18
+
19
+ | Change touches | Also test |
20
+ |---|---|
21
+ | a shared service | the existing callers, not only the new one |
22
+ | money, tax, fiscal | rounding, currency, negative and zero amounts, refunds |
23
+ | a state machine | every transition that is now reachable, and the ones that must stay unreachable |
24
+ | an async consumer | retry, double delivery, out-of-order delivery, poison message |
25
+ | an external integration | timeout, 500, malformed response, partial write |
26
+ | a write path | idempotency, concurrency, transaction boundaries |
27
+ | authorization | the negative case, for every role |
28
+ | a public API or event | the old shape still works |
29
+
30
+ ## Three scopes, three moments
31
+
32
+ A task runs tests three times at most, and each run has its own scope. Nothing
33
+ else runs in between.
34
+
35
+ | When | What runs | Command |
36
+ |---|---|---|
37
+ | after each implementation step, before `step-done` | **only the tests the plan named for that step** — the files it touched, their direct callers' tests | `step_test_command` |
38
+ | once, after the **last** step | the full fast suite — unit and integration, no e2e | `verify_command` |
39
+ | once, at the **end of the task**, after the full suite is green | the end-to-end suite | `e2e_command` |
40
+
41
+ - A step never runs the full suite, and **never** runs e2e. If the plan named
42
+ no test for a step, nothing runs for it.
43
+ - A step's tests are scoped by path or filter, not by `--stop-on-failure`: the
44
+ scoped run still goes to the end, and its failures are fixed inside that step.
45
+ - The e2e suite runs **once per task**, at the very end, never per step. Skip it
46
+ at T0/T1, and when the change cannot reach a flow the e2e suite covers — say
47
+ which, in one line; silence is not a verdict.
48
+ - A failure in a step's own tests is fixed in that step, before `step-done`;
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.
51
+
52
+ ## Reading a failure
53
+
54
+ **Run to the end, then fix as one batch.** The verification command runs with
55
+ no fail-fast flag and is never interrupted at the first failure. Collect every
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
59
+ sends the task back to the start: the plan, the tier and the finished steps
60
+ stand. At most two remediation rounds; a third means the human decides.
61
+
62
+ Classify before fixing. The four answers are:
63
+
64
+ - **EXISTING TEST FAILURE** — it failed before this change too. Say so, do not
65
+ fix it inside this task.
66
+ - **NEW REGRESSION** — this change broke it. Fix the code, not the test.
67
+ - **TEST ENVIRONMENT FAILURE** — database, fixtures, network, container. Say what
68
+ is missing.
69
+ - **UNKNOWN** — you could not tell. Say what you tried.
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
+
74
+ **Never edit a test to make it pass when production behaviour changed
75
+ unexpectedly.** A test that suddenly disagrees with the code is evidence, and
76
+ deleting evidence is the worst available option.
77
+
78
+ ## Verification
79
+
80
+ Three commands, one per scope. `verify_command` proves the project is healthy;
81
+ `step_test_command` proves one step is; `e2e_command` proves the whole flow
82
+ still works. Keep each to one command — chain the pieces in a Makefile or
83
+ composer script if there are several — and make sure none of them stops at the
84
+ first failure (`--stop-on-failure`, `-x`, `--bail`, `failfast` are off).
85
+
86
+ `verify_command` must **exclude** the e2e suite: it is the fast loop run after
87
+ the last step and again after a remediation batch. `e2e_command` runs once, at
88
+ the end of the task. A bugfix still shows its new failing test first — that one
89
+ test only, through `single_test`.
90
+
91
+ ```
92
+ verify_command: # the full fast suite, e2e excluded — e.g. make test | composer qa && vendor/bin/phpunit --exclude-group e2e
93
+ step_test_command: # the same runner, scoped to a path or filter — e.g. vendor/bin/phpunit {files} | pytest {files}
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
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
97
+ typecheck_command: # e.g. vendor/bin/phpstan analyse --no-progress | mypy . | npx tsc --noEmit — `none` if the project has none
98
+ healthy_output: # two or three lines of what a green run ends with
99
+ runtime: # roughly, for each command, so a hang is recognisable
100
+ single_test: # how to run one test, for the failing-test-first step of a bugfix
101
+ ```
102
+
103
+ `step_test_command` may contain `{files}`, which is replaced by the paths the
104
+ step names; without it the paths are appended. The placeholder is what lets
105
+ `sensors.py` run a step's own tests for the "test must bite" check, so a runner
106
+ that only takes a `--filter` gets `bite: unavailable` rather than a wrong answer.
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
+
114
+ `lint_command` and `typecheck_command` are read by the deterministic sensors,
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
117
+ `unavailable` and keeps the review; only the literal `none`, written by a human,
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.
121
+
122
+ If the project has no e2e suite, write `e2e_command: none` and say so — an
123
+ empty line is read as "not written down yet" and the next task will go looking
124
+ for it. Say which group, tag or directory marks the e2e tests and how
125
+ `verify_command` excludes them, so a step's scoped run and the fast suite stay
126
+ free of them.
127
+
128
+ ## Project specifics
129
+
130
+ <!-- The real commands: how to run unit tests, integration tests, a single test.
131
+ Where fixtures live. What the CI runs. Cite the config. -->