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
@@ -0,0 +1,171 @@
1
+ """close: the `close` command -- what the mode table still owes, the gate, and the archive of a closed task.
2
+
3
+ core: close_owed
4
+ shell: cmd_close_v7, archive_v7, memory_after_close
5
+ """
6
+ import contextlib
7
+ import json
8
+ import os
9
+
10
+ from rr_state import dialog, fs, gate, handoff, journal, report, task
11
+ from rr_state import policy as rr_policy # close_owed and cmd_close_v7 bind a local `policy`
12
+ from rr_state import runtime as rr_runtime
13
+
14
+ # sensors.py sits beside state.py; the entry puts that directory on sys.path.
15
+ try:
16
+ import sensors # measurement only; it never writes this file
17
+ except ImportError: # an old install that shipped state.py alone
18
+ sensors = None
19
+
20
+
21
+ def close_owed(state, facts, tree, policy):
22
+ """What the mode table still asks for, in order (D7), as (kind, text) with
23
+ kind "review" or "verify"; empty means close."""
24
+ review, mode = state["review"], state["mode"]
25
+ reviewed = review["passes"] > 0 and review["status"] != "blockers_open"
26
+ on_tree = bool(reviewed and tree and review.get("tree") == tree)
27
+ red = [why for ok, why in (facts["a"], facts["b"]) if not ok]
28
+ owed = []
29
+ if mode == "light":
30
+ if red and not on_tree:
31
+ owed.append(("review", "a review, because %s — or make the facts green ('verify --scope all'; "
32
+ "'start --amend' for a file the task needs)" % "; ".join(red)))
33
+ return owed
34
+ if review["status"] == "blockers_open":
35
+ owed.append(("review", "the blockers fixed, 'verify --scope all --after-fix \"…\"', %s"
36
+ % ("a full review pass again" if rr_policy.batch_on(state) else "one scoped re-review")))
37
+ elif not reviewed:
38
+ owed.append(("review", "one review verdict (%s mode: %s)"
39
+ % (mode, rr_policy.mode_setting(policy, mode, "reviewer", "BALANCED"))))
40
+ elif red and not on_tree:
41
+ owed.append(("review", "a fresh 'verify --scope all' or a review of this tree, because %s"
42
+ % "; ".join(red)))
43
+ e2e = state["verify"].get("e2e")
44
+ if e2e not in ("passing", "not_applicable"):
45
+ owed.append(("verify", "e2e green, or 'verify --scope all --e2e not-applicable \"<why>\"' (e2e is %s)"
46
+ % e2e))
47
+ elif not tree or state["verify"].get("e2e_tree") != tree:
48
+ owed.append(("verify", "e2e on this tree: 'verify --scope e2e'%s (it was %s on %s)"
49
+ % (" or 'verify --scope e2e --e2e not-applicable \"<why>\"'"
50
+ if e2e == "not_applicable" else "",
51
+ e2e, (state["verify"].get("e2e_tree") or "?")[:9])))
52
+ return owed
53
+
54
+
55
+ def cmd_close_v7(args, root, rt=rr_runtime.Runtime()):
56
+ """Two facts, the mode table, the gate, the archive; exits 2 naming what is owed."""
57
+ state = task.load_v7(root, rt=rt)
58
+ if args.abandon:
59
+ archive_v7(root, state, fs.current_tree(root), "abandoned", rt=rt)
60
+ memory_after_close(root, state["task_id"])
61
+ return
62
+ policy = rr_policy.weight_policy(root)
63
+ tree = fs.current_tree(root)
64
+ if state["human_approval"].get("rejected_at"):
65
+ # A rejection is answered by changing the work, not by asking again: on the
66
+ # tree the human rejected, close refuses; on a changed one it asks anew.
67
+ if (state.get("gate") or {}).get("tree") == tree:
68
+ fs.die("APPROVAL_REFUSED — rejected at %s and nothing changed since. Address the rejection, "
69
+ "'verify --scope all', then close again; or 'state.py close --abandon'."
70
+ % state["human_approval"]["rejected_at"], 5)
71
+ state["human_approval"]["rejected_at"] = None
72
+ for step in state["steps"]:
73
+ if step.get("status") == "in_progress":
74
+ step.update(status="done", tree_after=tree)
75
+ journal.emit(root, state, "step_done", "%s (finished by close)" % step["id"],
76
+ {"step_id": step["id"], "by": "close"}, legacy="step_completed", rt=rt)
77
+ rr_policy.rescore_v7(root, state, policy, "close", rt=rt)
78
+ facts = report.two_facts(root, state, tree)
79
+ state["facts"] = {"a": list(facts["a"]), "b": list(facts["b"]), "tree": tree, "at": fs.now()}
80
+ if facts["documents"]:
81
+ state["facts"]["documents"] = facts["documents"]
82
+ owed = close_owed(state, facts, tree, policy)
83
+ if owed:
84
+ state["next_action"] = owed[0][1]
85
+ handoff.set_resume_point(state, rt=rt)
86
+ handoff.write_handoff(root, state, "close", rt=rt)
87
+ task.save(root, state)
88
+ report.write_task_md(root, state)
89
+ fs.die("%s required (%s mode) — %s. To drop the task instead: 'state.py close --abandon'."
90
+ % ("review" if any(k == "review" for k, _ in owed) else "verify", state["mode"],
91
+ " · ".join(text for _, text in owed)), 2)
92
+ if state["mode"] in ("full", "full_irreversible") and not gate.gate_approved_for(state, tree):
93
+ if state["human_approval"].get("granted") and (state.get("gate") or {}).get("tree") == tree:
94
+ print("the approval on record does not cover this task: full_irreversible needs approve "
95
+ "--confirm naming %s — asking again" % " and ".join(state["weight"].get("operations") or
96
+ ["the operation"]))
97
+ gate.request_gate(root, state, "human_approval", rt=rt)
98
+ state["gate"] = {"requested_at": state["human_approval"]["requested_at"],
99
+ "gate_id": state["human_approval"]["gate_id"], "tree": tree,
100
+ "approved_by": None, "approved_at": None, "approved_tree": None, "via": None,
101
+ "confirm": None, "operations": list(state["weight"].get("operations") or [])}
102
+ state["stage"] = "ship"
103
+ state["next_action"] = "the human approves in their own terminal, then 'state.py close' again"
104
+ handoff.set_resume_point(state, rt=rt)
105
+ handoff.write_handoff(root, state, "close", rt=rt)
106
+ task.save(root, state)
107
+ report.write_task_md(root, state)
108
+ print("%s %s ready for approval: %s" % (state["task_id"], state["mode"], state.get("goal")))
109
+ print("facts: (a) %s · (b) %s" % (facts["a"][1], facts["b"][1]))
110
+ decision = dialog.offer_approval(root, state, facts, rt=rt) if state["mode"] == "full" else None
111
+ if decision:
112
+ state["next_action"] = "the human picks in the dialog (%s), then 'state.py close' again" % decision["id"]
113
+ handoff.set_resume_point(state, rt=rt)
114
+ handoff.write_handoff(root, state, "close", rt=rt)
115
+ task.save(root, state)
116
+ return
117
+ print("approve in your own terminal, then run close again:\n %s" % dialog.approve_command(root, state))
118
+ return
119
+ if state.get("gate") and not state["gate"].get("approved_by"):
120
+ # The questions-file route grants through human_approval alone.
121
+ approval = state["human_approval"]
122
+ state["gate"].update(approved_by=approval.get("granted_by"), approved_at=approval.get("granted_at"),
123
+ via=approval.get("via"), approved_tree=state["gate"].get("tree"))
124
+ archive_v7(root, state, tree, "closed", facts, rt=rt)
125
+ memory_after_close(root, state["task_id"])
126
+
127
+
128
+ def archive_v7(root, state, tree, result, facts=None, rt=rr_runtime.Runtime()):
129
+ """§Changes and §Verdict into task.md, the state into .ai/state/logs/<task>/,
130
+ and nothing else under reports/ (D4, R8)."""
131
+ base = state["diff"].get("base_tree")
132
+ stats = {f["path"]: f for f in (fs.safe_sensor(sensors.numstat, root, base, tree) or [])} if base and tree else {}
133
+ names = (facts or {}).get("diff") or fs.safe_sensor(sensors.diff_files, root, base, tree) or []
134
+ state["changes"] = [{"path": d["path"], "status": d["status"],
135
+ "added": (stats.get(d["path"]) or {}).get("added", 0),
136
+ "deleted": (stats.get(d["path"]) or {}).get("deleted", 0),
137
+ "binary": (stats.get(d["path"]) or {}).get("binary", False)} for d in names]
138
+ state["stage"] = "done"
139
+ state["current_stage"] = "done"
140
+ state["result"] = result
141
+ state["closed_at"] = fs.now()
142
+ state["next_action"] = "none — task %s" % result
143
+ journal.emit(root, state, "task_closed", "abandoned" if result == "abandoned" else "",
144
+ {"abandoned": result == "abandoned", "mode": state["mode"], "w": state["weight"].get("w")}, rt=rt)
145
+ report.write_task_md(root, state)
146
+ fs.ensure_logs(root, state["task_id"])
147
+ target = os.path.join(fs.logs_dir(root, state["task_id"]), "state.json")
148
+ state["updated_at"] = fs.now()
149
+ tmp = "%s.%d.tmp" % (target, os.getpid())
150
+ with open(tmp, "w", encoding="utf-8") as fh:
151
+ json.dump(state, fh, indent=2, ensure_ascii=False)
152
+ fh.write("\n")
153
+ os.replace(tmp, target)
154
+ with contextlib.suppress(OSError):
155
+ os.remove(fs.handoff_path(root))
156
+ fs.remove_state(root)
157
+ print("%s %s (%s): %s" % (state["task_id"], result, state["mode"],
158
+ os.path.relpath(fs.task_md_path(root, state), root)))
159
+ print("state: %s" % os.path.relpath(target, root))
160
+
161
+
162
+ def memory_after_close(root, task_id):
163
+ """§D5: packages mode exports the closed task. Files mode (no memory.json)
164
+ imports nothing and prints nothing (R15)."""
165
+ if not os.path.exists(os.path.join(root, ".ai", "policies", "memory.json")):
166
+ return
167
+ try:
168
+ import memory_sync # pylint: disable=import-outside-toplevel
169
+ except ImportError:
170
+ return
171
+ memory_sync.after_close(root, task_id)