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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +29 -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 +114 -23
- package/hooks/ai-path-guard-defaults.json +3 -2
- package/hooks/context-guard.py +15 -12
- package/hooks/dialog-gate.py +1 -1
- package/hooks/guard/common.py +2 -0
- package/hooks/guard/dialog.py +107 -27
- package/hooks/guard/git.py +12 -3
- package/hooks/guard/paths.py +150 -37
- package/hooks/project-scaffold.py +3 -6
- package/hooks/runtime-gate.py +134 -33
- package/hooks/update_notice.py +22 -16
- 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/.ai/state/README.md +4 -4
- 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/rr_state/__init__.py +1 -0
- package/skills/ai-task/rr_state/cli.py +210 -0
- package/skills/ai-task/rr_state/close.py +171 -0
- package/skills/ai-task/rr_state/dialog.py +541 -0
- package/skills/ai-task/rr_state/dialog_cmd.py +164 -0
- package/skills/ai-task/rr_state/fs.py +220 -0
- package/skills/ai-task/rr_state/gate.py +125 -0
- package/skills/ai-task/rr_state/gate_cmd.py +128 -0
- package/skills/ai-task/rr_state/handoff.py +226 -0
- package/skills/ai-task/rr_state/journal.py +126 -0
- package/skills/ai-task/rr_state/labels.py +99 -0
- package/skills/ai-task/rr_state/ledger.py +145 -0
- package/skills/ai-task/rr_state/locks.py +84 -0
- package/skills/ai-task/rr_state/memory_cmd.py +310 -0
- package/skills/ai-task/rr_state/policy.py +378 -0
- package/skills/ai-task/rr_state/profile.py +373 -0
- package/skills/ai-task/rr_state/questions.py +336 -0
- package/skills/ai-task/rr_state/questions_cmd.py +321 -0
- package/skills/ai-task/rr_state/report.py +271 -0
- package/skills/ai-task/rr_state/report_cmd.py +230 -0
- package/skills/ai-task/rr_state/review.py +165 -0
- package/skills/ai-task/rr_state/runtime.py +80 -0
- package/skills/ai-task/rr_state/start.py +257 -0
- package/skills/ai-task/rr_state/steps.py +131 -0
- package/skills/ai-task/rr_state/task.py +396 -0
- package/skills/ai-task/rr_state/verify.py +231 -0
- package/skills/ai-task/sensors.py +37 -4
- package/skills/ai-task/state.py +24 -5138
- package/skills/project-init/templates/constitution.md +1 -1
- package/skills/project-init/templates/sdlc-README.md +20 -14
- package/skills/project-update/adopt.py +38 -24
- 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/a51517e13e57420d2d88a6bcfb4055d762d4a0d10a8f88450929c5c4572213a9 +67 -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 +38 -16
- package/skills/project-update/migrations/0003_context_diet.py +1 -1
- package/skills/project-update/migrations/0007_weight_model.py +130 -23
- package/skills/project-update/migrations/0011_retire_risk_tiers.py +37 -0
- package/skills/project-update/migrations/README.md +16 -1
- package/skills/project-update/migrations/__init__.py +6 -1
- package/skills/project-update/render_instructions.py +1 -1
- package/skills/project-update/update.py +569 -106
- 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 +24 -4
- package/skills/rr-setup/setup.py +7 -1
- package/skills/rr-spec/SKILL.md +4 -2
- package/skills/rr-status/status.py +36 -9
- 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
|
@@ -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`.
|
|
@@ -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
|
|
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 /
|
|
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 /
|
|
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 /
|
|
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 /
|
|
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 /
|
|
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 /
|
|
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 /
|
|
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,
|
|
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
|
-
|
|
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 `/
|
|
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 /
|
|
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 /
|
|
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 /
|
|
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 /
|
|
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 /
|
|
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 /
|
|
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
|
-
|
|
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 @@
|
|
|
1
|
+
11
|
|
@@ -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 -->
|