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
@@ -294,7 +294,7 @@ def mirror(path):
294
294
  (policy.get("rules") or {}).get("hint_raises_by", 1), h.get("note", ""))
295
295
  for h in policy.get("hints") or []]
296
296
  out += ["", "Rules: " + json.dumps(policy.get("rules") or {}, ensure_ascii=False), "",
297
- "Budgets: review_rounds_max %s · remediation_rounds %s · tests %s" % (
298
- policy.get("review_rounds_max"), policy.get("remediation_rounds"),
299
- json.dumps(policy.get("tests") or {}))]
297
+ "Budgets: review_rounds_max %s · remediation_rounds %s · tests %s%s" % (
298
+ policy.get("review_rounds_max"), policy.get("remediation_rounds"), json.dumps(policy.get("tests") or {}),
299
+ " · batch %s" % json.dumps(policy["batch"]) if isinstance(policy.get("batch"), dict) else "")]
300
300
  return "\n".join(out) + "\n"
@@ -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 commits, merges or deploys; a human approves 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. -->
@@ -0,0 +1,30 @@
1
+ # SDLC artefacts
2
+
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.
6
+
7
+ ```
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)
11
+ │
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
+ ```
15
+
16
+ | Folder | Produced by | Contains |
17
+ |---|---|---|
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 |
23
+
24
+ The skills fill in the plugin's templates
25
+ (`skills/project-init/templates/{spec,plan,adr}.md`); a folder holds a
26
+ `TEMPLATE.md` only when this project overrides the plugin's, and the skills read
27
+ 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,13 @@
1
+ <!-- riskrails:start -->
2
+ ## AI agent workflow
3
+
4
+ This repository runs an agentic pipeline under `.ai/`. Read `.ai/AGENTS.md` first: it routes to the policies, workflows and rules, which load on demand; `.ai/policies/` is binding.
5
+
6
+ - Production behaviour is the source of truth: document problems outside the task, do not fix them.
7
+
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
+
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 and the harness records, else a human. When a review flags a mistake twice, the fix goes in 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).
13
+ <!-- riskrails:end -->
@@ -0,0 +1,123 @@
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 a single remediation
57
+ step (`state.py remediate`), and run the command once more. One failure never
58
+ sends the task back to the start: the plan, the tier and the finished steps
59
+ stand. At most two remediation rounds; a third means the human decides.
60
+
61
+ Classify before fixing. The four answers are:
62
+
63
+ - **EXISTING TEST FAILURE** — it failed before this change too. Say so, do not
64
+ fix it inside this task.
65
+ - **NEW REGRESSION** — this change broke it. Fix the code, not the test.
66
+ - **TEST ENVIRONMENT FAILURE** — database, fixtures, network, container. Say what
67
+ is missing.
68
+ - **UNKNOWN** — you could not tell. Say what you tried.
69
+
70
+ A review's MEDIUM and LOW findings are recorded with `state.py memory risk add`, not
71
+ fixed in the task; only a BLOCKER, a HIGH or a regression buys an `--after-fix` run.
72
+
73
+ **Never edit a test to make it pass when production behaviour changed
74
+ unexpectedly.** A test that suddenly disagrees with the code is evidence, and
75
+ deleting evidence is the worst available option.
76
+
77
+ ## Verification
78
+
79
+ Three commands, one per scope. `verify_command` proves the project is healthy;
80
+ `step_test_command` proves one step is; `e2e_command` proves the whole flow
81
+ still works. Keep each to one command — chain the pieces in a Makefile or
82
+ composer script if there are several — and make sure none of them stops at the
83
+ first failure (`--stop-on-failure`, `-x`, `--bail`, `failfast` are off).
84
+
85
+ `verify_command` must **exclude** the e2e suite: it is the fast loop run after
86
+ the last step and again after a remediation batch. `e2e_command` runs once, at
87
+ the end of the task. A bugfix still shows its new failing test first — that one
88
+ test only, through `single_test`.
89
+
90
+ ```
91
+ verify_command: # the full fast suite, e2e excluded — e.g. make test | composer qa && vendor/bin/phpunit --exclude-group e2e
92
+ step_test_command: # the same runner, scoped to a path or filter — e.g. vendor/bin/phpunit {files} | pytest {files}
93
+ 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
94
+ lint_command: # e.g. vendor/bin/php-cs-fixer fix --dry-run | ruff check . | npm run lint — write `none` if the project has none
95
+ 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
96
+ typecheck_command: # e.g. vendor/bin/phpstan analyse --no-progress | mypy . | npx tsc --noEmit — `none` if the project has none
97
+ healthy_output: # two or three lines of what a green run ends with
98
+ runtime: # roughly, for each command, so a hang is recognisable
99
+ single_test: # how to run one test, for the failing-test-first step of a bugfix
100
+ ```
101
+
102
+ `step_test_command` may contain `{files}`, which is replaced by the paths the
103
+ step names; without it the paths are appended. The placeholder is what lets
104
+ `sensors.py` run a step's own tests for the "test must bite" check, so a runner
105
+ that only takes a `--filter` gets `bite: unavailable` rather than a wrong answer.
106
+
107
+ `lint_command` and `typecheck_command` are read by the deterministic sensors,
108
+ never by a model. They decide, together with the test run, whether a T2 change
109
+ can skip its model review (`risk-tiers.json`, `sensors`). A **missing** line is
110
+ `unavailable` and keeps the review; only the literal `none`, written by a human,
111
+ says the project has no such tool. `sensors.py detect` proposes the exact line
112
+ from the config files it finds, and never runs what it detected.
113
+
114
+ If the project has no e2e suite, write `e2e_command: none` and say so — an
115
+ empty line is read as "not written down yet" and the next task will go looking
116
+ for it. Say which group, tag or directory marks the e2e tests and how
117
+ `verify_command` excludes them, so a step's scoped run and the fast suite stay
118
+ free of them.
119
+
120
+ ## Project specifics
121
+
122
+ <!-- The real commands: how to run unit tests, integration tests, a single test.
123
+ Where fixtures live. What the CI runs. Cite the config. -->
@@ -0,0 +1 @@
1
+ { "version": 1, "enabled": true, "window_seconds": 120, "stop_blocks_max": 2 }
@@ -0,0 +1,15 @@
1
+ # Constitution — {{PROJECT}}
2
+
3
+ <!-- The 10–15 principles this project does not negotiate, one line each, numbered C1…;
4
+ /sdlc-spec and the planner cite them. Under 15: what is not here lives in .ai/policies/. -->
5
+
6
+ C1. Production behaviour is the source of truth; what looks wrong may be load-bearing.
7
+ C2. A bugfix starts with the failing test; legacy behaviour gets a characterization test before it changes.
8
+ C3. A refactoring changes no behaviour and is never mixed with a feature.
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, as the harness recorded; else a human runs it outside.
11
+ C6. <!-- project principle — filled by /ai-init from the survey, confirmed by you -->
12
+ C7. <!-- project principle -->
13
+ C8. <!-- project principle -->
14
+ C9. <!-- project principle -->
15
+ C10. <!-- project principle -->
@@ -0,0 +1,13 @@
1
+ <!-- riskrails:start -->
2
+ ## AI agent workflow
3
+
4
+ This repository runs an agentic pipeline under `.ai/`. Read `.ai/AGENTS.md` first: it routes to the policies, workflows and rules, which load on demand; `.ai/policies/` is binding.
5
+
6
+ - Production behaviour is the source of truth: document problems outside the task, do not fix them.
7
+
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
+
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).
13
+ <!-- riskrails:end -->
@@ -0,0 +1,67 @@
1
+ # Task state
2
+
3
+ `current.json` is the machine-readable record of the task in flight. It is
4
+ written by `skills/ai-task/state.py` (atomically), read by `/ai-status`, and
5
+ enforced by `ai-scope-guard`, which derives the files the current step may touch
6
+ from it.
7
+
8
+ Two other writers exist, and only these two. A schema migration in
9
+ `/project-update` may change the state through `ctx.patch_state()`, with the
10
+ same atomic write and a `schema_migrated` history entry — that is how a task in
11
+ flight survives a change to the shape of this file. And `hooks/context-guard.py`
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
14
+ them by hand, and the path guard refuses when one tries.
15
+
16
+ It is **not committed**: it describes a session, not the repository. The
17
+ `.gitignore` entry added by `/ai-init` keeps it out of git.
18
+
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`).
25
+
26
+ It holds facts, never transcripts:
27
+
28
+ ```
29
+ task_id, goal, workflow, risk_tier, current_stage, affected_modules,
30
+ context_summary_ref, approved_plan { ref, current_step_id, steps[] },
31
+ completed_steps, test_status, e2e_status, review_status, security_status, open_risks,
32
+ next_action, owner_runtime, resume_point { stage, step_id, next_action, at, runtime },
33
+ questions { file, pending[] }, handoff { file, written_at, reason },
34
+ human_approval { required, granted, granted_by, granted_at,
35
+ requested_at, gate_id, requested_session, rejected_at, via, unattended },
36
+ created_at, updated_at, history[]
37
+ ```
38
+
39
+ `state.py` applies the schema-2 keys in memory as it loads, so a project that has
40
+ not run `/project-update` yet still answers every command; `save()` then writes
41
+ them. A reader that opens this file directly sees what is on disk, not those
42
+ defaults.
43
+
44
+ ## The other files beside it
45
+
46
+ | File | Written by | Committed |
47
+ |---|---|---|
48
+ | `current.json` | `state.py`, and `update.py` during a migration | no |
49
+ | `session.json` | `context-guard.py`, on SessionStart and each prompt | no |
50
+ | `handoff.md` | `state.py handoff` — never a model | no |
51
+ | `../reports/<task-id>/task.md` §Questions | `state.py ask/answer/questions` | yes |
52
+ | `../reports/<task-id>/events.jsonl` | `state.py` (`emit`), append-only | yes |
53
+
54
+ `handoff.md` is the ≤ 30-line frame a session that lost its context reads first:
55
+ the goal, the next action and its resume point, the pending questions, the latest
56
+ decisions, rejected options and failed attempts, and the user's last instruction
57
+ verbatim. It is rendered from the four sources above, so deleting it loses
58
+ nothing — the next stage-moving command writes it again.
59
+
60
+ `events.jsonl` is the journal: one line per state change, `history[]`'s facts
61
+ with types beside them. It is best effort by contract — a line that cannot be
62
+ written never fails the command that emitted it — so nothing depends on it being
63
+ complete, and readers skip a line they cannot parse.
64
+
65
+ If this file is corrupt, `state.py` refuses to read it rather than replacing it.
66
+ Inspect it by hand: the scope guard's boundaries come from here, and a silently
67
+ regenerated state file means an unbounded implementer.
@@ -0,0 +1,13 @@
1
+ <!-- riskrails:start -->
2
+ ## AI agent workflow
3
+
4
+ This repository runs an agentic pipeline under `.ai/`. Read `.ai/AGENTS.md` first: it routes to the policies, workflows and rules, which load on demand; `.ai/policies/` is binding.
5
+
6
+ - Production behaviour is the source of truth: document problems outside the task, do not fix them.
7
+
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
+
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 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
+ <!-- riskrails:end -->
@@ -0,0 +1,49 @@
1
+ {
2
+ "version": 1,
3
+ "axes": {
4
+ "scope": {"measure": "references", "bands": [0, 5], "hot_paths": ["hooks/**", "src/Shared/**"]},
5
+ "behaviour": {"measure": "session",
6
+ "keywords": ["workflow", "transition", "state machine", "messenger", "queue", "async", "transaction"]},
7
+ "reversibility": {"measure": "paths+content",
8
+ "needs_step": ["**/migrations/**", "**/Migrations/**", "config/**", "k8s/**", "**/env/**"],
9
+ "irreversible": ["**/prod/**", "**/production/**", "deploy/**", "**/*.deploy.*"],
10
+ "irreversible_content": ["DROP ", "TRUNCATE", "DELETE FROM", "ALTER .* DROP", "RENAME"]},
11
+ "domain": {"measure": "modules+session",
12
+ "modules": ["**/Payment/**", "**/Tax/**", "**/Invoice/**", "**/Security/**", "**/account/**"],
13
+ "customer_visible": ["templates/**", "**/Shop/**", "**/Resources/views/**"]},
14
+ "novelty": {"measure": "session",
15
+ "manifests": ["composer.json", "requirements*.txt", "package.json", "pyproject.toml", "Gemfile"]},
16
+ "coverage": {"measure": "tests-map",
17
+ "code_globs": ["**/*.php", "**/*.py", "**/*.js", "**/*.ts", "**/*.sh"],
18
+ "test_globs": ["tests/**", "spec/**", "**/*Test.php", "**/*.test.*", "**/*.spec.*", "**/test_*.py"],
19
+ "map": {}}
20
+ },
21
+ "hints": [
22
+ {"paths": ["**/login*", "**/Security/**", "**/*Auth*"], "axis": "domain", "note": "auth-shaped path"},
23
+ {"paths": ["skills/project-update/migrations/**"], "axis": "reversibility", "note": "touches user trees"},
24
+ {"paths": [".ai/policies/**", ".claude/**", ".codex/**", "CLAUDE.md", "AGENTS.md"], "axis": "behaviour", "note": "pipeline"},
25
+ {"paths": ["composer.json", "package.json", "requirements*.txt"], "axis": "novelty", "note": "dependency manifest"}
26
+ ],
27
+ "modes": {
28
+ "light": {"max_w": 2, "plan": "inline", "review": "on_red_fact", "reviewer": "BALANCED",
29
+ "human_stops": 0, "suite": "verify", "e2e": false},
30
+ "guarded": {"max_w": 7, "plan": "plan_mode", "review": "one", "reviewer": "BALANCED",
31
+ "human_stops": 1, "suite": "verify+e2e", "e2e": true},
32
+ "full": {"min_w": 8, "hard_axes": ["reversibility", "domain"], "plan": "written", "review": "one",
33
+ "reviewer": "STRONG", "security_focus_when": "domain=2", "human_stops": 2, "suite": "verify+e2e", "e2e": true},
34
+ "full_irreversible": {"when": "reversibility=2 and domain=2, or irreversible_content",
35
+ "human_stops": 3, "confirm_names_operation": true}
36
+ },
37
+ "rules": {"unknown_scores": 1, "hint_raises_by": 1, "hint_raises_once_per_axis": true,
38
+ "rescore_axes": ["reversibility", "domain"], "rescore_direction": "up", "lower_by": "human",
39
+ "steps_advice_over": 8},
40
+ "review_rounds_max": 2,
41
+ "remediation_rounds": 2,
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"},
44
+ "examples": {
45
+ "light": ["a font or copy change in one template", "a test-only change", "a message-text change"],
46
+ "guarded": ["a regex fix in a hot guard script", "a dev-environment manifest", "a feature with a new screen and an additive migration"],
47
+ "full": ["a change in Payment/, Tax/ or Security/", "a migration that rewrites user trees", "a production manifest or secret"]
48
+ }
49
+ }
@@ -0,0 +1,6 @@
1
+ # claude-agentic (.ai)
2
+ .ai/state/*.json
3
+ .ai/state/handoff.md
4
+ .ai/state/*.tmp
5
+ .ai/state/*.jsonl
6
+ .ai/local/
@@ -0,0 +1,32 @@
1
+ # Weight table
2
+
3
+ <!-- generated by `state.py weight --mirror` from weight.json; edit the JSON and regenerate. sha256: 5353ca529c46f2443e12947ec205a6887fb594a4de1f87f0eb1c0e6c0f826133 -->
4
+
5
+ Six axes, 0–2 each; W is their sum. Why each rule is what it is: the plugin's `docs/weight.md`.
6
+
7
+ | Axis | Measured by | Table |
8
+ |---|---|---|
9
+ | scope | references | bands: [0, 5]; hot_paths: ["hooks/**", "src/Shared/**"] |
10
+ | behaviour | session | keywords: ["workflow", "transition", "state machine", "messenger", "queue", "async", "transaction"] |
11
+ | reversibility | paths+content | needs_step: ["**/migrations/**", "**/Migrations/**", "config/**", "k8s/**", "**/env/**"]; irreversible: ["**/prod/**", "**/production/**", "deploy/**", "**/*.deploy.*"]; irreversible_content: ["DROP ", "TRUNCATE", "DELETE FROM", "ALTER .* DROP", "RENAME"] |
12
+ | domain | modules+session | modules: ["**/Payment/**", "**/Tax/**", "**/Invoice/**", "**/Security/**", "**/account/**"]; customer_visible: ["templates/**", "**/Shop/**", "**/Resources/views/**"] |
13
+ | novelty | session | manifests: ["composer.json", "requirements*.txt", "package.json", "pyproject.toml", "Gemfile"] |
14
+ | coverage | tests-map | code_globs: ["**/*.php", "**/*.py", "**/*.js", "**/*.ts", "**/*.sh"]; test_globs: ["tests/**", "spec/**", "**/*Test.php", "**/*.test.*", "**/*.spec.*", "**/test_*.py"]; map: {} |
15
+
16
+ | Mode | When | Plan | Review | Reviewer | Human stops | Suite |
17
+ |---|---|---|---|---|---|---|
18
+ | light | W ≤ 2 | inline | on_red_fact | BALANCED | 0 | verify |
19
+ | guarded | W ≤ 7 | plan_mode | one | BALANCED | 1 | verify+e2e |
20
+ | full | W ≥ 8 or reversibility or domain = 2 | written | one | STRONG | 2 | verify+e2e |
21
+ | full_irreversible | reversibility=2 and domain=2, or irreversible_content | as full | as full | as full | 3 | as full |
22
+
23
+ | Hint paths | Axis | Note |
24
+ |---|---|---|
25
+ | `**/login*`, `**/Security/**`, `**/*Auth*` | domain +1 | auth-shaped path |
26
+ | `skills/project-update/migrations/**` | reversibility +1 | touches user trees |
27
+ | `.ai/policies/**`, `.claude/**`, `.codex/**`, `CLAUDE.md`, `AGENTS.md` | behaviour +1 | pipeline |
28
+ | `composer.json`, `package.json`, `requirements*.txt` | novelty +1 | dependency manifest |
29
+
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
+
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"}