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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +28 -15
- package/ai/policies/model-routing.md +7 -7
- package/ai/policies/production.md +3 -3
- package/ai/policies/release.md +2 -2
- package/ai/policies/review-economy.md +27 -30
- package/bin/riskrails.js +5 -5
- package/hooks/context-guard.py +1 -1
- package/hooks/guard/common.py +2 -0
- package/hooks/guard/paths.py +41 -30
- package/hooks/project-scaffold.py +1 -5
- package/hooks/runtime-gate.py +6 -6
- package/hooks/update_notice.py +20 -14
- package/instructions/routing.md +1 -1
- package/instructions/stub.md +3 -3
- package/package.json +1 -1
- package/skills/AGENTS.md +1 -1
- package/skills/CLAUDE.md +1 -1
- package/skills/ai-init/templates/.ai/AGENTS.md +1 -1
- package/skills/ai-init/templates/.ai/VERSION +1 -1
- package/skills/ai-init/templates/.ai/policies/security.md +7 -1
- package/skills/ai-init/templates/.ai/policies/testing.md +19 -6
- package/skills/ai-init/templates/AGENTS.block.md +2 -2
- package/skills/ai-init/templates/CLAUDE.block.md +2 -2
- package/skills/ai-init/templates/GEMINI.block.md +2 -2
- package/skills/ai-init/templates/gitignore.snippet +1 -0
- package/skills/ai-init/templates/junie-guidelines.block.md +2 -2
- package/skills/ai-task/sensors.py +24 -4
- package/skills/ai-task/state.py +37 -14
- package/skills/project-init/templates/constitution.md +1 -1
- package/skills/project-init/templates/sdlc-README.md +20 -14
- package/skills/project-update/history/blobs/00289f98434fed230eebe768dfb3425f83d737866a3aebc97aa95167c4fa0c6e +131 -0
- package/skills/project-update/history/blobs/079a91f976ad69b5cf49ccb9a28849576e4735ddd9485fc26599e528a69fd2f4 +30 -0
- package/skills/project-update/history/blobs/07ff7fa6f93d471ff533f553b4e523e4105ccde1f3dab70132c5237728f68422 +13 -0
- package/skills/project-update/history/blobs/0ef4a360c76da290173dea0f8073949cb0f8113710a38df95acae5d96bdd1286 +123 -0
- package/skills/project-update/history/blobs/18c7a2753ce9cb6f6d3d1e9d5d1125d75950b879d56b7325225b200869631b29 +15 -0
- package/skills/project-update/history/blobs/25d4f2a86deb5e2574bb3210b67bb24fcc4afb19f93a7b65a057daa874a9d18e +1 -0
- package/skills/project-update/history/blobs/48d9950f16e31d55475f566a80dc95f64b35f1c804d2f51729e313a4f29bac8d +13 -0
- package/skills/project-update/history/blobs/5a93f78b4ed4d416c171139b2ce610a117f9c1dd46fbb56ca86729cb4c53d10c +6 -0
- package/skills/project-update/history/blobs/6a3be0e624e5ad91e44d08857b7183ee53b7903e1e8d56ff196dbc9dde271e4a +75 -0
- package/skills/project-update/history/blobs/7170553410bba904ffb7edb4b231e3b3579b85542d84de717299980938f82c9d +13 -0
- package/skills/project-update/history/blobs/774b1e3a04ef0d49d33c44a2cfecd92eb6f6b738a12b6a681103ba7fc13e7b86 +67 -0
- package/skills/project-update/history/blobs/7f063e79195a5c85c37b7260dd20c184d3b9bea85e37202e0bbca73ad84bba49 +13 -0
- package/skills/project-update/history/blobs/c8573ca1f810ce6c4ac49456a4c1725b93e0d3b74c54ce95d46d8747777895f0 +13 -0
- package/skills/project-update/history/blobs/d6e5b34d475e7187d2a8cb2aac7e7f1a427142baf6461bda387d70ceb4d8a8c1 +30 -0
- package/skills/project-update/history/blobs/e120ff4e32b2e950fc94ada4e787d57dc9c790da37223e5a202d74f7e3aadf80 +13 -0
- package/skills/project-update/history/index.json +36 -15
- package/skills/project-update/migrations/0011_retire_risk_tiers.py +37 -0
- package/skills/project-update/migrations/__init__.py +6 -1
- package/skills/project-update/render_instructions.py +1 -1
- package/skills/project-update/update.py +374 -87
- package/skills/rr-build-all/SKILL.md +2 -2
- package/skills/rr-plan/SKILL.md +3 -1
- package/skills/rr-review/SKILL.md +13 -8
- package/skills/rr-setup/SKILL.md +15 -2
- package/skills/rr-setup/setup.py +7 -1
- package/skills/rr-spec/SKILL.md +4 -2
- package/skills/usage-report/usage-report.py +4 -2
- package/skills/ai-init/templates/.ai/policies/risk-tiers.json +0 -628
- package/skills/ai-init/templates/.ai/policies/risk-tiers.md +0 -180
package/instructions/stub.md
CHANGED
|
@@ -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
|
|
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
|
|
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/
|
|
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/
|
|
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/`, `
|
|
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
|
-
|
|
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`,
|
|
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
|
|
56
|
-
|
|
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
|
|
104
|
-
can skip its model review (`
|
|
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.
|
|
107
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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",
|
|
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
|
|
package/skills/ai-task/state.py
CHANGED
|
@@ -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 /
|
|
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
|
-
"(/
|
|
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, "
|
|
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 /
|
|
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 /
|
|
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 /
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
4
|
-
Each stage has a skill that produces one file here,
|
|
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
|
-
/
|
|
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
|
-
/
|
|
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/` | `/
|
|
19
|
-
| `specs/` | `/
|
|
20
|
-
| `plans/` | `/
|
|
21
|
-
| `adr/` | you
|
|
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
|
-
|
|
24
|
-
|
|
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. -->
|