riskrails 3.0.1 → 3.2.1

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 (99) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/README.md +29 -15
  5. package/ai/policies/model-routing.md +7 -7
  6. package/ai/policies/production.md +3 -3
  7. package/ai/policies/release.md +2 -2
  8. package/ai/policies/review-economy.md +27 -30
  9. package/bin/riskrails.js +114 -23
  10. package/hooks/ai-path-guard-defaults.json +3 -2
  11. package/hooks/context-guard.py +15 -12
  12. package/hooks/dialog-gate.py +1 -1
  13. package/hooks/guard/common.py +2 -0
  14. package/hooks/guard/dialog.py +107 -27
  15. package/hooks/guard/git.py +12 -3
  16. package/hooks/guard/paths.py +150 -37
  17. package/hooks/project-scaffold.py +3 -6
  18. package/hooks/runtime-gate.py +134 -33
  19. package/hooks/update_notice.py +22 -16
  20. package/instructions/routing.md +1 -1
  21. package/instructions/stub.md +3 -3
  22. package/package.json +1 -1
  23. package/skills/AGENTS.md +1 -1
  24. package/skills/CLAUDE.md +1 -1
  25. package/skills/ai-init/templates/.ai/AGENTS.md +1 -1
  26. package/skills/ai-init/templates/.ai/VERSION +1 -1
  27. package/skills/ai-init/templates/.ai/policies/security.md +7 -1
  28. package/skills/ai-init/templates/.ai/policies/testing.md +19 -6
  29. package/skills/ai-init/templates/.ai/state/README.md +4 -4
  30. package/skills/ai-init/templates/AGENTS.block.md +2 -2
  31. package/skills/ai-init/templates/CLAUDE.block.md +2 -2
  32. package/skills/ai-init/templates/GEMINI.block.md +2 -2
  33. package/skills/ai-init/templates/gitignore.snippet +1 -0
  34. package/skills/ai-init/templates/junie-guidelines.block.md +2 -2
  35. package/skills/ai-task/rr_state/__init__.py +1 -0
  36. package/skills/ai-task/rr_state/cli.py +210 -0
  37. package/skills/ai-task/rr_state/close.py +171 -0
  38. package/skills/ai-task/rr_state/dialog.py +541 -0
  39. package/skills/ai-task/rr_state/dialog_cmd.py +164 -0
  40. package/skills/ai-task/rr_state/fs.py +220 -0
  41. package/skills/ai-task/rr_state/gate.py +125 -0
  42. package/skills/ai-task/rr_state/gate_cmd.py +128 -0
  43. package/skills/ai-task/rr_state/handoff.py +226 -0
  44. package/skills/ai-task/rr_state/journal.py +126 -0
  45. package/skills/ai-task/rr_state/labels.py +99 -0
  46. package/skills/ai-task/rr_state/ledger.py +145 -0
  47. package/skills/ai-task/rr_state/locks.py +84 -0
  48. package/skills/ai-task/rr_state/memory_cmd.py +310 -0
  49. package/skills/ai-task/rr_state/policy.py +378 -0
  50. package/skills/ai-task/rr_state/profile.py +373 -0
  51. package/skills/ai-task/rr_state/questions.py +336 -0
  52. package/skills/ai-task/rr_state/questions_cmd.py +321 -0
  53. package/skills/ai-task/rr_state/report.py +271 -0
  54. package/skills/ai-task/rr_state/report_cmd.py +230 -0
  55. package/skills/ai-task/rr_state/review.py +165 -0
  56. package/skills/ai-task/rr_state/runtime.py +80 -0
  57. package/skills/ai-task/rr_state/start.py +257 -0
  58. package/skills/ai-task/rr_state/steps.py +131 -0
  59. package/skills/ai-task/rr_state/task.py +396 -0
  60. package/skills/ai-task/rr_state/verify.py +231 -0
  61. package/skills/ai-task/sensors.py +37 -4
  62. package/skills/ai-task/state.py +24 -5138
  63. package/skills/project-init/templates/constitution.md +1 -1
  64. package/skills/project-init/templates/sdlc-README.md +20 -14
  65. package/skills/project-update/adopt.py +38 -24
  66. package/skills/project-update/history/blobs/00289f98434fed230eebe768dfb3425f83d737866a3aebc97aa95167c4fa0c6e +131 -0
  67. package/skills/project-update/history/blobs/079a91f976ad69b5cf49ccb9a28849576e4735ddd9485fc26599e528a69fd2f4 +30 -0
  68. package/skills/project-update/history/blobs/07ff7fa6f93d471ff533f553b4e523e4105ccde1f3dab70132c5237728f68422 +13 -0
  69. package/skills/project-update/history/blobs/0ef4a360c76da290173dea0f8073949cb0f8113710a38df95acae5d96bdd1286 +123 -0
  70. package/skills/project-update/history/blobs/18c7a2753ce9cb6f6d3d1e9d5d1125d75950b879d56b7325225b200869631b29 +15 -0
  71. package/skills/project-update/history/blobs/25d4f2a86deb5e2574bb3210b67bb24fcc4afb19f93a7b65a057daa874a9d18e +1 -0
  72. package/skills/project-update/history/blobs/48d9950f16e31d55475f566a80dc95f64b35f1c804d2f51729e313a4f29bac8d +13 -0
  73. package/skills/project-update/history/blobs/5a93f78b4ed4d416c171139b2ce610a117f9c1dd46fbb56ca86729cb4c53d10c +6 -0
  74. package/skills/project-update/history/blobs/6a3be0e624e5ad91e44d08857b7183ee53b7903e1e8d56ff196dbc9dde271e4a +75 -0
  75. package/skills/project-update/history/blobs/7170553410bba904ffb7edb4b231e3b3579b85542d84de717299980938f82c9d +13 -0
  76. package/skills/project-update/history/blobs/774b1e3a04ef0d49d33c44a2cfecd92eb6f6b738a12b6a681103ba7fc13e7b86 +67 -0
  77. package/skills/project-update/history/blobs/7f063e79195a5c85c37b7260dd20c184d3b9bea85e37202e0bbca73ad84bba49 +13 -0
  78. package/skills/project-update/history/blobs/a51517e13e57420d2d88a6bcfb4055d762d4a0d10a8f88450929c5c4572213a9 +67 -0
  79. package/skills/project-update/history/blobs/c8573ca1f810ce6c4ac49456a4c1725b93e0d3b74c54ce95d46d8747777895f0 +13 -0
  80. package/skills/project-update/history/blobs/d6e5b34d475e7187d2a8cb2aac7e7f1a427142baf6461bda387d70ceb4d8a8c1 +30 -0
  81. package/skills/project-update/history/blobs/e120ff4e32b2e950fc94ada4e787d57dc9c790da37223e5a202d74f7e3aadf80 +13 -0
  82. package/skills/project-update/history/index.json +38 -16
  83. package/skills/project-update/migrations/0003_context_diet.py +1 -1
  84. package/skills/project-update/migrations/0007_weight_model.py +130 -23
  85. package/skills/project-update/migrations/0011_retire_risk_tiers.py +37 -0
  86. package/skills/project-update/migrations/README.md +16 -1
  87. package/skills/project-update/migrations/__init__.py +6 -1
  88. package/skills/project-update/render_instructions.py +1 -1
  89. package/skills/project-update/update.py +569 -106
  90. package/skills/rr-build-all/SKILL.md +2 -2
  91. package/skills/rr-plan/SKILL.md +3 -1
  92. package/skills/rr-review/SKILL.md +13 -8
  93. package/skills/rr-setup/SKILL.md +24 -4
  94. package/skills/rr-setup/setup.py +7 -1
  95. package/skills/rr-spec/SKILL.md +4 -2
  96. package/skills/rr-status/status.py +36 -9
  97. package/skills/usage-report/usage-report.py +4 -2
  98. package/skills/ai-init/templates/.ai/policies/risk-tiers.json +0 -628
  99. package/skills/ai-init/templates/.ai/policies/risk-tiers.md +0 -180
@@ -28,6 +28,7 @@ import urllib.request
28
28
  REGISTRY_URL = "https://registry.npmjs.org/riskrails/latest"
29
29
  MAX_AGE = 24 * 3600
30
30
  TIMEOUT = 3
31
+ MAX_BODY = 65536
31
32
  COMMAND = "npx -y riskrails@latest --yes"
32
33
  VERSION_RE = re.compile(r"^(\d+)\.(\d+)\.(\d+)$") # no prerelease, no build metadata
33
34
 
@@ -50,7 +51,7 @@ def installed_version(runtime, plugin_root, home=None):
50
51
  try:
51
52
  with open(path, encoding="utf-8") as fh:
52
53
  version = json.load(fh).get(key)
53
- except (OSError, ValueError, AttributeError):
54
+ except (OSError, ValueError, AttributeError, RecursionError):
54
55
  continue
55
56
  if parse(version):
56
57
  return version
@@ -62,7 +63,7 @@ def read_cache(path):
62
63
  with open(path, encoding="utf-8") as fh:
63
64
  data = json.load(fh)
64
65
  return data if isinstance(data, dict) else {}
65
- except (OSError, ValueError):
66
+ except (OSError, ValueError, RecursionError):
66
67
  return {}
67
68
 
68
69
 
@@ -103,20 +104,7 @@ def notice(runtime, home, plugin_root, now):
103
104
  return message, context
104
105
 
105
106
 
106
- def refresh(path):
107
- """Ask the registry and write the cache; on failure only move checked_at."""
108
- latest = None
109
- try:
110
- url = os.environ.get("RISKRAILS_REGISTRY_URL") or REGISTRY_URL
111
- with urllib.request.urlopen(url, timeout=TIMEOUT) as resp: # nosec: the registry, or a test's file URL
112
- version = json.loads(resp.read().decode("utf-8")).get("version")
113
- if parse(version):
114
- latest = version
115
- except (OSError, ValueError, AttributeError):
116
- pass
117
- if latest is None:
118
- previous = read_cache(path).get("latest")
119
- latest = previous if isinstance(previous, str) else None
107
+ def write_cache(path, latest):
120
108
  tmp = f"{path}.{os.getpid()}.tmp"
121
109
  try:
122
110
  with open(tmp, "w", encoding="utf-8") as fh:
@@ -129,6 +117,24 @@ def refresh(path):
129
117
  pass
130
118
 
131
119
 
120
+ def refresh(path):
121
+ """Ask the registry and write the cache; checked_at moves first, so a fetch that hangs
122
+ or is killed still costs one spawn a day, and a failed fetch keeps the previous latest."""
123
+ previous = read_cache(path).get("latest")
124
+ previous = previous if isinstance(previous, str) else None
125
+ write_cache(path, previous)
126
+ latest = None
127
+ try:
128
+ url = os.environ.get("RISKRAILS_REGISTRY_URL") or REGISTRY_URL
129
+ with urllib.request.urlopen(url, timeout=TIMEOUT) as resp: # nosec: the registry, or a test's file URL
130
+ version = json.loads(resp.read(MAX_BODY).decode("utf-8")).get("version")
131
+ if parse(version):
132
+ latest = version
133
+ except (OSError, ValueError, AttributeError, RecursionError):
134
+ pass
135
+ write_cache(path, latest if latest is not None else previous)
136
+
137
+
132
138
  if __name__ == "__main__":
133
139
  if len(sys.argv) == 3 and sys.argv[1] == "--refresh" and not disabled():
134
140
  refresh(sys.argv[2])
@@ -46,7 +46,7 @@ Facts are collected cheaply; thinking is paid for. Session model {{SESSION_MODEL
46
46
  # Guards
47
47
 
48
48
  <!-- stub: runtime=claude -->
49
- - `ai-git-guard` runs in every repository: no force push, history rewrite, push or merge to a protected branch, `--no-verify`, staged secret or production deploy. `ai-path-guard` and `ai-scope-guard` arm only where `.ai/` exists. No agent merges or deploys; commit, push and approval run only as the command the human picked in the dialog — verbatim, once — or the human runs them outside the agent.
49
+ - `ai-git-guard` runs in every repository: no force push, history rewrite, push or merge to a protected branch, `--no-verify`, staged secret or production deploy. `ai-path-guard` and `ai-scope-guard` arm only where `.ai/` exists. No agent merges or deploys; commit, push and approval run only as the command the human picked in the dialog, as the harness recorded — verbatim, once — or the human runs them outside the agent.
50
50
  - `context-guard` measures the context on every prompt and holds a prompt back once at the block threshold above, when there is one; its "Session state before compaction" block and `.ai/state/handoff.md` are what a resumed session trusts over the summary.
51
51
 
52
52
  <!-- stub: runtime=codex -->
@@ -24,7 +24,7 @@ Rules for every repository; everything else loads on demand. `{{HOME}}/riskrails
24
24
  - With `.ai/`: read `.ai/AGENTS.md` first, run every change through `/rr-build <request>`, treat `.ai/policies/` as binding; `/rr-setup --update` after a plugin reinstall. Without `.ai/`: `/rr-setup` first.
25
25
  - Production behaviour is the source of truth: document problems outside the task, do not fix them. Never mix a refactoring with a feature change.
26
26
  - Each step names the files it may touch; an edit outside them is refused — answer `SCOPE_CHANGE_REQUIRED`, do not widen the step.{{MULTI_EDIT}}
27
- - No agent merges or deploys. Commit, push and approval run only as the command the human picked in the dialog — verbatim, once — or the human runs them outside the agent. A guard's refusal is not a hint to reword.{{TRUST}}
27
+ - No agent merges or deploys. Commit, push and approval run only as the command the human picked in the dialog, as the harness recorded — verbatim, once — or the human runs them outside the agent. A guard's refusal is not a hint to reword.{{TRUST}}
28
28
  - Verify before reporting done: the verification command once, to the end, every failure fixed as one batch, output shown.
29
29
  - Context: `grep -n`, then read the hits; never `cat` logs, lockfiles or generated code; a file over ~{{READ_LINES}} lines is read by a FAST reader (`Explore`, `log-reader`) that returns `file:line` excerpts, never the file. Deterministic tools first. {{NEW_CONTEXT}} between tasks. `/rr-status --cost` shows where tokens went.
30
30
  - Models are tiers, not names: runners and verbatim extraction on FAST at `low`, readers that judge on BALANCED; the session implements; STRONG and EXPERT only on a named trigger, one agent at a time, never a fleet on the top tier; a failed thinking task is never retried on a cheaper model.
@@ -47,8 +47,8 @@ This repository runs an agentic pipeline under `.ai/`. Read `.ai/AGENTS.md` firs
47
47
 
48
48
  <!-- stub: scope=project runtime=* -->
49
49
  - Verify before reporting done: `verify_command` from `.ai/policies/testing.md` once, to the end, then `e2e_command` once; every failure fixed as one batch; show the output.
50
- - No agent merges or deploys; commit, push, approval only as the dialog picks, else a human outside. When a review flags the same mistake twice, the correction goes into this file.
51
- - Files, not chat, carry decisions: `.ai/reports/<task-id>/task.md` (answer by filling `[Answer]:`), `.ai/state/handoff.md` (read first when resuming), `docs/sdlc/constitution.md` (this project's principles).
50
+ - No agent merges or deploys; commit, push, approval only as the dialog picks and the harness records, else a human outside. When a review flags the same mistake twice, the correction goes into this file.
51
+ - Files, not chat, carry decisions: `.ai/reports/<task-id>/task.md` (answer by filling `[Answer]:`), `.ai/state/handoff.md` (read first when resuming), `docs/sdlc/constitution.md`.
52
52
 
53
53
  <!-- stub: scope=skeleton,skeleton-sdlc runtime=* -->
54
54
  # {{PROJECT}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "riskrails",
3
- "version": "3.0.1",
3
+ "version": "3.2.1",
4
4
  "description": "RiskRails: a weight-based engineering pipeline for Claude Code and Codex — scoped steps, enforcement hooks, adversarial review, model routing.",
5
5
  "keywords": ["claude-code", "codex", "sdlc", "agents", "guardrails", "code-review"],
6
6
  "homepage": "https://github.com/vanssata/claude-agentic-sdlc-module#readme",
package/skills/AGENTS.md CHANGED
@@ -2,5 +2,5 @@
2
2
  # Skills rules
3
3
  - Weigh a change by what it does to a user project's decisions, never by the skill being a process (constitution C6). The mode is what W gives (`state.py weight`); the rows below set the behaviour axis.
4
4
  - behaviour 0: skill or agent text, `usage-report`, no `state.py` refusal moves. behaviour 1: adds a `state.py` check, a sensor or a refusal. behaviour 2: weakens a refusal, changes the approval path (`approve`, `questions --sync`, `AI_UNATTENDED`, the review verdict transitions) or changes install and migration (the `project-update` merge; a migration also raises reversibility through its hint) — declare it, `--declare behaviour=2:"<the effect>"`. Start such a task with `start --mode full --by <name>`: its weight lands in guarded, and these effects keep a STRONG review and the gate.
5
- - A change to a shipped template or to `ai/` runs `tools/build-template-history.py` in the same task, or `tests/test-project-update.sh` goes red.
5
+ - A change to a shipped template or to `ai/` runs `tools/build-template-history.py` in the same task, or `tests/test_project_update.py` (`test_history_covers_every_template`) goes red.
6
6
  <!-- riskrails:rule:skills:end -->
package/skills/CLAUDE.md CHANGED
@@ -2,5 +2,5 @@
2
2
  # Skills rules
3
3
  - Weigh a change by what it does to a user project's decisions, never by the skill being a process (constitution C6). The mode is what W gives (`state.py weight`); the rows below set the behaviour axis.
4
4
  - behaviour 0: skill or agent text, `usage-report`, no `state.py` refusal moves. behaviour 1: adds a `state.py` check, a sensor or a refusal. behaviour 2: weakens a refusal, changes the approval path (`approve`, `questions --sync`, `AI_UNATTENDED`, the review verdict transitions) or changes install and migration (the `project-update` merge; a migration also raises reversibility through its hint) — declare it, `--declare behaviour=2:"<the effect>"`. Start such a task with `start --mode full --by <name>`: its weight lands in guarded, and these effects keep a STRONG review and the gate.
5
- - A change to a shipped template or to `ai/` runs `tools/build-template-history.py` in the same task, or `tests/test-project-update.sh` goes red.
5
+ - A change to a shipped template or to `ai/` runs `tools/build-template-history.py` in the same task, or `tests/test_project_update.py` (`test_history_covers_every_template`) goes red.
6
6
  <!-- riskrails:rule:skills:end -->
@@ -31,7 +31,7 @@ Everything else loads on demand; nothing here is worth reading twice.
31
31
  ## Shared files
32
32
 
33
33
  This `.ai/` holds `project/`, `rules/`, `state/`, `reports/`, `VERSION` and, in
34
- `policies/`, `risk-tiers.json`, `risk-tiers.md`, `path-guard.json`, `testing.md`,
34
+ `policies/`, `weight.json`, `weight.md`, `path-guard.json`, `testing.md`,
35
35
  `coding.md`, `safety.md` and `security.md`. Every other path below ships with the
36
36
  plugin at `<home>/riskrails/ai/<path>`; `<home>` is `~/.claude`, or
37
37
  `$CODEX_HOME` (default `~/.codex`). A file at the same path here overrides it and
@@ -1 +1 @@
1
- 10
1
+ 11
@@ -57,12 +57,18 @@ it, add its pattern to `deny_patterns` (a `dependency_instruction_patterns`
57
57
  list from an older `path-guard.json` is read as `deny_patterns`).
58
58
 
59
59
  Always read-only for an agent: the task's state under `.ai/state/`, its
60
- `task.md` and `events.jsonl`, `.ai/policies/*.json`, and the installed guards —
60
+ `task.md` and `events.jsonl`, `.ai/policies/*.json`, the compact
61
+ record under `.ai/memory/` and `.ai/local/memory/` (written by `state.py` alone),
62
+ and the installed guards —
61
63
  their hooks, engine and `skills/ai-task/` under `.claude/` and `.codex/`. While a
62
64
  task is in flight the runtime's settings and this project's `.ai/policies/` are
63
65
  read-only too. Changing the rules a run is judged by, inside that run, is not a
64
66
  change an agent may make on its own.
65
67
 
68
+ A review's evidence for a closed task is its record — `state.py memory show <id>`
69
+ and `recall` — once cleanup has moved the report directory; an approval in the
70
+ record authorises nothing.
71
+
66
72
  Project-specific additions go in `.ai/policies/path-guard.json`
67
73
  (`deny_patterns`, `allow_patterns`, `protected_config_patterns`,
68
74
  `task_protected_patterns`; allow wins over all of them). When a deny is wrong, widen the allow list in that file — do not
@@ -47,13 +47,15 @@ else runs in between.
47
47
  which, in one line; silence is not a verdict.
48
48
  - A failure in a step's own tests is fixed in that step, before `step-done`;
49
49
  the batch rule below is for the two end-of-task runs.
50
+ - After a green suite, a documents-only edit runs `doc_tests` (step scope), not the suite.
50
51
 
51
52
  ## Reading a failure
52
53
 
53
54
  **Run to the end, then fix as one batch.** The verification command runs with
54
55
  no fail-fast flag and is never interrupted at the first failure. Collect every
55
- failure, classify each one, fix all the new regressions in a single remediation
56
- step (`state.py remediate`), and run the command once more. One failure never
56
+ failure, classify each one, fix all the new regressions in one batch
57
+ (`state.py start --amend` widens the task when a fix needs more files), and run the
58
+ command once more. One failure never
57
59
  sends the task back to the start: the plan, the tier and the finished steps
58
60
  stand. At most two remediation rounds; a third means the human decides.
59
61
 
@@ -66,6 +68,9 @@ Classify before fixing. The four answers are:
66
68
  is missing.
67
69
  - **UNKNOWN** — you could not tell. Say what you tried.
68
70
 
71
+ A review's MEDIUM and LOW findings are recorded with `state.py memory risk add`, not
72
+ fixed in the task; only a BLOCKER, a HIGH or a regression buys an `--after-fix` run.
73
+
69
74
  **Never edit a test to make it pass when production behaviour changed
70
75
  unexpectedly.** A test that suddenly disagrees with the code is evidence, and
71
76
  deleting evidence is the worst available option.
@@ -88,6 +93,7 @@ verify_command: # the full fast suite, e2e excluded — e.g. make test |
88
93
  step_test_command: # the same runner, scoped to a path or filter — e.g. vendor/bin/phpunit {files} | pytest {files}
89
94
  e2e_command: # the end-to-end suite, run once at the end of a task — e.g. make e2e | vendor/bin/behat | npx playwright test
90
95
  lint_command: # e.g. vendor/bin/php-cs-fixer fix --dry-run | ruff check . | npm run lint — write `none` if the project has none
96
+ doc_tests: # the suites a documents-only edit runs after a green suite — e.g. tests/test_docs.py; `none` if the project has none
91
97
  typecheck_command: # e.g. vendor/bin/phpstan analyse --no-progress | mypy . | npx tsc --noEmit — `none` if the project has none
92
98
  healthy_output: # two or three lines of what a green run ends with
93
99
  runtime: # roughly, for each command, so a hang is recognisable
@@ -99,12 +105,19 @@ step names; without it the paths are appended. The placeholder is what lets
99
105
  `sensors.py` run a step's own tests for the "test must bite" check, so a runner
100
106
  that only takes a `--filter` gets `bite: unavailable` rather than a wrong answer.
101
107
 
108
+ `bite_codes` (optional) lists the exit codes that mean "a test failed" when a
109
+ test is shown failing before its fix; the default is `1`. Runners that exit
110
+ differently say so here — `bite_codes: 1,2` for pytest (2 is a collection
111
+ error, such as a missing import), `1,2` for phpunit, `101` for cargo. Any other
112
+ code is the runner's own usage error and does not count as a bite.
113
+
102
114
  `lint_command` and `typecheck_command` are read by the deterministic sensors,
103
- never by a model. They decide, together with the test run, whether a T2 change
104
- can skip its model review (`risk-tiers.json`, `sensors`). A **missing** line is
115
+ never by a model. They decide, together with the test run, whether a light-mode change
116
+ can skip its model review (`state.py close` measures them). A **missing** line is
105
117
  `unavailable` and keeps the review; only the literal `none`, written by a human,
106
- says the project has no such tool. `sensors.py detect` proposes the exact line
107
- from the config files it finds, and never runs what it detected.
118
+ says the project has no such tool. Read the exact line from the project's
119
+ config files (`package.json`, `composer.json`, `pyproject.toml`, `Makefile`), and
120
+ never run a command just to find it out.
108
121
 
109
122
  If the project has no e2e suite, write `e2e_command: none` and say so — an
110
123
  empty line is read as "not written down yet" and the next task will go looking
@@ -1,12 +1,12 @@
1
1
  # Task state
2
2
 
3
3
  `state.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
4
+ written by `skills/ai-task/state.py` (atomically), read by `/rr-status`, and
5
5
  enforced by `ai-scope-guard`, which derives the files the current step may touch
6
6
  from it.
7
7
 
8
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
9
+ `/rr-setup --update` may change the state through `ctx.patch_state()`, with the
10
10
  same atomic write and a `schema_migrated` history entry — that is how a task in
11
11
  flight survives a change to the shape of this file. And `hooks/context-guard.py`
12
12
  writes `session.json`, the sidecar beside this file, and nothing else: it never
@@ -14,7 +14,7 @@ touches `state.json`. Nothing else writes any of it — an agent never edits
14
14
  them by hand, and the path guard refuses when one tries.
15
15
 
16
16
  It is **not committed**: it describes a session, not the repository. The
17
- `.gitignore` entry added by `/ai-init` keeps it out of git.
17
+ `.gitignore` entry added by `/rr-setup` keeps it out of git.
18
18
 
19
19
  When a task closes, `state.py close` writes the final state to
20
20
  `.ai/state/logs/<task-id>/state.json`, beside the task's verify logs — local,
@@ -37,7 +37,7 @@ created_at, updated_at, history[]
37
37
  ```
38
38
 
39
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
40
+ not run `/rr-setup --update` yet still answers every command; `save()` then writes
41
41
  them. A reader that opens this file directly sees what is on disk, not those
42
42
  defaults.
43
43
 
@@ -8,6 +8,6 @@ This repository runs an agentic pipeline under `.ai/`. Read `.ai/AGENTS.md` firs
8
8
  - A change runs through `/rr-build <request>`; `/rr-status` shows where it stands. Each step names the files it may touch — an edit outside them is refused: answer `SCOPE_CHANGE_REQUIRED`. One `apply_patch` is checked file by file: a patch that reaches outside the step is refused whole.
9
9
 
10
10
  - Verify before reporting done: `verify_command` from `.ai/policies/testing.md` once, to the end, then `e2e_command` once; every failure fixed as one batch; show the output.
11
- - No agent merges or deploys; commit, push, approval only as the dialog picks, else a human outside. When a review flags the same mistake twice, the correction goes into this file.
12
- - Files, not chat, carry decisions: `.ai/reports/<task-id>/task.md` (answer by filling `[Answer]:`), `.ai/state/handoff.md` (read first when resuming), `docs/sdlc/constitution.md` (this project's principles).
11
+ - No agent merges or deploys; commit, push, approval only as the dialog picks and the harness records, else a human outside. When a review flags the same mistake twice, the correction goes into this file.
12
+ - Files, not chat, carry decisions: `.ai/reports/<task-id>/task.md` (answer by filling `[Answer]:`), `.ai/state/handoff.md` (read first when resuming), `docs/sdlc/constitution.md`.
13
13
  <!-- riskrails:end -->
@@ -8,6 +8,6 @@ This repository runs an agentic pipeline under `.ai/`. Read `.ai/AGENTS.md` firs
8
8
  - A change runs through `/rr-build <request>`; `/rr-status` shows where it stands. Each step names the files it may touch — an edit outside them is refused: answer `SCOPE_CHANGE_REQUIRED`.
9
9
 
10
10
  - Verify before reporting done: `verify_command` from `.ai/policies/testing.md` once, to the end, then `e2e_command` once; every failure fixed as one batch; show the output.
11
- - No agent merges or deploys; commit, push, approval only as the dialog picks, else a human outside. When a review flags the same mistake twice, the correction goes into this file.
12
- - Files, not chat, carry decisions: `.ai/reports/<task-id>/task.md` (answer by filling `[Answer]:`), `.ai/state/handoff.md` (read first when resuming), `docs/sdlc/constitution.md` (this project's principles).
11
+ - No agent merges or deploys; commit, push, approval only as the dialog picks and the harness records, else a human outside. When a review flags the same mistake twice, the correction goes into this file.
12
+ - Files, not chat, carry decisions: `.ai/reports/<task-id>/task.md` (answer by filling `[Answer]:`), `.ai/state/handoff.md` (read first when resuming), `docs/sdlc/constitution.md`.
13
13
  <!-- riskrails:end -->
@@ -8,6 +8,6 @@ This repository runs an agentic pipeline under `.ai/`. Read `.ai/AGENTS.md` firs
8
8
  - Changes run through the pipeline (`/rr-build` in Claude Code or Codex); without it, follow `.ai/policies/safety.md`, touch only the files the current task names, and answer questions in prose or by filling the file.
9
9
 
10
10
  - Verify before reporting done: `verify_command` from `.ai/policies/testing.md` once, to the end, then `e2e_command` once; every failure fixed as one batch; show the output.
11
- - No agent merges or deploys; commit, push, approval only as the dialog picks, else a human outside. When a review flags the same mistake twice, the correction goes into this file.
12
- - Files, not chat, carry decisions: `.ai/reports/<task-id>/task.md` (answer by filling `[Answer]:`), `.ai/state/handoff.md` (read first when resuming), `docs/sdlc/constitution.md` (this project's principles).
11
+ - No agent merges or deploys; commit, push, approval only as the dialog picks and the harness records, else a human outside. When a review flags the same mistake twice, the correction goes into this file.
12
+ - Files, not chat, carry decisions: `.ai/reports/<task-id>/task.md` (answer by filling `[Answer]:`), `.ai/state/handoff.md` (read first when resuming), `docs/sdlc/constitution.md`.
13
13
  <!-- riskrails:end -->
@@ -2,4 +2,5 @@
2
2
  .ai/state/*.json
3
3
  .ai/state/handoff.md
4
4
  .ai/state/*.tmp
5
+ .ai/state/*.jsonl
5
6
  .ai/local/
@@ -8,6 +8,6 @@ This repository runs an agentic pipeline under `.ai/`. Read `.ai/AGENTS.md` firs
8
8
  - Changes run through the pipeline (`/rr-build` in Claude Code or Codex); without it, follow `.ai/policies/safety.md`, touch only the files the current task names, and answer questions in prose or by filling the file.
9
9
 
10
10
  - Verify before reporting done: `verify_command` from `.ai/policies/testing.md` once, to the end, then `e2e_command` once; every failure fixed as one batch; show the output.
11
- - No agent merges or deploys; commit, push, approval only as the dialog picks, else a human outside. When a review flags the same mistake twice, the correction goes into this file.
12
- - Files, not chat, carry decisions: `.ai/reports/<task-id>/task.md` (answer by filling `[Answer]:`), `.ai/state/handoff.md` (read first when resuming), `docs/sdlc/constitution.md` (this project's principles).
11
+ - No agent merges or deploys; commit, push, approval only as the dialog picks and the harness records, else a human outside. When a review flags the same mistake twice, the correction goes into this file.
12
+ - Files, not chat, carry decisions: `.ai/reports/<task-id>/task.md` (answer by filling `[Answer]:`), `.ai/state/handoff.md` (read first when resuming), `docs/sdlc/constitution.md`.
13
13
  <!-- riskrails:end -->
@@ -0,0 +1 @@
1
+ """rr_state: the task state machine behind skills/ai-task/state.py."""
@@ -0,0 +1,210 @@
1
+ """cli: the argument parser and the dispatch -- the one place that resolves the runtime and builds the Runtime every command receives.
2
+
3
+ shell: BLOCKING_COMMANDS, MUTATING_COMMANDS, DIALOG_SYNC_COMMANDS, memory_parsers, main
4
+ """
5
+ import argparse
6
+ import os
7
+
8
+ from rr_state import close, dialog, dialog_cmd, fs, gate, gate_cmd, memory_cmd, policy, questions_cmd
9
+ from rr_state import report_cmd, review, start, steps, task, verify
10
+ from rr_state import runtime as rr_runtime
11
+
12
+ # A pending question stops a stage from moving; it never stops a command that
13
+ # records, reads or answers (R3). close --abandon is exempt: abandoning a task is
14
+ # how an unanswerable question is closed.
15
+ BLOCKING_COMMANDS = {"step-done", "approve", "close", "verify"}
16
+
17
+ # The commands that change the task. Only these take ownership of it: reading a
18
+ # task from the other runtime, or writing a note about it, is not a handoff.
19
+ MUTATING_COMMANDS = {"step-done", "ask", "answer", "close", "start", "verify", "review", "batch"}
20
+ # What prunes decision.json and mirrors consent.jsonl first (R36.14, R36.24):
21
+ # every command that changes something; status and consent stay read-only.
22
+ DIALOG_SYNC_COMMANDS = MUTATING_COMMANDS | {"approve", "reject", "decision"}
23
+
24
+
25
+ def memory_parsers(sub):
26
+ p = sub.add_parser("recall", help="what the record holds about these files, within a token budget")
27
+ p.add_argument("--files", default=""); p.add_argument("--budget", type=int)
28
+ p.add_argument("--kinds", default=""); p.add_argument("--all-risks", dest="all_risks", action="store_true")
29
+ p.add_argument("--offset", type=int, default=0); p.add_argument("--json", action="store_true")
30
+ p.set_defaults(func=memory_cmd.cmd_recall)
31
+ p = sub.add_parser("memory", help="the compact project record (docs/local-memory.md)")
32
+ p.set_defaults(func=memory_cmd.cmd_memory)
33
+ msub = p.add_subparsers(dest="memory_action", required=True)
34
+ m = msub.add_parser("status"); m.add_argument("--json", action="store_true")
35
+ m = msub.add_parser("enable"); m.add_argument("--author"); m.add_argument("--period", choices=["month", "quarter", "year"])
36
+ m.add_argument("--index", choices=["none", "sqlite"])
37
+ msub.add_parser("disable")
38
+ m = msub.add_parser("export"); m.add_argument("ids", nargs="*"); m.add_argument("--pending", action="store_true")
39
+ m = msub.add_parser("verify"); m.add_argument("ids", nargs="*"); m.add_argument("--all", action="store_true")
40
+ m.add_argument("--range"); m.add_argument("--fresh-clone", dest="fresh_clone", action="store_true")
41
+ m.add_argument("--json", action="store_true")
42
+ m = msub.add_parser("show"); m.add_argument("id"); m.add_argument("--md", action="store_true")
43
+ m.add_argument("--json", action="store_true"); m.add_argument("--entry")
44
+ m = msub.add_parser("search"); m.add_argument("query", nargs="?", default="")
45
+ for flag in ("--kind", "--by", "--file", "--task", "--since", "--until"):
46
+ m.add_argument(flag)
47
+ m.add_argument("--limit", type=int, default=20); m.add_argument("--offset", type=int, default=0)
48
+ m.add_argument("--json", action="store_true")
49
+ m = msub.add_parser("risk"); rsub = m.add_subparsers(dest="risk_action", required=True)
50
+ r = rsub.add_parser("add"); r.add_argument("--title", required=True); r.add_argument("--where", default="")
51
+ r.add_argument("--label", required=True); r.add_argument("--what", required=True)
52
+ r.add_argument("--likely"); r.add_argument("--fix"); r.add_argument("--status", default="open")
53
+ r = rsub.add_parser("close"); r.add_argument("rkey"); r.add_argument("--status", required=True)
54
+ r.add_argument("--why", required=True)
55
+ m = msub.add_parser("risks"); m.add_argument("--status", choices=["open", "ticketed", "accepted", "fixed", "all"],
56
+ default="open")
57
+ m.add_argument("--md", action="store_true"); m.add_argument("--json", action="store_true")
58
+ m = msub.add_parser("migrate"); g = m.add_mutually_exclusive_group(required=True)
59
+ g.add_argument("--report", action="store_true"); g.add_argument("--apply", action="store_true")
60
+ g.add_argument("--move-logs", dest="move_logs", action="store_true")
61
+ m.add_argument("--risks", action="store_true"); m.add_argument("--confirm-delete", dest="confirm_delete")
62
+ msub.add_parser("rebuild")
63
+ m = msub.add_parser("cleanup"); m.add_argument("ids", nargs="*"); m.add_argument("--all", action="store_true")
64
+ m.add_argument("--confirm-delete", dest="confirm_delete")
65
+ m = msub.add_parser("restore"); m.add_argument("ids", nargs="*"); m.add_argument("--all", action="store_true")
66
+ m = msub.add_parser("pack"); m.add_argument("--period", required=True)
67
+ m.add_argument("--confirm-delete", dest="confirm_delete")
68
+ m = msub.add_parser("redact"); m.add_argument("rid"); m.add_argument("--field", required=True)
69
+ m.add_argument("--why", required=True)
70
+
71
+
72
+ def main():
73
+ # description: the first line of state.py's docstring, which argparse printed as __doc__ before the split
74
+ parser = argparse.ArgumentParser(
75
+ description="Task state for /ai-task — the small, machine-readable record of one task.")
76
+ parser.add_argument("--root", default=".", help="project directory (default: search upwards for .ai/)")
77
+ parser.add_argument("--runtime", choices=rr_runtime.RUNTIMES,
78
+ help="overrides AI_RUNTIME and detection for this call")
79
+ sub = parser.add_subparsers(dest="command", required=True)
80
+
81
+ p = sub.add_parser("step-done"); p.add_argument("step_id")
82
+ p.add_argument("--force", action="store_true",
83
+ help="accepted for older callers; it forces nothing")
84
+ p.set_defaults(func=steps.cmd_step_done_v7)
85
+
86
+ who = os.environ.get("USER") or "unknown"
87
+ p = sub.add_parser("ask"); p.add_argument("question", nargs="?")
88
+ p.add_argument("--option", action="append", default=[]); p.add_argument("--recommend")
89
+ p.add_argument("--context", default=""); p.add_argument("--gate")
90
+ p.add_argument("--batch"); p.add_argument("--topic"); p.add_argument("--by", default="")
91
+ p.set_defaults(func=questions_cmd.cmd_ask)
92
+
93
+ p = sub.add_parser("answer"); p.add_argument("assignment", nargs="*")
94
+ p.add_argument("--prose"); p.add_argument("--by", default=who)
95
+ # `file` is not offered: it is what --sync writes, and it is the one route
96
+ # that is recorded as a human's.
97
+ p.add_argument("--via", choices=["picker", "prose"], default="prose")
98
+ p.add_argument("--topic"); p.set_defaults(func=questions_cmd.cmd_answer)
99
+
100
+ p = sub.add_parser("questions"); p.add_argument("--pending", action="store_true")
101
+ p.add_argument("--sync", action="store_true"); p.add_argument("--topic")
102
+ p.add_argument("--by", default=who)
103
+ p.add_argument("--format", choices=["md", "prose", "json"], default="prose")
104
+ p.set_defaults(func=questions_cmd.cmd_questions)
105
+
106
+ p = sub.add_parser("handoff")
107
+ p.add_argument("--print", dest="print_it", action="store_true")
108
+ p.add_argument("--reason", choices=["stage", "precompact", "manual", "session-start"],
109
+ default="manual")
110
+ p.add_argument("--to", choices=rr_runtime.RUNTIMES)
111
+ p.add_argument("--for", dest="for_", choices=["continue", "review"], default="continue")
112
+ p.add_argument("--why", default="")
113
+ p.set_defaults(func=report_cmd.cmd_handoff)
114
+
115
+ p = sub.add_parser("profile"); p.add_argument("action", nargs="?", choices=["detect", "set", "ack"])
116
+ p.add_argument("plan", nargs="?")
117
+ p.add_argument("--field")
118
+ p.add_argument("--runtime", dest="detect_runtime", choices=rr_runtime.RUNTIMES)
119
+ p.add_argument("--open", action="store_true")
120
+ p.add_argument("--tier", choices=["FAST", "BALANCED", "STRONG", "EXPERT"])
121
+ p.add_argument("--other", action="store_true"); p.add_argument("--json", action="store_true")
122
+ p.set_defaults(func=report_cmd.cmd_profile)
123
+
124
+ p = sub.add_parser("weight"); p.add_argument("--files", default="")
125
+ p.add_argument("--goal", default=""); p.add_argument("--declare", action="append", default=[])
126
+ out = p.add_mutually_exclusive_group()
127
+ out.add_argument("--json", action="store_true"); out.add_argument("--long", action="store_true")
128
+ out.add_argument("--mirror", action="store_true"); p.set_defaults(func=start.cmd_weight)
129
+
130
+ p = sub.add_parser("event"); p.add_argument("type"); p.add_argument("--detail", default="")
131
+ p.add_argument("--data"); p.set_defaults(func=report_cmd.cmd_event)
132
+
133
+ p = sub.add_parser("approve"); p.add_argument("--by", required=True)
134
+ p.add_argument("--note", default="")
135
+ p.add_argument("--confirm", default="", help="the operation a full_irreversible task performs")
136
+ p.set_defaults(func=gate_cmd.cmd_approve_v7)
137
+
138
+ p = sub.add_parser("reject"); p.add_argument("--by", required=True)
139
+ p.add_argument("--why", required=True); p.set_defaults(func=gate_cmd.cmd_reject)
140
+
141
+ p = sub.add_parser("close")
142
+ p.add_argument("--abandon", action="store_true", help="archive without the checks")
143
+ p.set_defaults(func=close.cmd_close_v7)
144
+
145
+ p = sub.add_parser("start"); p.add_argument("--goal", default="")
146
+ p.add_argument("--workflow", default=""); p.add_argument("--files", default="")
147
+ p.add_argument("--tests", default=""); p.add_argument("--declare", action="append", default=[])
148
+ p.add_argument("--mode"); p.add_argument("--by", default=""); p.add_argument("--why", default="")
149
+ p.add_argument("--steps", default=""); p.add_argument("--plan-file", dest="plan_file")
150
+ p.add_argument("--task-id", help="an id of your own; a legacy T-YYYY-MM-DD-NNN id is still accepted")
151
+ p.add_argument("--slug", help="3-24 characters of [a-z0-9] words joined by '-': the id becomes "
152
+ "T-YYYY-MM-DD-HHMM-<slug> (UTC). Default: derived from --goal")
153
+ p.add_argument("--amend", action="store_true")
154
+ p.add_argument("--batch", action="store_true", help="the batch loop of /rr-build-all (needs --by)")
155
+ p.add_argument("--no-batch", dest="no_batch", action="store_true", help="opt out of weight.json batch.default")
156
+ p.set_defaults(func=start.cmd_start)
157
+
158
+ p = sub.add_parser("steps"); p.add_argument("--ready", action="store_true")
159
+ p.add_argument("--json", action="store_true"); p.set_defaults(func=steps.cmd_steps)
160
+
161
+ p = sub.add_parser("batch"); p.add_argument("action", choices=["on", "off"])
162
+ p.add_argument("--by", default=""); p.add_argument("--why", default="")
163
+ p.set_defaults(func=steps.cmd_batch)
164
+
165
+ p = sub.add_parser("verify")
166
+ p.add_argument("--scope", required=True, choices=["step", "suite", "e2e", "all"])
167
+ p.add_argument("--files", default=""); p.add_argument("--expect", choices=["fail"])
168
+ p.add_argument("--e2e", nargs=2, metavar=("not-applicable", "WHY"))
169
+ p.add_argument("--after-fix", dest="after_fix", default="")
170
+ p.add_argument("--by", default=""); p.set_defaults(func=verify.cmd_verify)
171
+
172
+ p = sub.add_parser("review"); p.add_argument("--verdict", choices=sorted(policy.VERDICT_STATUS))
173
+ p.add_argument("--tier", choices=review.REVIEW_TIERS); p.add_argument("--focus", choices=["security"])
174
+ p.add_argument("--report", default=""); p.add_argument("--closes", default="")
175
+ p.add_argument("--by", default=""); p.set_defaults(func=review.cmd_review)
176
+
177
+ p = sub.add_parser("status"); p.add_argument("--json", action="store_true")
178
+ p.set_defaults(func=report_cmd.cmd_status)
179
+
180
+ p = sub.add_parser("decision"); p.add_argument("action", choices=["show", "open", "settle", "fallback"])
181
+ p.add_argument("--id"); p.add_argument("--kind"); p.add_argument("--command", dest="commands", action="append", default=[])
182
+ p.add_argument("--pick", type=int); p.add_argument("--why", default="")
183
+ p.add_argument("--json", action="store_true"); p.set_defaults(func=dialog_cmd.cmd_decision)
184
+
185
+ p = sub.add_parser("consent"); p.add_argument("action", nargs="?", choices=["audit"])
186
+ p.add_argument("--json", action="store_true"); p.set_defaults(func=dialog_cmd.cmd_consent)
187
+
188
+ memory_parsers(sub)
189
+
190
+ args = parser.parse_args()
191
+ if getattr(args, "topic", None):
192
+ # Topic mode never touches .ai/: it must work where there is none (I11).
193
+ args.func(args, fs.find_docs_root(args.root), rt=rr_runtime.Runtime())
194
+ return
195
+ if args.command == "profile":
196
+ # Read-only and task-free: it answers where there is no .ai/ at all.
197
+ name = args.runtime or os.environ.get("AI_RUNTIME") or \
198
+ ("claude" if os.environ.get("CLAUDECODE") else "unknown")
199
+ args.func(args, None, rt=rr_runtime.Runtime(name=name))
200
+ return
201
+ root = fs.find_root(args.root)
202
+ rt = rr_runtime.Runtime(name=rr_runtime.detect_runtime(args.runtime, root),
203
+ mutating=args.command in MUTATING_COMMANDS)
204
+ task.route_surface(root, args, rt=rt)
205
+ if args.command in BLOCKING_COMMANDS and not getattr(args, "abandon", False):
206
+ gate.guard_pending(root, args.command, rt=rt)
207
+ # After the guard: an exit-4 refusal leaves every file as it was.
208
+ if args.command in DIALOG_SYNC_COMMANDS:
209
+ dialog.sync_dialog(root, rt=rt)
210
+ args.func(args, root, rt=rt)