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
@@ -7,7 +7,7 @@ C1. Production behaviour is the source of truth; what looks wrong may be load-be
7
7
  C2. A bugfix starts with the failing test; legacy behaviour gets a characterization test before it changes.
8
8
  C3. A refactoring changes no behaviour and is never mixed with a feature.
9
9
  C4. Every step names its files; scope grows only by amending the plan.
10
- C5. No agent merges or deploys; an agent commits, pushes or approves only by running, verbatim and once, a command a human picked in the dialog; otherwise a human runs it outside the agent.
10
+ C5. No agent merges or deploys; an agent commits, pushes or approves only by running, verbatim and once, a command a human picked in the dialog, as the harness recorded; else a human runs it outside.
11
11
  C6. <!-- project principle — filled by /ai-init from the survey, confirmed by you -->
12
12
  C7. <!-- project principle -->
13
13
  C8. <!-- project principle -->
@@ -1,24 +1,30 @@
1
1
  # SDLC artefacts
2
2
 
3
- Feature work in this repo follows the AI-Native SDLC flow: **intent → spec → plan → build**.
4
- Each stage has a skill that produces one file here, and the next stage reads it.
3
+ Work that needs a written intent and design before any code follows
4
+ **spec → plan → build**. Each stage has a skill that produces one file here,
5
+ and the next stage reads it.
5
6
 
6
7
  ```
7
- /sdlc-intent <topic> → docs/sdlc/intent/<slug>.md (problem, outcome, constraints)
8
+ /rr-spec <idea | intent/<slug>.md> → docs/sdlc/specs/<slug>.md (## Intent, requirements, design, Flagged concerns)
9
+ │ docs/sdlc/intent/<slug>.questions.md (the brainstorm, recorded by state.py)
10
+ /rr-plan specs/<slug>.md → docs/sdlc/plans/<slug>.md (files that change, order, risks, proof, rollback)
8
11
  │
9
- /sdlc-spec intent/<slug>.md → docs/sdlc/specs/<slug>.md (requirements, design, flagged concerns)
10
- │
11
- /sdlc-plan specs/<slug>.md → docs/sdlc/plans/<slug>.md (files, order, risks, proof)
12
- │
13
- build (main session, one plan step at a time; `ai-reviewer` before commit)
12
+ /rr-build — one row at a time (each row is one task, weighed and started by /rr-plan)
13
+ /rr-build-all plans/<slug>.md — the whole plan as one task: parallel implementers, one review loop
14
14
  ```
15
15
 
16
16
  | Folder | Produced by | Contains |
17
17
  |---|---|---|
18
- | `intent/` | `/sdlc-intent` | one file per idea; what and why, not how |
19
- | `specs/` | `/sdlc-spec` | requirements + design; ends with **Flagged concerns** |
20
- | `plans/` | `/sdlc-plan` | ordered implementation steps with verification |
21
- | `adr/` | you / `architect` | Architecture Decision Records (MADR style) |
18
+ | `intent/` | `/rr-spec`'s brainstorm (`state.py ask --topic`) | `<slug>.questions.md`, one question at a time with the answer on record; an `intent/<slug>.md` written by hand is accepted instead of the brainstorm |
19
+ | `specs/` | `/rr-spec`, `architect` on STRONG | `## Intent` (problem, outcome, affected users and systems, constraints, decisions taken), requirements, design; ends with **Flagged concerns**, which a human resolves or accepts before planning |
20
+ | `plans/` | `/rr-plan` | Files that change / Order of work / Risks / Proof (every requirement to a test or command) / Rollback; each row carries its files and its *Consumes* line |
21
+ | `adr/` | you, or `architect` | Architecture Decision Records (MADR style) |
22
+ | `constitution.md` | `/rr-setup`, then you | the principles the project does not negotiate, cited as `C<n>` in specs, plans and reviews; at most 15 lines |
22
23
 
23
- Each folder holds a `TEMPLATE.md`; the skills copy it and fill it in. Commit the artefacts with the
24
- code they describe so the reasoning survives the pull request.
24
+ The skills fill in the plugin's templates
25
+ (`$AI_HOME/skills/project-init/templates/{intent,spec,plan,adr}.md`, in the
26
+ installed plugin); to override one, copy it to `docs/sdlc/<folder>/TEMPLATE.md`
27
+ and edit it — the skills read that one first. Commit the artefacts with the code they describe, so the
28
+ reasoning survives the pull request. A task's own record — weight, plan, questions, verify runs, review,
29
+ verdict — is not here but in `.ai/reports/<task-id>/task.md`, written by
30
+ `state.py`.
@@ -32,6 +32,12 @@ def test_mode():
32
32
  return os.environ.get("RISKRAILS_TEST", os.environ.get("CLAUDE_AGENTIC_TEST")) == "1"
33
33
 
34
34
 
35
+ def ai_unattended():
36
+ """AI_UNATTENDED is set when it is non-empty and not 0, false or no (case
37
+ and blanks ignored): one rule in every reader (risk 0587)."""
38
+ return os.environ.get("AI_UNATTENDED", "").strip().lower() not in ("", "0", "false", "no")
39
+
40
+
35
41
  def human_gate():
36
42
  """(passed, via, why) for --confirm-delete. A terminal outside any agent
37
43
  session is a human: via "terminal". Otherwise a launcher that declared the
@@ -44,7 +50,7 @@ def human_gate():
44
50
  markers = [name for name in AGENT_MARKERS if name in os.environ]
45
51
  if os.isatty(0) and not markers:
46
52
  return True, "terminal", ""
47
- if os.environ.get("AI_UNATTENDED"):
53
+ if ai_unattended():
48
54
  return True, "unattended", ""
49
55
  if markers:
50
56
  return False, None, ("this run is inside an agent session (%s %s set); run it in a terminal "
@@ -175,7 +181,7 @@ def load_table(path=MAP_FILE):
175
181
  try:
176
182
  with open(path, encoding="utf-8") as fh:
177
183
  table = json.load(fh)
178
- except (OSError, ValueError) as exc:
184
+ except (OSError, ValueError, RecursionError) as exc:
179
185
  raise TableError("%s: %s" % (path, exc)) from exc
180
186
  tools = table.get("tools")
181
187
  rows = table.get("rows")
@@ -455,7 +461,7 @@ def load_decisions(root):
455
461
  try:
456
462
  with open(os.path.join(root, where, "decisions.json"), encoding="utf-8") as fh:
457
463
  return json.load(fh)
458
- except (OSError, ValueError) as exc:
464
+ except (OSError, ValueError, RecursionError) as exc:
459
465
  raise TableError("%s/decisions.json: %s" % (where, exc)) from exc
460
466
 
461
467
 
@@ -925,7 +931,7 @@ def state_line(root, shipped_block, skeleton_cap, table=None, instruction_files=
925
931
  try:
926
932
  return _state_line(root, shipped_block, skeleton_cap, table, instruction_files, shipped_files)
927
933
  except (TableError, OSError, ValueError, KeyError, TypeError, AttributeError) as exc:
928
- return 1, "adopt state unreadable (%s) — run /project-update --adopt" % exc
934
+ return 1, "adopt state unreadable (%s) — run /rr-setup --adopt" % exc
929
935
 
930
936
 
931
937
  def _state_line(root, shipped_block, skeleton_cap, table, instruction_files, shipped_files):
@@ -943,11 +949,11 @@ def _state_line(root, shipped_block, skeleton_cap, table, instruction_files, shi
943
949
  return 0, "no foreign structure detected"
944
950
  bits = ["%s (%d file(s))" % (t, sum(g.values())) for t, g in adoption.detected.items()]
945
951
  bits += ["%s (split?)" % c.path for c, _ in adoption.candidates]
946
- return 1, "foreign structure detected: %s — run /project-update --adopt" % ", ".join(bits)
952
+ return 1, "foreign structure detected: %s — run /rr-setup --adopt" % ", ".join(bits)
947
953
  try:
948
954
  with open(os.path.join(root, where, "adopt.json"), encoding="utf-8") as fh:
949
955
  record_data = json.load(fh)
950
- except (OSError, ValueError):
956
+ except (OSError, ValueError, RecursionError):
951
957
  return 0, "no foreign structure detected"
952
958
  date = where.rsplit("adopt-", 1)[-1]
953
959
  regenerated = []
@@ -968,13 +974,13 @@ def _state_line(root, shipped_block, skeleton_cap, table, instruction_files, shi
968
974
  if current != src.get("sha"):
969
975
  regenerated.append(src["path"])
970
976
  if regenerated:
971
- return 1, ("foreign files regenerated since the adopt of %s: %s — run /project-update --adopt"
977
+ return 1, ("foreign files regenerated since the adopt of %s: %s — run /rr-setup --adopt"
972
978
  % (date, ", ".join(sorted(regenerated))))
973
979
  checks = record_data.get("checks", {})
974
980
  failed = [name for name, c in checks.items() if c.get("status") not in ("pass", "not_applicable")]
975
981
  if failed or record_data.get("status") == "partial":
976
982
  what = failed[0].replace("_", "-") + " FAIL" if failed else "the apply was interrupted"
977
- return 1, "adoption of %s incomplete: %s — run /project-update --adopt" % (date, what)
983
+ return 1, "adoption of %s incomplete: %s — run /rr-setup --adopt" % (date, what)
978
984
  tools = ", ".join(sorted(record_data.get("tools", {})))
979
985
  pending = sum(1 for s in record_data.get("sources", []) if s.get("cleanup") and safe_rel(s.get("path"))
980
986
  and os.path.isfile(os.path.join(root, s["path"])))
@@ -1306,7 +1312,7 @@ def read_record(root, where):
1306
1312
  try:
1307
1313
  with open(os.path.join(root, where, "adopt.json"), encoding="utf-8") as fh:
1308
1314
  return json.load(fh)
1309
- except (OSError, ValueError):
1315
+ except (OSError, ValueError, RecursionError):
1310
1316
  return None
1311
1317
 
1312
1318
 
@@ -1331,7 +1337,7 @@ def task_in_flight(root):
1331
1337
  try:
1332
1338
  with open(path, encoding="utf-8") as fh:
1333
1339
  state = json.load(fh)
1334
- except (OSError, ValueError):
1340
+ except (OSError, ValueError, RecursionError):
1335
1341
  return None
1336
1342
  stage = state.get("current_stage")
1337
1343
  return None if stage in (None, "done") else "%s at stage %s" % (state.get("task_id", "a task"), stage)
@@ -1340,13 +1346,13 @@ def task_in_flight(root):
1340
1346
  def gates(root, u, planned_writes):
1341
1347
  """R5, in the order a human fixes them. Returns a refusal (reason, how) or None."""
1342
1348
  if not os.path.isdir(os.path.join(root, ".ai")):
1343
- return "the project has no .ai/", "run /ai-init first, then /project-update --adopt"
1349
+ return "the project has no .ai/", "run /rr-setup first, then /rr-setup --adopt"
1344
1350
  dirty = clean_tree(root)
1345
1351
  if dirty is None:
1346
1352
  return "the project is not a git work tree", "run `git init` and commit, so the adopt can be undone"
1347
1353
  running = task_in_flight(root)
1348
1354
  if running:
1349
- return "a task is in flight (%s)" % running, "finish or abandon it with /ai-task first"
1355
+ return "a task is in flight (%s)" % running, "finish or abandon it with /rr-build first"
1350
1356
  outside = [p for p in dirty if not p.startswith(ALLOWED_DIRT) and p not in planned_writes]
1351
1357
  if outside:
1352
1358
  more = " and %d more" % (len(outside) - 3) if len(outside) > 3 else ""
@@ -1356,7 +1362,7 @@ def gates(root, u, planned_writes):
1356
1362
  "commit it or remove it")
1357
1363
  return ("the tree has uncommitted changes: %s%s" % (", ".join(outside[:3]), more), how)
1358
1364
  if u.plain_pending(root, ignore=planned_writes):
1359
- return "the project is behind the installed plugin", "run /project-update --apply first, then adopt"
1365
+ return "the project is behind the installed plugin", "run /rr-setup --update --apply first, then adopt"
1360
1366
  return None
1361
1367
 
1362
1368
 
@@ -1403,7 +1409,8 @@ def write_json(u, root, rel, data):
1403
1409
 
1404
1410
  def apply_run(plan, args, u, skeleton_cap, instruction_files=frozenset(), shipped_files=None):
1405
1411
  """`update.py --adopt --apply`: gates, originals, phased writes, the checks
1406
- on disk, the record. Exit 0, 3 (aborted), 4 (incomplete) or 5 (refused)."""
1412
+ on disk, the record. Exit 0, 2 (the routing table or a migration could not be
1413
+ read), 3 (aborted), 4 (incomplete) or 5 (refused)."""
1407
1414
  root = plan.root
1408
1415
  try:
1409
1416
  table = load_table()
@@ -1413,7 +1420,11 @@ def apply_run(plan, args, u, skeleton_cap, instruction_files=frozenset(), shippe
1413
1420
  tools = [t.strip() for t in args.tool.split(",")] if args.tool else None
1414
1421
  resume_dir, resume = partial_record(root)
1415
1422
  planned_writes = set(resume.get("planned_writes", [])) if resume else set()
1416
- refusal = gates(root, u, planned_writes)
1423
+ try:
1424
+ refusal = gates(root, u, planned_writes)
1425
+ except u.SchemaError as exc: # plain_pending builds the plan: a migration's error is exit 2, as in update.py
1426
+ print("project-update: %s" % exc, file=sys.stderr)
1427
+ return 2
1417
1428
  if refusal:
1418
1429
  return refuse(*refusal)
1419
1430
  plan.report_dir = plan.adopt_report_dir = resume_dir or ".ai/reports/adopt-%s" % today()
@@ -1577,7 +1588,9 @@ def report_md(plan, adoption, record):
1577
1588
  # ---------------------------------------------------------------------------
1578
1589
 
1579
1590
  CLEANUP_HEADING = "## Cleanup"
1580
- REVIEW_LINE = "Run `/project-update --adopt --cleanup` to review; a human confirms the deletion."
1591
+ REVIEW_LINE = "Run `/rr-setup --adopt --cleanup` to review; a human confirms the deletion."
1592
+ # Reports written by v2.0.2-v3.1.0 carry this text; cleanup still replaces it.
1593
+ LEGACY_REVIEW_LINE = "Run `/project-update --adopt --cleanup` to review; a human confirms the deletion."
1581
1594
 
1582
1595
 
1583
1596
  def plan_cleanup(plan, args, u, table, skeleton_cap, instruction_files, shipped_files):
@@ -1589,21 +1602,21 @@ def plan_cleanup(plan, args, u, table, skeleton_cap, instruction_files, shipped_
1589
1602
  def no(reason, how, adoption=None):
1590
1603
  return (reason, how), None, None, None, adoption
1591
1604
  if not os.path.isdir(os.path.join(root, ".ai")):
1592
- return no("the project has no .ai/", "run /ai-init first")
1605
+ return no("the project has no .ai/", "run /rr-setup first")
1593
1606
  where = latest_with(root, "adopt.json")
1594
1607
  record = read_record(root, where) if where else None
1595
1608
  if record is None:
1596
- return no("no adopt record to clean up", "run /project-update --adopt --apply first")
1609
+ return no("no adopt record to clean up", "run /rr-setup --adopt --apply first")
1597
1610
  if args.mode == "coexist" or record.get("mode") == "coexist":
1598
1611
  return no("coexist keeps the foreign files",
1599
- "to move them, run /project-update --adopt --apply (mode migrate), then --cleanup")
1612
+ "to move them, run /rr-setup --adopt --apply (mode migrate), then --cleanup")
1600
1613
  checks = record.get("checks", {})
1601
1614
  if record.get("status") != "applied" or any(c.get("status") != "pass" for c in checks.values()):
1602
1615
  return no("the adopt of %s is not complete (R12)" % where.rsplit("adopt-", 1)[-1],
1603
- "run /project-update --adopt --apply until both checks pass")
1616
+ "run /rr-setup --adopt --apply until both checks pass")
1604
1617
  running = task_in_flight(root)
1605
1618
  if running:
1606
- return no("a task is in flight (%s)" % running, "finish or abandon it with /ai-task first")
1619
+ return no("a task is in flight (%s)" % running, "finish or abandon it with /rr-build first")
1607
1620
  dirty = clean_tree(root)
1608
1621
  if dirty is None:
1609
1622
  return no("the project is not a git work tree", "cleanup deletes only what git can restore")
@@ -1642,7 +1655,7 @@ def plan_cleanup(plan, args, u, table, skeleton_cap, instruction_files, shipped_
1642
1655
  what.append("%d unmapped, %d split(s) waiting" % (len(adoption.unmapped), len(adoption.split)))
1643
1656
  if what:
1644
1657
  return no("the adopt no longer holds on the current tree: %s" % "; ".join(what),
1645
- "run /project-update --adopt to see why, settle it, then --cleanup", adoption)
1658
+ "run /rr-setup --adopt to see why, settle it, then --cleanup", adoption)
1646
1659
  # Only what the adopt finds again as a cleanup source, and only what git
1647
1660
  # can restore: an ignored file skipped from original/ would be gone for good.
1648
1661
  found = {path for path, _t, _tr, _n, cleanup in adoption.sources if cleanup}
@@ -1773,7 +1786,8 @@ def cleanup_report_md(u, root, where, record):
1773
1786
  except OSError:
1774
1787
  text = ""
1775
1788
  text = text.split("\n" + CLEANUP_HEADING + "\n", 1)[0].rstrip("\n") + "\n"
1776
- text = text.replace(REVIEW_LINE, "The deletion is recorded under Cleanup below.")
1789
+ for line in (REVIEW_LINE, LEGACY_REVIEW_LINE):
1790
+ text = text.replace(line, "The deletion is recorded under Cleanup below.")
1777
1791
  c = record["cleanup"]
1778
1792
  runs = list(c.get("history") or []) + [c]
1779
1793
 
@@ -1900,7 +1914,7 @@ def find_proposal(root, cand, stale=False):
1900
1914
  try:
1901
1915
  with open(os.path.join(base, d, name), encoding="utf-8") as fh:
1902
1916
  data = json.load(fh)
1903
- except (OSError, ValueError):
1917
+ except (OSError, ValueError, RecursionError):
1904
1918
  continue
1905
1919
  if isinstance(data, dict) and data.get("source") == cand.path and (stale or data.get("source_sha") == cand.sha):
1906
1920
  return ".ai/reports/%s/%s" % (d, name), data
@@ -0,0 +1,131 @@
1
+ # Testing policy
2
+
3
+ Tests describe behaviour. Coverage percentage is a side effect, not a goal.
4
+
5
+ ## Characterization first
6
+
7
+ Before changing behaviour that is not covered by a test — which is most legacy
8
+ behaviour — write a test that passes against the code **as it is today**. That
9
+ test is the definition of what must not break. Only then change the code.
10
+
11
+ ```
12
+ characterization test → refactor → verify identical behaviour → feature change
13
+ ```
14
+
15
+ Never do those in one step, and never in one commit.
16
+
17
+ ## What to consider, by kind of change
18
+
19
+ | Change touches | Also test |
20
+ |---|---|
21
+ | a shared service | the existing callers, not only the new one |
22
+ | money, tax, fiscal | rounding, currency, negative and zero amounts, refunds |
23
+ | a state machine | every transition that is now reachable, and the ones that must stay unreachable |
24
+ | an async consumer | retry, double delivery, out-of-order delivery, poison message |
25
+ | an external integration | timeout, 500, malformed response, partial write |
26
+ | a write path | idempotency, concurrency, transaction boundaries |
27
+ | authorization | the negative case, for every role |
28
+ | a public API or event | the old shape still works |
29
+
30
+ ## Three scopes, three moments
31
+
32
+ A task runs tests three times at most, and each run has its own scope. Nothing
33
+ else runs in between.
34
+
35
+ | When | What runs | Command |
36
+ |---|---|---|
37
+ | after each implementation step, before `step-done` | **only the tests the plan named for that step** — the files it touched, their direct callers' tests | `step_test_command` |
38
+ | once, after the **last** step | the full fast suite — unit and integration, no e2e | `verify_command` |
39
+ | once, at the **end of the task**, after the full suite is green | the end-to-end suite | `e2e_command` |
40
+
41
+ - A step never runs the full suite, and **never** runs e2e. If the plan named
42
+ no test for a step, nothing runs for it.
43
+ - A step's tests are scoped by path or filter, not by `--stop-on-failure`: the
44
+ scoped run still goes to the end, and its failures are fixed inside that step.
45
+ - The e2e suite runs **once per task**, at the very end, never per step. Skip it
46
+ at T0/T1, and when the change cannot reach a flow the e2e suite covers — say
47
+ which, in one line; silence is not a verdict.
48
+ - A failure in a step's own tests is fixed in that step, before `step-done`;
49
+ the batch rule below is for the two end-of-task runs.
50
+ - After a green suite, a documents-only edit runs `doc_tests` (step scope), not the suite.
51
+
52
+ ## Reading a failure
53
+
54
+ **Run to the end, then fix as one batch.** The verification command runs with
55
+ no fail-fast flag and is never interrupted at the first failure. Collect every
56
+ failure, classify each one, fix all the new regressions in one batch
57
+ (`state.py start --amend` widens the task when a fix needs more files), and run the
58
+ command once more. One failure never
59
+ sends the task back to the start: the plan, the tier and the finished steps
60
+ stand. At most two remediation rounds; a third means the human decides.
61
+
62
+ Classify before fixing. The four answers are:
63
+
64
+ - **EXISTING TEST FAILURE** — it failed before this change too. Say so, do not
65
+ fix it inside this task.
66
+ - **NEW REGRESSION** — this change broke it. Fix the code, not the test.
67
+ - **TEST ENVIRONMENT FAILURE** — database, fixtures, network, container. Say what
68
+ is missing.
69
+ - **UNKNOWN** — you could not tell. Say what you tried.
70
+
71
+ A review's MEDIUM and LOW findings are recorded with `state.py memory risk add`, not
72
+ fixed in the task; only a BLOCKER, a HIGH or a regression buys an `--after-fix` run.
73
+
74
+ **Never edit a test to make it pass when production behaviour changed
75
+ unexpectedly.** A test that suddenly disagrees with the code is evidence, and
76
+ deleting evidence is the worst available option.
77
+
78
+ ## Verification
79
+
80
+ Three commands, one per scope. `verify_command` proves the project is healthy;
81
+ `step_test_command` proves one step is; `e2e_command` proves the whole flow
82
+ still works. Keep each to one command — chain the pieces in a Makefile or
83
+ composer script if there are several — and make sure none of them stops at the
84
+ first failure (`--stop-on-failure`, `-x`, `--bail`, `failfast` are off).
85
+
86
+ `verify_command` must **exclude** the e2e suite: it is the fast loop run after
87
+ the last step and again after a remediation batch. `e2e_command` runs once, at
88
+ the end of the task. A bugfix still shows its new failing test first — that one
89
+ test only, through `single_test`.
90
+
91
+ ```
92
+ verify_command: # the full fast suite, e2e excluded — e.g. make test | composer qa && vendor/bin/phpunit --exclude-group e2e
93
+ step_test_command: # the same runner, scoped to a path or filter — e.g. vendor/bin/phpunit {files} | pytest {files}
94
+ e2e_command: # the end-to-end suite, run once at the end of a task — e.g. make e2e | vendor/bin/behat | npx playwright test
95
+ lint_command: # e.g. vendor/bin/php-cs-fixer fix --dry-run | ruff check . | npm run lint — write `none` if the project has none
96
+ doc_tests: # the suites a documents-only edit runs after a green suite — e.g. tests/test_docs.py; `none` if the project has none
97
+ typecheck_command: # e.g. vendor/bin/phpstan analyse --no-progress | mypy . | npx tsc --noEmit — `none` if the project has none
98
+ healthy_output: # two or three lines of what a green run ends with
99
+ runtime: # roughly, for each command, so a hang is recognisable
100
+ single_test: # how to run one test, for the failing-test-first step of a bugfix
101
+ ```
102
+
103
+ `step_test_command` may contain `{files}`, which is replaced by the paths the
104
+ step names; without it the paths are appended. The placeholder is what lets
105
+ `sensors.py` run a step's own tests for the "test must bite" check, so a runner
106
+ that only takes a `--filter` gets `bite: unavailable` rather than a wrong answer.
107
+
108
+ `bite_codes` (optional) lists the exit codes that mean "a test failed" when a
109
+ test is shown failing before its fix; the default is `1`. Runners that exit
110
+ differently say so here — `bite_codes: 1,2` for pytest (2 is a collection
111
+ error, such as a missing import), `1,2` for phpunit, `101` for cargo. Any other
112
+ code is the runner's own usage error and does not count as a bite.
113
+
114
+ `lint_command` and `typecheck_command` are read by the deterministic sensors,
115
+ never by a model. They decide, together with the test run, whether a light-mode change
116
+ can skip its model review (`state.py close` measures them). A **missing** line is
117
+ `unavailable` and keeps the review; only the literal `none`, written by a human,
118
+ says the project has no such tool. Read the exact line from the project's
119
+ config files (`package.json`, `composer.json`, `pyproject.toml`, `Makefile`), and
120
+ never run a command just to find it out.
121
+
122
+ If the project has no e2e suite, write `e2e_command: none` and say so — an
123
+ empty line is read as "not written down yet" and the next task will go looking
124
+ for it. Say which group, tag or directory marks the e2e tests and how
125
+ `verify_command` excludes them, so a step's scoped run and the fast suite stay
126
+ free of them.
127
+
128
+ ## Project specifics
129
+
130
+ <!-- The real commands: how to run unit tests, integration tests, a single test.
131
+ Where fixtures live. What the CI runs. Cite the config. -->
@@ -0,0 +1,30 @@
1
+ # SDLC artefacts
2
+
3
+ Work that needs a written intent and design before any code follows
4
+ **spec → plan → build**. Each stage has a skill that produces one file here,
5
+ and the next stage reads it.
6
+
7
+ ```
8
+ /rr-spec <idea | intent/<slug>.md> → docs/sdlc/specs/<slug>.md (## Intent, requirements, design, Flagged concerns)
9
+ │ docs/sdlc/intent/<slug>.questions.md (the brainstorm, recorded by state.py)
10
+ /rr-plan specs/<slug>.md → docs/sdlc/plans/<slug>.md (files that change, order, risks, proof, rollback)
11
+ │
12
+ /rr-build — one row at a time (each row is one task, weighed and started by /rr-plan)
13
+ /rr-build-all plans/<slug>.md — the whole plan as one task: parallel implementers, one review loop
14
+ ```
15
+
16
+ | Folder | Produced by | Contains |
17
+ |---|---|---|
18
+ | `intent/` | `/rr-spec`'s brainstorm (`state.py ask --topic`) | `<slug>.questions.md`, one question at a time with the answer on record; an `intent/<slug>.md` written by hand is accepted instead of the brainstorm |
19
+ | `specs/` | `/rr-spec`, `architect` on STRONG | `## Intent` (problem, outcome, affected users and systems, constraints, decisions taken), requirements, design; ends with **Flagged concerns**, which a human resolves or accepts before planning |
20
+ | `plans/` | `/rr-plan` | Files that change / Order of work / Risks / Proof (every requirement to a test or command) / Rollback; each row carries its files and its *Consumes* line |
21
+ | `adr/` | you, or `architect` | Architecture Decision Records (MADR style) |
22
+ | `constitution.md` | `/rr-setup`, then you | the principles the project does not negotiate, cited as `C<n>` in specs, plans and reviews; at most 15 lines |
23
+
24
+ The skills fill in the plugin's templates
25
+ (`skills/project-init/templates/{spec,plan,adr}.md`); a folder holds a
26
+ `TEMPLATE.md` only when this project overrides the plugin's, and the skills read
27
+ that one first. Commit the artefacts with the code they describe, so the
28
+ reasoning survives the pull request. A task's own record — weight, plan, questions, verify runs, review,
29
+ verdict — is not here but in `.ai/reports/<task-id>/task.md`, written by
30
+ `state.py`.
@@ -0,0 +1,13 @@
1
+ <!-- riskrails:start -->
2
+ ## AI agent workflow
3
+
4
+ This repository runs an agentic pipeline under `.ai/`. Read `.ai/AGENTS.md` first: it routes to the policies, workflows and rules, which load on demand; `.ai/policies/` is binding.
5
+
6
+ - Production behaviour is the source of truth: document problems outside the task, do not fix them.
7
+
8
+ - A change runs through `/rr-build <request>`; `/rr-status` shows where it stands. Each step names the files it may touch — an edit outside them is refused: answer `SCOPE_CHANGE_REQUIRED`.
9
+
10
+ - Verify before reporting done: `verify_command` from `.ai/policies/testing.md` once, to the end, then `e2e_command` once; every failure fixed as one batch; show the output.
11
+ - No agent merges or deploys; commit, push, approval only as the dialog picks and the harness records, else a human. When a review flags a mistake twice, the fix goes in this file.
12
+ - Files, not chat, carry decisions: `.ai/reports/<task-id>/task.md` (answer by filling `[Answer]:`), `.ai/state/handoff.md` (read first when resuming), `docs/sdlc/constitution.md` (this project's principles).
13
+ <!-- riskrails:end -->
@@ -0,0 +1,123 @@
1
+ # Testing policy
2
+
3
+ Tests describe behaviour. Coverage percentage is a side effect, not a goal.
4
+
5
+ ## Characterization first
6
+
7
+ Before changing behaviour that is not covered by a test — which is most legacy
8
+ behaviour — write a test that passes against the code **as it is today**. That
9
+ test is the definition of what must not break. Only then change the code.
10
+
11
+ ```
12
+ characterization test → refactor → verify identical behaviour → feature change
13
+ ```
14
+
15
+ Never do those in one step, and never in one commit.
16
+
17
+ ## What to consider, by kind of change
18
+
19
+ | Change touches | Also test |
20
+ |---|---|
21
+ | a shared service | the existing callers, not only the new one |
22
+ | money, tax, fiscal | rounding, currency, negative and zero amounts, refunds |
23
+ | a state machine | every transition that is now reachable, and the ones that must stay unreachable |
24
+ | an async consumer | retry, double delivery, out-of-order delivery, poison message |
25
+ | an external integration | timeout, 500, malformed response, partial write |
26
+ | a write path | idempotency, concurrency, transaction boundaries |
27
+ | authorization | the negative case, for every role |
28
+ | a public API or event | the old shape still works |
29
+
30
+ ## Three scopes, three moments
31
+
32
+ A task runs tests three times at most, and each run has its own scope. Nothing
33
+ else runs in between.
34
+
35
+ | When | What runs | Command |
36
+ |---|---|---|
37
+ | after each implementation step, before `step-done` | **only the tests the plan named for that step** — the files it touched, their direct callers' tests | `step_test_command` |
38
+ | once, after the **last** step | the full fast suite — unit and integration, no e2e | `verify_command` |
39
+ | once, at the **end of the task**, after the full suite is green | the end-to-end suite | `e2e_command` |
40
+
41
+ - A step never runs the full suite, and **never** runs e2e. If the plan named
42
+ no test for a step, nothing runs for it.
43
+ - A step's tests are scoped by path or filter, not by `--stop-on-failure`: the
44
+ scoped run still goes to the end, and its failures are fixed inside that step.
45
+ - The e2e suite runs **once per task**, at the very end, never per step. Skip it
46
+ at T0/T1, and when the change cannot reach a flow the e2e suite covers — say
47
+ which, in one line; silence is not a verdict.
48
+ - A failure in a step's own tests is fixed in that step, before `step-done`;
49
+ the batch rule below is for the two end-of-task runs.
50
+ - After a green suite, a documents-only edit runs `doc_tests` (step scope), not the suite.
51
+
52
+ ## Reading a failure
53
+
54
+ **Run to the end, then fix as one batch.** The verification command runs with
55
+ no fail-fast flag and is never interrupted at the first failure. Collect every
56
+ failure, classify each one, fix all the new regressions in a single remediation
57
+ step (`state.py remediate`), and run the command once more. One failure never
58
+ sends the task back to the start: the plan, the tier and the finished steps
59
+ stand. At most two remediation rounds; a third means the human decides.
60
+
61
+ Classify before fixing. The four answers are:
62
+
63
+ - **EXISTING TEST FAILURE** — it failed before this change too. Say so, do not
64
+ fix it inside this task.
65
+ - **NEW REGRESSION** — this change broke it. Fix the code, not the test.
66
+ - **TEST ENVIRONMENT FAILURE** — database, fixtures, network, container. Say what
67
+ is missing.
68
+ - **UNKNOWN** — you could not tell. Say what you tried.
69
+
70
+ A review's MEDIUM and LOW findings are recorded with `state.py memory risk add`, not
71
+ fixed in the task; only a BLOCKER, a HIGH or a regression buys an `--after-fix` run.
72
+
73
+ **Never edit a test to make it pass when production behaviour changed
74
+ unexpectedly.** A test that suddenly disagrees with the code is evidence, and
75
+ deleting evidence is the worst available option.
76
+
77
+ ## Verification
78
+
79
+ Three commands, one per scope. `verify_command` proves the project is healthy;
80
+ `step_test_command` proves one step is; `e2e_command` proves the whole flow
81
+ still works. Keep each to one command — chain the pieces in a Makefile or
82
+ composer script if there are several — and make sure none of them stops at the
83
+ first failure (`--stop-on-failure`, `-x`, `--bail`, `failfast` are off).
84
+
85
+ `verify_command` must **exclude** the e2e suite: it is the fast loop run after
86
+ the last step and again after a remediation batch. `e2e_command` runs once, at
87
+ the end of the task. A bugfix still shows its new failing test first — that one
88
+ test only, through `single_test`.
89
+
90
+ ```
91
+ verify_command: # the full fast suite, e2e excluded — e.g. make test | composer qa && vendor/bin/phpunit --exclude-group e2e
92
+ step_test_command: # the same runner, scoped to a path or filter — e.g. vendor/bin/phpunit {files} | pytest {files}
93
+ e2e_command: # the end-to-end suite, run once at the end of a task — e.g. make e2e | vendor/bin/behat | npx playwright test
94
+ lint_command: # e.g. vendor/bin/php-cs-fixer fix --dry-run | ruff check . | npm run lint — write `none` if the project has none
95
+ doc_tests: # the suites a documents-only edit runs after a green suite — e.g. tests/test_docs.py; `none` if the project has none
96
+ typecheck_command: # e.g. vendor/bin/phpstan analyse --no-progress | mypy . | npx tsc --noEmit — `none` if the project has none
97
+ healthy_output: # two or three lines of what a green run ends with
98
+ runtime: # roughly, for each command, so a hang is recognisable
99
+ single_test: # how to run one test, for the failing-test-first step of a bugfix
100
+ ```
101
+
102
+ `step_test_command` may contain `{files}`, which is replaced by the paths the
103
+ step names; without it the paths are appended. The placeholder is what lets
104
+ `sensors.py` run a step's own tests for the "test must bite" check, so a runner
105
+ that only takes a `--filter` gets `bite: unavailable` rather than a wrong answer.
106
+
107
+ `lint_command` and `typecheck_command` are read by the deterministic sensors,
108
+ never by a model. They decide, together with the test run, whether a T2 change
109
+ can skip its model review (`risk-tiers.json`, `sensors`). A **missing** line is
110
+ `unavailable` and keeps the review; only the literal `none`, written by a human,
111
+ says the project has no such tool. `sensors.py detect` proposes the exact line
112
+ from the config files it finds, and never runs what it detected.
113
+
114
+ If the project has no e2e suite, write `e2e_command: none` and say so — an
115
+ empty line is read as "not written down yet" and the next task will go looking
116
+ for it. Say which group, tag or directory marks the e2e tests and how
117
+ `verify_command` excludes them, so a step's scoped run and the fast suite stay
118
+ free of them.
119
+
120
+ ## Project specifics
121
+
122
+ <!-- The real commands: how to run unit tests, integration tests, a single test.
123
+ Where fixtures live. What the CI runs. Cite the config. -->
@@ -0,0 +1,15 @@
1
+ # Constitution — {{PROJECT}}
2
+
3
+ <!-- The 10–15 principles this project does not negotiate, one line each, numbered C1…;
4
+ /sdlc-spec and the planner cite them. Under 15: what is not here lives in .ai/policies/. -->
5
+
6
+ C1. Production behaviour is the source of truth; what looks wrong may be load-bearing.
7
+ C2. A bugfix starts with the failing test; legacy behaviour gets a characterization test before it changes.
8
+ C3. A refactoring changes no behaviour and is never mixed with a feature.
9
+ C4. Every step names its files; scope grows only by amending the plan.
10
+ C5. No agent merges or deploys; an agent commits, pushes or approves only by running, verbatim and once, a command a human picked in the dialog, as the harness recorded; else a human runs it outside.
11
+ C6. <!-- project principle — filled by /ai-init from the survey, confirmed by you -->
12
+ C7. <!-- project principle -->
13
+ C8. <!-- project principle -->
14
+ C9. <!-- project principle -->
15
+ C10. <!-- project principle -->
@@ -0,0 +1,13 @@
1
+ <!-- riskrails:start -->
2
+ ## AI agent workflow
3
+
4
+ This repository runs an agentic pipeline under `.ai/`. Read `.ai/AGENTS.md` first: it routes to the policies, workflows and rules, which load on demand; `.ai/policies/` is binding.
5
+
6
+ - Production behaviour is the source of truth: document problems outside the task, do not fix them.
7
+
8
+ - A change runs through `/rr-build <request>`; `/rr-status` shows where it stands. Each step names the files it may touch — an edit outside them is refused: answer `SCOPE_CHANGE_REQUIRED`. One `apply_patch` is checked file by file: a patch that reaches outside the step is refused whole.
9
+
10
+ - Verify before reporting done: `verify_command` from `.ai/policies/testing.md` once, to the end, then `e2e_command` once; every failure fixed as one batch; show the output.
11
+ - No agent merges or deploys; commit, push, approval only as the dialog picks and the harness records, else a human outside. When a review flags the same mistake twice, the correction goes into this file.
12
+ - Files, not chat, carry decisions: `.ai/reports/<task-id>/task.md` (answer by filling `[Answer]:`), `.ai/state/handoff.md` (read first when resuming), `docs/sdlc/constitution.md`.
13
+ <!-- riskrails:end -->
@@ -0,0 +1,6 @@
1
+ # claude-agentic (.ai)
2
+ .ai/state/*.json
3
+ .ai/state/handoff.md
4
+ .ai/state/*.tmp
5
+ .ai/state/*.jsonl
6
+ .ai/local/