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
|
@@ -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)
|