riskrails 3.2.2 → 4.1.0
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 +50 -84
- package/agents/ai-reviewer.md.tmpl +1 -1
- package/ai/using-riskrails.md +0 -3
- package/bin/menu.js +116 -0
- package/bin/riskrails.js +132 -79
- package/hooks/ai-path-guard-defaults.json +1 -1
- package/hooks/context-guard.py +2 -4
- package/hooks/guard/dialog.py +14 -2
- package/hooks/guard/paths.py +3 -6
- package/hooks/guard/scope.py +1 -3
- package/hooks/runtime-gate.py +18 -58
- package/instructions/routing.md +2 -2
- package/package.json +1 -1
- package/scripts/ai-path +1 -1
- package/scripts/resolve-profile.py +0 -3
- package/skills/ai-audit/templates/ai-sdlc-adoption-plan.md +1 -1
- package/skills/ai-audit/templates/play-scoring-rubric.md +1 -1
- package/skills/ai-init/scaffold-ai.sh +2 -1
- package/skills/ai-init/templates/.ai/AGENTS.md +6 -6
- package/skills/ai-init/templates/.ai/VERSION +1 -1
- package/skills/ai-init/templates/.ai/policies/safety.md +4 -3
- package/skills/ai-init/templates/.ai/policies/security.md +1 -2
- package/skills/ai-init/templates/.ai/policies/testing.md +5 -12
- package/skills/ai-init/templates/.ai/policies/verification.md +18 -0
- package/skills/ai-init/templates/.ai/policies/weight.json +0 -1
- package/skills/ai-init/templates/.ai/policies/weight.md +1 -2
- package/skills/ai-init/templates/.ai/policies/weight.project.json +4 -0
- package/skills/ai-init/templates/.ai/project/architecture.md +1 -1
- package/skills/ai-init/templates/.ai/project/initial-assessment.md +2 -2
- package/skills/ai-init/templates/.ai/project/overview.md +1 -1
- package/skills/ai-init/templates/.ai/rules/README.md +2 -2
- package/skills/ai-init/templates/.ai/state/README.md +1 -1
- package/skills/ai-task/memory_store.py +8 -9
- package/skills/ai-task/rr_state/cli.py +2 -2
- package/skills/ai-task/rr_state/dialog.py +2 -4
- package/skills/ai-task/rr_state/dialog_cmd.py +183 -6
- package/skills/ai-task/rr_state/fs.py +5 -13
- package/skills/ai-task/rr_state/labels.py +14 -2
- package/skills/ai-task/rr_state/memory_cmd.py +7 -4
- package/skills/ai-task/rr_state/profile.py +2 -4
- package/skills/ai-task/rr_state/questions_cmd.py +1 -1
- package/skills/ai-task/rr_state/report.py +3 -4
- package/skills/ai-task/rr_state/runtime.py +1 -1
- package/skills/ai-task/rr_state/start.py +7 -7
- package/skills/ai-task/sensors.py +10 -11
- package/skills/ai-task/state.py +4 -24
- package/skills/ai-task/weight.py +17 -17
- package/skills/ai-task/weight_overlay.py +38 -0
- package/skills/project-init/templates/constitution.md +2 -2
- package/skills/project-update/history/blobs/00f5e27ce2d66e3a1d8b222b013ae8fa1fed32612d91a551e0c661cee3ef9c2d +34 -0
- package/skills/project-update/history/blobs/0a6b1b6dd06a35582b2e9989a1f41c40abc29254b8e482714fcc6f0a7b16c406 +32 -0
- package/skills/project-update/history/blobs/0aeb04b04262683cac6513545683486a73dc473b447bba43b5aaab18403957a6 +34 -0
- package/skills/project-update/history/blobs/0ce913a28996f546f72d570b0e5a376362e993ce4d805bc4aae57d38caa44212 +41 -0
- package/skills/project-update/history/blobs/0ea9e36446b624b73b2a821231b2ee66a357b43d584a5b23da48b3c800855e65 +41 -0
- package/skills/project-update/history/blobs/0fb1d68678a1349195b2e79433bb20174d25874e3bfd0b70410bb4e428b4e4b8 +35 -0
- package/skills/project-update/history/blobs/119b2b9d0f9c647a243540724edac6b29afa020c69aa205797bcfa9eb06bc021 +35 -0
- package/skills/project-update/history/blobs/1d0fe3ee5fb8260ba610942d4bbdfff7410fbe80ed826ac07f32985031dc2b3f +34 -0
- package/skills/project-update/history/blobs/1de9a37ed6402893dcab6e61ddbf15ffd35f15a6d84b18e675a0c5bf4e981f85 +42 -0
- package/skills/project-update/history/blobs/1f44e1a236233dcbab58064f39b69f798b35d1d9a29f8c615b5b12854ce633cb +15 -0
- package/skills/project-update/history/blobs/1f824645299579cbdd8141642ddbb4bca9fd5fe9d3112f036d8221f0811e0abf +40 -0
- package/skills/project-update/history/blobs/235fb88393cfe20dd58b35fe9785cc45baf5ffe21a13d926652d76ec68cea8b7 +35 -0
- package/skills/project-update/history/blobs/3918b4ee011d9477b9d2779e2893ab599749c406d4b09439a9748b64fbff58f3 +18 -0
- package/skills/project-update/history/blobs/449273200dfec2a56885c881ff34d40e8ecc00da53c24c2d09b7fbeb1004d255 +40 -0
- package/skills/project-update/history/blobs/4c45aee9b9fc5f6b4e1d9739e434c4094975a5a8dec6b9703fb25f3080680c77 +124 -0
- package/skills/project-update/history/blobs/4dd3fa12bc45acc8c836ab65d790248ca7504a021bca6677499d17bb3d798ed9 +64 -0
- package/skills/project-update/history/blobs/51e3f15095d7123ea8566b512479ee10a6cb2f336b7352a259a083a887bd7116 +33 -0
- package/skills/project-update/history/blobs/5497a204967bea7dab29079a6e1dfa3d8713b2d2cb294c2779378039f03a55fc +36 -0
- package/skills/project-update/history/blobs/60c4d7e7fde7049d92d84295fc13ca9a91eb2de423be08f093b3ee5d5e817ff8 +64 -0
- package/skills/project-update/history/blobs/6221b7c14cc04634e485e562d464c6860d45666a7f63e293c06bb2944ff556cc +27 -0
- package/skills/project-update/history/blobs/637971cfb844ef4285e2886a4b1c834fb2ed5c46e773adb88c2c147fea626ad3 +33 -0
- package/skills/project-update/history/blobs/65af2282eea2a51fcc27a4acf2c26c727b0961c30e1be13176369200c460c219 +34 -0
- package/skills/project-update/history/blobs/66ecd73e303e7bbc1aa8ee72435d0d10812ff7b2eff0adac59bbf0a303e2cc01 +41 -0
- package/skills/project-update/history/blobs/6a43bf32461b22b87fafe2865529dc04fa66cd00182a2a7598aa071667ca255a +35 -0
- package/skills/project-update/history/blobs/6d6dee8634abf9ac057bb206828b83a893a2afcf4f86bbe69e839a3b47d0adde +48 -0
- package/skills/project-update/history/blobs/7224ede803ea81e65a456e21b6028a0e848e8a880456c6503eebe7534ca4f6aa +74 -0
- package/skills/project-update/history/blobs/765a39a202617096c232017822ae738d37e4405430c71700dc7bc14bf1ba794e +33 -0
- package/skills/project-update/history/blobs/77577440a933d1cabd8c6f4a6401cf361a784aa09a25500a6f7b44ead8ab5e55 +41 -0
- package/skills/project-update/history/blobs/7bae90789625d6fb9071ecb412d14a8d1eb0bc5659d8d284c4eaf213e1b5de56 +67 -0
- package/skills/project-update/history/blobs/7bcdf9a0e6b85277b9b60669150224b9414d2e32821ff06f4d148eb1d18785bb +42 -0
- package/skills/project-update/history/blobs/7f834103e9d97aea44e76717bcdcbbe349c560e5b81a42ce85a1cec341041281 +4 -0
- package/skills/project-update/history/blobs/7fb3acb0211818efebc2234863db0b8c846022a7280a6d2d69f267dbf63db48c +34 -0
- package/skills/project-update/history/blobs/87f0855859ff68cc6c988cc0b631ee7f56af7fbba062172bf80e01f4982cc054 +35 -0
- package/skills/project-update/history/blobs/8abab74759766d02b5cb7b0c3e59a3d440b25a454216cd8fea325c23fd822d36 +33 -0
- package/skills/project-update/history/blobs/8ec171f0fbdf38cc6996d496f01491fd1f21f04a435a118ad979b127a8be06f1 +85 -0
- package/skills/project-update/history/blobs/a1fb50e6c86fae1679ef3351296fd6713411a08cf8dd1790a4fd05fae8688164 +1 -0
- package/skills/project-update/history/blobs/a3df7268feb0780be0d2cf9acced6ed81780a7790f36fe024b6ea0b1bd6eb7a6 +35 -0
- package/skills/project-update/history/blobs/b28721136482dafb9fd2801af340364cb97e9f6eaa987908606c786045b8d334 +28 -0
- package/skills/project-update/history/blobs/b3726422d767f9219fea633bea77f0c99ad9af843c591e6e928e1ccd2a47f374 +27 -0
- package/skills/project-update/history/blobs/b3839425f9d0ab2efa42a6f5e9488ec2d26ca5fb8dcd694b8ab098e10743ca46 +32 -0
- package/skills/project-update/history/blobs/ba14a457e83f9dde65e4190fdfab83b912f7398c1442a2a1cb3d89bfac741cf3 +8 -0
- package/skills/project-update/history/blobs/bceb7189444bb6291b262ca08e34db01010dda2f6e64d696231ec3c343c95c66 +34 -0
- package/skills/project-update/history/blobs/bf82a2bb4fce1bd349ff66118930a01d61a7af8e2bd8d200c721d43afc58c713 +34 -0
- package/skills/project-update/history/blobs/c4dbeb180292a6199ad73a3be861ece07c1f8bf6bf0cc7ca204412bc8e1bb1d8 +31 -0
- package/skills/project-update/history/blobs/c644838866df2001a42501a988dd7b77aacf862bf09d22b465f3cd8a22c6b6eb +33 -0
- package/skills/project-update/history/blobs/c980d74f76a4bdfb38c94b573ac10a54eb50b850b052e15fa6f2442889b36ccb +32 -0
- package/skills/project-update/history/blobs/ceed2f3bd937f862d0e6ca59ce1b1e3603e19a0d7f85d90d6dfeb7bf690153be +29 -0
- package/skills/project-update/history/blobs/d51ea2e893482a20c5882056cf8043327ca1dc1c957927f6746929a47ee8368e +45 -0
- package/skills/project-update/history/blobs/d78f5e0bb1380259fce2cce009785a9a11f6de8b87a160080b9872c059f95060 +34 -0
- package/skills/project-update/history/blobs/d97f46a2cca020406252b1f7baa4184617fa71faca3885e2379c44ae4e1d9327 +67 -0
- package/skills/project-update/history/blobs/da3ecd4c4c414c93d04488685931df39a56bf959bfeb9c45cb1aea2c13bd991b +38 -0
- package/skills/project-update/history/blobs/e129bf71a5a922e24b2c1bc8daa8a5d2b5589b0069770e17ce17e7db5ed4e7a4 +27 -0
- package/skills/project-update/history/blobs/e504529519a371892838725f8b89b5d32e3300e345bcbf2643a3a1a48434b3dd +24 -0
- package/skills/project-update/history/blobs/e9e62d03ca70b3dffccef8dceeb214173352cfef005145e519f0e037993f1c03 +36 -0
- package/skills/project-update/history/blobs/effefbaaf4beb8f1ba55081d6b7e74bf25ebb91045dd10613418f17b95fa7182 +33 -0
- package/skills/project-update/history/blobs/f6af4581bafbf62b257d57e06c3ae2c5998aeca608ee7df037fcc3c3e4b563a9 +34 -0
- package/skills/project-update/history/blobs/fc35045777d29630303430752a651b3e553580a581036a02cc103eb96a46f4b0 +34 -0
- package/skills/project-update/history/blobs/fdc21345c77f92a42d244aa104f393f439a0ff9981a9fd462bd837e8a47931bb +48 -0
- package/skills/project-update/history/index.json +34 -14
- package/skills/project-update/render_instructions.py +12 -45
- package/skills/project-update/update.py +757 -1172
- package/skills/rr-setup/SKILL.md +114 -97
- package/skills/rr-setup/scaffold_ai.py +15 -9
- package/skills/rr-setup/setup.py +462 -164
- package/skills/rr-status/status.py +7 -10
- package/skills/usage-report/usage-report.py +4 -5
- package/skills/ai-audit/SKILL.md +0 -9
- package/skills/ai-init/SKILL.md +0 -9
- package/skills/ai-status/SKILL.md +0 -9
- package/skills/ai-task/SKILL.md +0 -9
- package/skills/project-init/SKILL.md +0 -9
- package/skills/project-update/SKILL.md +0 -9
- package/skills/project-update/adopt-map.json +0 -91
- package/skills/project-update/adopt.py +0 -2225
- package/skills/project-update/migrations/0001_schema_version.py +0 -13
- package/skills/project-update/migrations/0002_task_journal.py +0 -123
- package/skills/project-update/migrations/0003_context_diet.py +0 -65
- package/skills/project-update/migrations/0004_deterministic_gates.py +0 -57
- package/skills/project-update/migrations/0005_runtimes_and_plans.py +0 -35
- package/skills/project-update/migrations/0006_shared_files.py +0 -154
- package/skills/project-update/migrations/0007_weight_model.py +0 -295
- package/skills/project-update/migrations/0008_renames.py +0 -43
- package/skills/project-update/migrations/0009_state_file.py +0 -25
- package/skills/project-update/migrations/0010_dialog_switch.py +0 -30
- package/skills/project-update/migrations/0011_retire_risk_tiers.py +0 -37
- package/skills/project-update/migrations/README.md +0 -76
- package/skills/project-update/migrations/__init__.py +0 -186
- package/skills/sdlc-intent/SKILL.md +0 -9
- package/skills/sdlc-plan/SKILL.md +0 -9
- package/skills/sdlc-spec/SKILL.md +0 -9
- package/skills/usage-report/SKILL.md +0 -10
- /package/{ai/workflows/refactoring.md → skills/project-update/history/blobs/36cb593293496fbcf38df828a9b9b2bc0d5a53333f584a5319ebc3d799016e24} +0 -0
- /package/{ai/workflows/feature.md → skills/project-update/history/blobs/56e16072c2f3edc6c8897c690d09a11d12fc414c3e3f361f359f7dc609a21282} +0 -0
- /package/{ai/workflows/hotfix.md → skills/project-update/history/blobs/8cbefbf1aa412bae3e5019acd40b8fe0598fceba6c781906b3bc6dbd014f6b23} +0 -0
- /package/{ai/workflows/bugfix.md → skills/project-update/history/blobs/af56dc16e44ae1d8c3dac4c3ca82e2163b719553c02ea455e77430bab9a14089} +0 -0
- /package/{ai/workflows/investigation.md → skills/project-update/history/blobs/b9e09cb24958cd6831589997558974bd6356305306e8f9685e226bcdc30d60ac} +0 -0
- /package/{ai/agents/manager.md → skills/project-update/history/blobs/d0cf6388f35a92a0cf607d33909278382f6d7b6a4c32e281b9f9e0ea652c7b91} +0 -0
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
"""dialog_cmd: the consent and decision commands -- decision open/show/settle/fallback, the pick applied, and the read-only consent view.
|
|
2
2
|
|
|
3
|
-
core: SETTLE_EFFECT
|
|
4
|
-
shell: cmd_decision, decision_open, decision_settle, apply_choice, cmd_consent
|
|
3
|
+
core: SETTLE_EFFECT, SETUP_VALUES, SETUP_VALUE_MAX, SETUP_SPEC_MAX, setup_option_why, setup_decision_why, setup_spec_why
|
|
4
|
+
shell: cmd_decision, decision_open, read_setup_spec, setup_command_why, check_setup_labels, setup_open, decision_settle, apply_choice, cmd_consent
|
|
5
5
|
"""
|
|
6
6
|
# pylint: disable=unused-argument # uniform cli dispatch: cmd_consent takes rt like every command
|
|
7
7
|
import argparse
|
|
8
8
|
import json
|
|
9
9
|
import os
|
|
10
|
+
import shlex
|
|
10
11
|
import sys
|
|
11
12
|
|
|
12
13
|
from rr_state import dialog, fs, labels, profile, questions_cmd, task
|
|
13
14
|
from rr_state import ledger as rr_ledger
|
|
14
15
|
from rr_state import runtime as rr_runtime
|
|
15
16
|
|
|
16
|
-
SETTLE_EFFECT = {"defer": "defer", "keep": "ack", "ack": "ack", "answer": "answered"}
|
|
17
|
+
SETTLE_EFFECT = {"defer": "defer", "keep": "ack", "ack": "ack", "answer": "answered", "set": "set"}
|
|
18
|
+
# What `decision open --kind setup` accepts (R11): the questions /rr-setup asks, each with the words a choice may set
|
|
19
|
+
# (what the skill applies is what the human read: one of these words, or, for verify only, a command shown verbatim
|
|
20
|
+
# in its own label), the longest value, and the most characters of a spec it reads.
|
|
21
|
+
SETUP_VALUES = {"memory": ("off", "keep"), "runtime": ("auto", "claude", "codex", "both"),
|
|
22
|
+
"survey": ("light", "full", "keep"), "verify": ("", "other", "keep", "edit")}
|
|
23
|
+
SETUP_VALUE_MAX = 200
|
|
24
|
+
SETUP_SPEC_MAX = 262144
|
|
17
25
|
|
|
18
26
|
|
|
19
27
|
def cmd_decision(args, root, rt=rr_runtime.Runtime()):
|
|
@@ -53,10 +61,16 @@ def cmd_decision(args, root, rt=rr_runtime.Runtime()):
|
|
|
53
61
|
|
|
54
62
|
|
|
55
63
|
def decision_open(args, root, rt=rr_runtime.Runtime()):
|
|
56
|
-
"""The
|
|
64
|
+
"""The openers the model may call: --kind commit (git commit and git push) and --kind setup (the /rr-setup
|
|
65
|
+
questions: choices that set a value, and this project's `memory enable`); nothing else."""
|
|
66
|
+
if args.kind == "setup":
|
|
67
|
+
setup_open(args, root, rt=rt)
|
|
68
|
+
return
|
|
57
69
|
if args.kind != "commit":
|
|
58
|
-
fs.die("decision open takes --kind commit; every other decision is opened by the
|
|
59
|
-
"needs it (close, ask, the budgets, start --mode)")
|
|
70
|
+
fs.die("decision open takes --kind commit or --kind setup; every other decision is opened by the "
|
|
71
|
+
"command that needs it (close, ask, the budgets, start --mode)")
|
|
72
|
+
if args.spec is not None:
|
|
73
|
+
fs.die("decision open --kind commit takes --command, not --spec", 2)
|
|
60
74
|
if not args.commands:
|
|
61
75
|
fs.die("decision open --kind commit needs --command \"git commit …\" [--command \"git push <remote> <branch>\"]")
|
|
62
76
|
for command in args.commands:
|
|
@@ -83,6 +97,165 @@ def decision_open(args, root, rt=rr_runtime.Runtime()):
|
|
|
83
97
|
dialog.print_dialog([decision], rt=rt)
|
|
84
98
|
|
|
85
99
|
|
|
100
|
+
def read_setup_spec(path):
|
|
101
|
+
"""--spec FILE|- as JSON: a spec that cannot be read, is longer than SETUP_SPEC_MAX characters, is not JSON or
|
|
102
|
+
nests deeper than the dialog's own cap is a bad spec (exit 2)."""
|
|
103
|
+
try:
|
|
104
|
+
if path == "-":
|
|
105
|
+
text = sys.stdin.read(SETUP_SPEC_MAX + 1)
|
|
106
|
+
else:
|
|
107
|
+
with open(path, encoding="utf-8") as fh:
|
|
108
|
+
text = fh.read(SETUP_SPEC_MAX + 1)
|
|
109
|
+
if len(text) > SETUP_SPEC_MAX:
|
|
110
|
+
fs.die("bad spec — more than %d characters" % SETUP_SPEC_MAX, 2)
|
|
111
|
+
spec = dialog.json_loads_capped(text)
|
|
112
|
+
# A lone surrogate ("\ud800") parses, yet no file can hold it: refused here, not after a first decision is written.
|
|
113
|
+
json.dumps(spec, ensure_ascii=False).encode("utf-8")
|
|
114
|
+
return spec
|
|
115
|
+
except (OSError, ValueError, RecursionError) as exc:
|
|
116
|
+
fs.die("bad spec — %s" % exc, 2)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def setup_option_why(option, key):
|
|
120
|
+
"""Why one option of the decision `key` is not {kind: choice, label, value[, description]} or {kind: command,
|
|
121
|
+
label[, description]}, else None. A choice sets one of its key's words (SETUP_VALUES); a verify choice may also
|
|
122
|
+
set a command, and check_setup_labels (exit 6) makes its label show it, like every rule of the labels."""
|
|
123
|
+
if not isinstance(option, dict) or option.get("kind") not in ("choice", "command"):
|
|
124
|
+
return "an option is an object whose kind is choice or command"
|
|
125
|
+
need = {"kind", "label", "value"} if option["kind"] == "choice" else {"kind", "label"}
|
|
126
|
+
if not need <= set(option) <= need | {"description"}:
|
|
127
|
+
return "a %s option holds %s, and a description if it has one" % (option["kind"], ", ".join(sorted(need)))
|
|
128
|
+
text, value = option.get("description", ""), option.get("value", "")
|
|
129
|
+
if not isinstance(option["label"], str) or not isinstance(text, str) or len(text) > dialog.QUESTION_MAX:
|
|
130
|
+
return "a label and a description are text, a description at most %d characters" % dialog.QUESTION_MAX
|
|
131
|
+
if not isinstance(value, str) or len(value) > SETUP_VALUE_MAX or not value.isprintable():
|
|
132
|
+
return "a choice value is at most %d printable characters (empty is allowed)" % SETUP_VALUE_MAX
|
|
133
|
+
if option["kind"] == "choice" and key != "verify" and value not in SETUP_VALUES[key]:
|
|
134
|
+
return "a %s choice sets one of: %s" % (key, ", ".join(SETUP_VALUES[key]))
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def setup_decision_why(item):
|
|
139
|
+
"""Why one decision of a spec is not {key, header, question, options}, else None."""
|
|
140
|
+
if not isinstance(item, dict) or set(item) != {"key", "header", "question", "options"}:
|
|
141
|
+
return "a decision is an object holding exactly key, header, question and options"
|
|
142
|
+
if not isinstance(item["key"], str) or item["key"] not in SETUP_VALUES:
|
|
143
|
+
return "a decision key is one of %s" % ", ".join(SETUP_VALUES)
|
|
144
|
+
if not all(isinstance(item[name], str) and item[name].strip() for name in ("header", "question")):
|
|
145
|
+
return "a header and a question are text and not empty"
|
|
146
|
+
if not isinstance(item["options"], list) or not 2 <= len(item["options"]) <= 4:
|
|
147
|
+
return "a decision has 2 to 4 options"
|
|
148
|
+
for number, option in enumerate(item["options"], 1):
|
|
149
|
+
why = setup_option_why(option, item["key"])
|
|
150
|
+
if why:
|
|
151
|
+
return "option %d: %s" % (number, why)
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def setup_spec_why(spec):
|
|
156
|
+
"""Why a parsed spec is not {"schema": 1, "decisions": [...]} with 1 to DECISIONS_MAX decisions, each key
|
|
157
|
+
once, else None (R11)."""
|
|
158
|
+
if not isinstance(spec, dict) or set(spec) != {"schema", "decisions"}:
|
|
159
|
+
return "a spec is an object holding exactly schema and decisions"
|
|
160
|
+
schema, decisions = spec["schema"], spec["decisions"]
|
|
161
|
+
if not isinstance(schema, int) or isinstance(schema, bool) or schema != 1:
|
|
162
|
+
return "schema is 1"
|
|
163
|
+
if not isinstance(decisions, list) or not 1 <= len(decisions) <= dialog.DECISIONS_MAX:
|
|
164
|
+
return "decisions is a list of 1 to %d decisions" % dialog.DECISIONS_MAX
|
|
165
|
+
for number, item in enumerate(decisions, 1):
|
|
166
|
+
why = setup_decision_why(item)
|
|
167
|
+
if why:
|
|
168
|
+
return "decision %d: %s" % (number, why)
|
|
169
|
+
keys = [item["key"] for item in decisions]
|
|
170
|
+
return None if len(set(keys)) == len(keys) else "a decision key is used once"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def setup_command_why(root, label):
|
|
174
|
+
"""Why `label` is not exactly `python3 <this state.py> --root <this project> memory enable [options]`, else
|
|
175
|
+
None. validate_label alone also passes another --root, a --runtime, interpreter flags, approve and git commit
|
|
176
|
+
(R11, B1); the paths must be absolute, as the labels state_label writes."""
|
|
177
|
+
why = labels.validate_label(label)
|
|
178
|
+
if why:
|
|
179
|
+
return why
|
|
180
|
+
words = shlex.split(label)
|
|
181
|
+
if len(words) >= 6 and words[0] == "python3" and words[2] == "--root" and words[4:6] == ["memory", "enable"] \
|
|
182
|
+
and os.path.isabs(words[1]) and os.path.realpath(words[1]) == os.path.realpath(fs.ENTRY) \
|
|
183
|
+
and os.path.isabs(words[3]) and os.path.realpath(words[3]) == os.path.realpath(root):
|
|
184
|
+
return None
|
|
185
|
+
return ("a setup command is exactly python3 <this state.py> --root <this project> memory enable "
|
|
186
|
+
"[--author S] [--period P] [--index none|sqlite]")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def check_setup_labels(root, decisions):
|
|
190
|
+
"""Every label of the spec, before anything is written (exit 6): only the memory decision offers a command, and
|
|
191
|
+
that is setup_command_why's; a choice never reads as a command, because the gate passes a Bash command equal
|
|
192
|
+
to any pick and dialog_pick runs a pick that is a state.py label; and a verify choice that sets a command
|
|
193
|
+
shows it, whole, as one `span` of its label, because the skill applies the value and the human reads the
|
|
194
|
+
label (R11, B1)."""
|
|
195
|
+
for item in decisions:
|
|
196
|
+
options = item["options"]
|
|
197
|
+
if len({option["label"] for option in options}) != len(options):
|
|
198
|
+
fs.die("CONSENT_LABEL_INVALID — two options of the %s decision carry the same label" % item["key"], 6)
|
|
199
|
+
for option in options:
|
|
200
|
+
label = option["label"]
|
|
201
|
+
if option["kind"] == "command":
|
|
202
|
+
why = setup_command_why(root, label) if item["key"] == "memory" else \
|
|
203
|
+
"only the memory decision offers a command"
|
|
204
|
+
elif not 0 < len(label) <= labels.LABEL_MAX or not label.isprintable():
|
|
205
|
+
why = "a choice label is 1 to %d printable characters" % labels.LABEL_MAX
|
|
206
|
+
elif label.startswith(("python3", "git", "npx")) or labels.label_argv(label) \
|
|
207
|
+
or labels.validate_label(label) is None:
|
|
208
|
+
why = "a choice label never reads as a command"
|
|
209
|
+
elif item["key"] == "verify" and option["value"] not in SETUP_VALUES["verify"] \
|
|
210
|
+
and option["value"] not in label.split("`")[1:-1:2]:
|
|
211
|
+
why = "a verify command is shown verbatim in its label, inside backticks"
|
|
212
|
+
else:
|
|
213
|
+
why = None
|
|
214
|
+
if why:
|
|
215
|
+
fs.die("CONSENT_LABEL_INVALID — %s: %s" % (why, label[:120]), 6)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def setup_open(args, root, rt=rr_runtime.Runtime()):
|
|
219
|
+
"""decision open --kind setup --spec FILE|-: every check runs for every decision of the spec before the first
|
|
220
|
+
write, then all are opened in the spec's order, or none (R11). The caller sends the one dialog."""
|
|
221
|
+
if args.commands or args.spec is None:
|
|
222
|
+
fs.die("decision open --kind setup takes --spec FILE|- and no --command", 2)
|
|
223
|
+
spec = read_setup_spec(args.spec)
|
|
224
|
+
why = setup_spec_why(spec)
|
|
225
|
+
if why:
|
|
226
|
+
fs.die("bad spec — %s" % why, 2)
|
|
227
|
+
decisions = spec["decisions"]
|
|
228
|
+
check_setup_labels(root, decisions)
|
|
229
|
+
if not dialog.dialog_on(root, runtime=rt.name):
|
|
230
|
+
fs.die("the dialog is off here (.ai/policies/dialog.json, the runtime's dialog tool, a session id): "
|
|
231
|
+
"suggest the commands; the human runs them outside the agent")
|
|
232
|
+
data = rr_ledger.read_decisions(root)
|
|
233
|
+
questions = [item["question"].strip()[:dialog.QUESTION_MAX] for item in decisions] # as open_decision stores them
|
|
234
|
+
if len(set(questions)) != len(questions):
|
|
235
|
+
fs.die("DECISION_DUPLICATE — two decisions of the spec carry one question text", 6)
|
|
236
|
+
twin = next((d for d in data["open"] if d.get("question") in questions), None)
|
|
237
|
+
if twin:
|
|
238
|
+
fs.die("DECISION_DUPLICATE — %s is open with this very question; settle it, or 'state.py decision fallback "
|
|
239
|
+
"--id %s --why …', before opening it again" % (twin.get("id"), twin.get("id")), 6)
|
|
240
|
+
open_questions = sum(len(dialog.dialog_questions(d)) for d in data["open"])
|
|
241
|
+
if open_questions + len(decisions) > dialog.DECISIONS_MAX:
|
|
242
|
+
fs.die("DECISION_LIMIT — %d questions are open and the spec adds %d; one dialog call carries %d"
|
|
243
|
+
% (open_questions, len(decisions), dialog.DECISIONS_MAX), 6)
|
|
244
|
+
state = task.load(root, required=False, claim=False, rt=rt)
|
|
245
|
+
state = state if state and task.is_v7(state) else None
|
|
246
|
+
opened = []
|
|
247
|
+
for item in decisions:
|
|
248
|
+
options = [dict(o, effect="set") if o["kind"] == "choice" else o for o in item["options"]]
|
|
249
|
+
decision, why = dialog.open_decision(root, state, "setup", item["header"], item["question"], options,
|
|
250
|
+
"decision open --kind setup", rt=rt, setup_key=item["key"])
|
|
251
|
+
if decision is None:
|
|
252
|
+
for done in opened: # a race since the checks above: none of this call stays open
|
|
253
|
+
dialog.settle_decision(root, done["id"], "withdrawn")
|
|
254
|
+
fs.die(why, 6)
|
|
255
|
+
opened.append(decision)
|
|
256
|
+
dialog.print_dialog(opened, rt=rt)
|
|
257
|
+
|
|
258
|
+
|
|
86
259
|
def decision_settle(args, root, decision, rt=rr_runtime.Runtime()):
|
|
87
260
|
"""(b) apply what the recorder wrote — a choice pick or a decline; (c) a
|
|
88
261
|
human in a terminal picks an option by number (R36.14)."""
|
|
@@ -112,6 +285,8 @@ def decision_settle(args, root, decision, rt=rr_runtime.Runtime()):
|
|
|
112
285
|
dialog.settle_decision(root, decision["id"], "declined")
|
|
113
286
|
print("%s settled: declined in the dialog (%s); nothing runs"
|
|
114
287
|
% (decision["id"], mine[-1].get("why") or "no answer"))
|
|
288
|
+
if decision.get("kind") == "setup":
|
|
289
|
+
print("SETUP_PICK %s=" % decision.get("setup_key"))
|
|
115
290
|
return
|
|
116
291
|
live = [p for p in rr_ledger.live_picks(ledger, (dialog.dialog_on(root, runtime=rt.name) or {}).get("session"))
|
|
117
292
|
if p.get("decision") == decision["id"]]
|
|
@@ -132,6 +307,8 @@ def apply_choice(root, decision, option, pick_id, by, via, rt=rr_runtime.Runtime
|
|
|
132
307
|
root, rt=rt)
|
|
133
308
|
dialog.settle_decision(root, decision["id"], effect, pick_id)
|
|
134
309
|
print("%s settled: %s (%s)" % (decision["id"], option["label"], effect))
|
|
310
|
+
if decision.get("kind") == "setup":
|
|
311
|
+
print("SETUP_PICK %s=%s" % (decision.get("setup_key"), option.get("value", "")))
|
|
135
312
|
|
|
136
313
|
|
|
137
314
|
def cmd_consent(args, root, rt=rr_runtime.Runtime()):
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""The store: where the state, the session, the journal and the logs live, and the clock, the root search and the one subprocess runner.
|
|
2
2
|
|
|
3
3
|
shell: ENTRY, now, now_ms, find_root, find_root_quiet, die, state_path, remove_state, read_session, safe_sensor, find_docs_root, questions_path, read_lines, run_text, read_text, current_tree, ensure_logs
|
|
4
|
-
core:
|
|
4
|
+
core: session_path, journal_path, _split_files, SLUG_RE, one_line, handoff_path, decision_path, ledger_path, logs_dir, task_md_path
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import contextlib
|
|
@@ -59,22 +59,14 @@ def die(message, code=1):
|
|
|
59
59
|
sys.exit(code)
|
|
60
60
|
|
|
61
61
|
|
|
62
|
-
STATE_NAMES = ("state.json", "current.json")
|
|
63
|
-
|
|
64
|
-
|
|
65
62
|
def state_path(root):
|
|
66
|
-
"""The task's state file
|
|
67
|
-
|
|
68
|
-
starts in state.json (R34's contract half, migration 0009 moves an old one);
|
|
69
|
-
both names stay readable, so a revert of that move still finds the task."""
|
|
70
|
-
new, legacy = (os.path.join(root, ".ai", "state", name) for name in STATE_NAMES)
|
|
71
|
-
return legacy if not os.path.exists(new) and os.path.exists(legacy) else new
|
|
63
|
+
"""The task's state file: state.json, the only name read."""
|
|
64
|
+
return os.path.join(root, ".ai", "state", "state.json")
|
|
72
65
|
|
|
73
66
|
|
|
74
67
|
def remove_state(root):
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
os.remove(path)
|
|
68
|
+
with contextlib.suppress(FileNotFoundError):
|
|
69
|
+
os.remove(os.path.join(root, ".ai", "state", "state.json"))
|
|
78
70
|
|
|
79
71
|
|
|
80
72
|
def session_path(root):
|
|
@@ -43,12 +43,23 @@ def subcommand(argv):
|
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
def state_label_ok(argv):
|
|
46
|
-
"""approve | reject | profile set | verify --by | review --by | start --mode … --by
|
|
46
|
+
"""approve | reject | profile set | verify --by | review --by | start --mode … --by |
|
|
47
|
+
memory enable [--author S] [--period month|quarter|year] [--index none|sqlite]: each option at most
|
|
48
|
+
once, as two words, in any order, and no other memory verb. hooks/guard/dialog.py keeps the same
|
|
49
|
+
rule; tests/test_dialog_contract.py pins the two equal."""
|
|
47
50
|
rest = subcommand(argv)
|
|
48
51
|
if rest[:1] in (["approve"], ["reject"]) or rest[:2] == ["profile", "set"]:
|
|
49
52
|
return True
|
|
50
53
|
if rest[:1] in (["verify"], ["review"]):
|
|
51
54
|
return "--by" in rest
|
|
55
|
+
if rest[:2] == ["memory", "enable"]:
|
|
56
|
+
# The pick is the exact command the human read: no `--opt=value`, no abbreviation argparse would
|
|
57
|
+
# take, no positional, an option once, a value of its own kind (an author never starts with '-').
|
|
58
|
+
names, values = rest[2::2], rest[3::2]
|
|
59
|
+
kinds = {"--author": None, "--period": ("month", "quarter", "year"), "--index": ("none", "sqlite")}
|
|
60
|
+
return (len(names) == len(values) == len(set(names)) and set(names) <= set(kinds)
|
|
61
|
+
and all((v[:1] not in ("", "-")) if kinds[n] is None else v in kinds[n]
|
|
62
|
+
for n, v in zip(names, values)))
|
|
52
63
|
return rest[:1] == ["start"] and "--mode" in rest and "--by" in rest
|
|
53
64
|
|
|
54
65
|
|
|
@@ -78,7 +89,8 @@ def validate_label(label):
|
|
|
78
89
|
and state_label_ok(script[1]):
|
|
79
90
|
return None
|
|
80
91
|
return ("a label starts with git commit, git push <remote> <branch>, state.py approve|reject|"
|
|
81
|
-
"profile set|verify --by|review --by|start --mode --by
|
|
92
|
+
"profile set|verify --by|review --by|start --mode --by|memory enable [--author S] "
|
|
93
|
+
"[--period P] [--index none|sqlite], or npx riskrails@<v> … --yes")
|
|
82
94
|
|
|
83
95
|
|
|
84
96
|
def human_name(root):
|
|
@@ -39,9 +39,10 @@ def memory_config(sync, root):
|
|
|
39
39
|
return None
|
|
40
40
|
|
|
41
41
|
|
|
42
|
-
def memory_human(root, what, rt=rr_runtime.Runtime()):
|
|
43
|
-
"""R16: enable, disable, cleanup, pack, redact and move-logs take the human's route, as approve does.
|
|
44
|
-
|
|
42
|
+
def memory_human(root, what, allow_dialog=False, rt=rr_runtime.Runtime()):
|
|
43
|
+
"""R16: enable, disable, cleanup, pack, redact and move-logs take the human's route, as approve does. Only
|
|
44
|
+
enable passes allow_dialog: a live pick of this session that is this very command is the human's answer."""
|
|
45
|
+
via, why = rr_dialog.human_route(root, allow_dialog=allow_dialog, rt=rt)
|
|
45
46
|
if via is None:
|
|
46
47
|
fs.die("APPROVAL_REFUSED — 'memory %s' is the human's, from their own terminal:\n python3 %s --root %s "
|
|
47
48
|
"%s\nRefused because %s." % (what, shlex.quote(fs.ENTRY), shlex.quote(root),
|
|
@@ -98,7 +99,9 @@ def memory_status(args, root, sync, _store, rt=rr_runtime.Runtime()):
|
|
|
98
99
|
|
|
99
100
|
|
|
100
101
|
def memory_enable(args, root, sync, _store, rt=rr_runtime.Runtime()):
|
|
101
|
-
memory_human(root, "enable", rt=rt)
|
|
102
|
+
if memory_human(root, "enable", allow_dialog=True, rt=rt) == "dialog":
|
|
103
|
+
pick = rr_dialog.pick_data(rr_dialog.DIALOG_PICK[0])
|
|
104
|
+
print("memory: via dialog (consent %s, decision %s)" % (pick["consent"], pick["decision"]))
|
|
102
105
|
hook = sync.enable(root, args.author, args.period, args.index)
|
|
103
106
|
print("memory: packages mode on (%s); every teammate's next close exports" % sync.POLICY_REL)
|
|
104
107
|
print("pre-push: %s" % hook)
|
|
@@ -22,10 +22,8 @@ from rr_state import task
|
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def profile_file(home):
|
|
25
|
-
"""<home>/riskrails/profile.json
|
|
26
|
-
|
|
27
|
-
path = os.path.join(home, "riskrails", "profile.json")
|
|
28
|
-
return path if os.path.exists(path) else os.path.join(home, "claude-agentic", "profile.json")
|
|
25
|
+
"""<home>/riskrails/profile.json."""
|
|
26
|
+
return os.path.join(home, "riskrails", "profile.json")
|
|
29
27
|
|
|
30
28
|
|
|
31
29
|
def read_profile(runtime=None, rt=rr_runtime.Runtime()):
|
|
@@ -241,7 +241,7 @@ def cmd_questions(args, root, rt=rr_runtime.Runtime()):
|
|
|
241
241
|
# into the file must be in the state and the history too.
|
|
242
242
|
# Only a sync that changed something writes at all — questions.pending
|
|
243
243
|
# is a cache, and guard_pending re-parses the file anyway, so a read
|
|
244
|
-
# (which /
|
|
244
|
+
# (which /rr-status and the session-start hook both perform) never
|
|
245
245
|
# races a command that is mid-write.
|
|
246
246
|
_, questions = rr_questions.parse_questions(path)
|
|
247
247
|
rr_questions.set_pending(root, state, questions, path)
|
|
@@ -154,11 +154,10 @@ def schema_line(root):
|
|
|
154
154
|
project = "0"
|
|
155
155
|
here = os.path.dirname(os.path.dirname(fs.ENTRY))
|
|
156
156
|
try:
|
|
157
|
-
|
|
157
|
+
with open(os.path.join(here, "ai-init", "templates", ".ai", "VERSION"), encoding="utf-8") as fh:
|
|
158
|
+
plugin = fh.read().strip() or "?"
|
|
158
159
|
except OSError:
|
|
159
|
-
|
|
160
|
-
numbers = [int(n[:4]) for n in names if re.match(r"\d{4}_", n)]
|
|
161
|
-
plugin = str(max(numbers)) if numbers else "?"
|
|
160
|
+
plugin = "?"
|
|
162
161
|
behind = project.isdigit() and plugin.isdigit() and int(project) < int(plugin)
|
|
163
162
|
return "schema: project %s · plugin %s%s" % (project, plugin, " — behind: run /rr-setup --update" if behind else "")
|
|
164
163
|
|
|
@@ -69,7 +69,7 @@ def installed(runtime):
|
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
# Set by the runtimes in every process they start, the `!` prefix's shell
|
|
72
|
-
# included. Duplicated on purpose from
|
|
72
|
+
# included. Duplicated on purpose from the removed adopt script
|
|
73
73
|
# (AGENT_MARKERS): the two skills are installed independently and neither
|
|
74
74
|
# imports the other.
|
|
75
75
|
AGENT_MARKERS = ("CLAUDECODE", "CODEX_THREAD_ID", "CODEX_SESSION_ID", "CODEX_CI",
|
|
@@ -39,15 +39,15 @@ def cmd_weight(args, root, rt=rr_runtime.Runtime()):
|
|
|
39
39
|
except (OSError, ValueError, RecursionError) as exc:
|
|
40
40
|
fs.die("cannot read the weight table: %s" % exc)
|
|
41
41
|
if args.mirror:
|
|
42
|
-
|
|
43
|
-
if path
|
|
44
|
-
fs.die("%s has no .ai/policies
|
|
42
|
+
policies = os.path.join(root, ".ai", "policies")
|
|
43
|
+
if not os.path.isdir(policies):
|
|
44
|
+
fs.die("%s has no .ai/policies/: the mirror is written beside the project's own table"
|
|
45
45
|
% root)
|
|
46
|
-
target = os.path.join(
|
|
46
|
+
target = os.path.join(policies, "weight.md")
|
|
47
47
|
tmp = "%s.%d.tmp" % (target, os.getpid())
|
|
48
48
|
try:
|
|
49
49
|
with open(tmp, "w", encoding="utf-8") as fh:
|
|
50
|
-
fh.write(weight.mirror(
|
|
50
|
+
fh.write(weight.mirror(policy))
|
|
51
51
|
os.replace(tmp, target)
|
|
52
52
|
except OSError as exc:
|
|
53
53
|
with contextlib.suppress(OSError):
|
|
@@ -70,9 +70,9 @@ def cmd_weight(args, root, rt=rr_runtime.Runtime()):
|
|
|
70
70
|
except ValueError as exc:
|
|
71
71
|
fs.die(str(exc))
|
|
72
72
|
except (TypeError, AttributeError) as exc: # a table whose values have the wrong shape
|
|
73
|
-
fs.die("%s is malformed: %s" % (path, exc))
|
|
73
|
+
fs.die("%s is malformed: %s" % (path[-1] if os.path.isfile(path[-1]) else path[0], exc))
|
|
74
74
|
if args.json:
|
|
75
|
-
out = dict(result.as_dict(), policy=path)
|
|
75
|
+
out = dict(result.as_dict(), policy=path[0])
|
|
76
76
|
if result.documents_only is not None:
|
|
77
77
|
out["documents_only"] = result.documents_only
|
|
78
78
|
print(json.dumps(out, indent=2, ensure_ascii=False))
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
Why a separate module: state.py owns every write to state.json and has no
|
|
5
5
|
subprocess use at all; this module shells out to git and to the commands written
|
|
6
|
-
in .ai/policies/
|
|
6
|
+
in .ai/policies/verification.md, imports nothing from state.py, and never opens
|
|
7
7
|
state.json for writing. state.py imports it, not the reverse. Since redesign
|
|
8
8
|
2.6 it measures nothing else: the size budget, the re-score and the static
|
|
9
9
|
sensors (bite, duplicates, traceability, plan sections, lint, typecheck, detect)
|
|
@@ -213,11 +213,11 @@ SCOPE_FIELD = {"suite": "verify_command", "step": "step_test_command",
|
|
|
213
213
|
|
|
214
214
|
|
|
215
215
|
def testing_path(root):
|
|
216
|
-
return os.path.join(root, ".ai", "policies", "
|
|
216
|
+
return os.path.join(root, ".ai", "policies", "verification.md")
|
|
217
217
|
|
|
218
218
|
|
|
219
219
|
def read_commands(root):
|
|
220
|
-
"""The project's own commands, as a human wrote them into
|
|
220
|
+
"""The project's own commands, as a human wrote them into verification.md.
|
|
221
221
|
|
|
222
222
|
A line's value is everything after the colon, minus a trailing comment; the
|
|
223
223
|
template ships the fields with an explanatory comment and no value, and that
|
|
@@ -253,7 +253,7 @@ def _escaped(text):
|
|
|
253
253
|
|
|
254
254
|
def bite_codes(root):
|
|
255
255
|
"""The exit codes that count as "the test fails" under --expect fail: the
|
|
256
|
-
|
|
256
|
+
verification.md `bite_codes` list of integers 1-255, 1 when absent or not a clean list
|
|
257
257
|
(an empty part, a non-integer, 0, a negative or a value over 255 all fall back)."""
|
|
258
258
|
raw = read_commands(root).get("bite_codes", "")
|
|
259
259
|
try:
|
|
@@ -349,8 +349,6 @@ def resolve_trees(root, args):
|
|
|
349
349
|
from_tree = args.from_tree
|
|
350
350
|
if not from_tree:
|
|
351
351
|
state = os.path.join(root, ".ai", "state", "state.json")
|
|
352
|
-
if not os.path.isfile(state):
|
|
353
|
-
state = os.path.join(root, ".ai", "state", "current.json") # R34: legacy name, one version
|
|
354
352
|
try:
|
|
355
353
|
with open(state, encoding="utf-8") as fh:
|
|
356
354
|
from_tree = ((json.load(fh) or {}).get("diff") or {}).get("base_tree")
|
|
@@ -387,11 +385,12 @@ def cmd_diff(args, root):
|
|
|
387
385
|
|
|
388
386
|
|
|
389
387
|
def tests_config(root):
|
|
390
|
-
"""The runner's caps from weight
|
|
388
|
+
"""The runner's caps from the weight table's `tests` (the plugin's, the project's
|
|
389
|
+
weight.project.json over it), {} when the table cannot be read."""
|
|
391
390
|
try:
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
except (OSError, ValueError, AttributeError):
|
|
391
|
+
import weight # pylint: disable=import-outside-toplevel # sits beside this file; only cmd_run needs it
|
|
392
|
+
config = weight.load_policy(root)[0].get("tests") or {}
|
|
393
|
+
except (ImportError, OSError, ValueError, AttributeError):
|
|
395
394
|
config = {}
|
|
396
395
|
return config if isinstance(config, dict) else {}
|
|
397
396
|
|
|
@@ -400,7 +399,7 @@ def cmd_run(args, root):
|
|
|
400
399
|
config = tests_config(root)
|
|
401
400
|
command = command_for(root, args.scope)
|
|
402
401
|
if not command:
|
|
403
|
-
print("%-13s UNAVAIL %s is not written down in .ai/policies/
|
|
402
|
+
print("%-13s UNAVAIL %s is not written down in .ai/policies/verification.md"
|
|
404
403
|
% (args.scope, SCOPE_FIELD.get(args.scope, args.scope)))
|
|
405
404
|
return UNKNOWNCODE
|
|
406
405
|
files = [f.strip() for f in (args.files or "").split(",") if f.strip()]
|
package/skills/ai-task/state.py
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Task state for /
|
|
2
|
+
"""Task state for /rr-build — the small, machine-readable record of one task.
|
|
3
3
|
|
|
4
4
|
Why a file and not the conversation: a conversation is expensive to re-read and
|
|
5
5
|
impossible for a hook to consult. This file is what ai-scope-guard enforces
|
|
6
|
-
against, what /
|
|
6
|
+
against, what /rr-status reports, and what lets a task survive a /clear or a
|
|
7
7
|
crash. It holds facts, never transcripts.
|
|
8
8
|
|
|
9
9
|
Everything is stdlib. Writes are atomic (temp file + os.replace), so a hook that
|
|
10
10
|
reads the file while a stage is being recorded never sees half a document.
|
|
11
11
|
|
|
12
12
|
Every state change is also appended to .ai/reports/<task-id>/events.jsonl, the
|
|
13
|
-
append-only journal /
|
|
13
|
+
append-only journal /rr-status and /rr-status --cost read at zero model cost. The
|
|
14
14
|
journal is best effort by contract: a line that cannot be written never fails
|
|
15
15
|
the command that emitted it, and nothing here depends on it being lossless.
|
|
16
16
|
history[] keeps its exact shape and its original event names beside it.
|
|
@@ -57,8 +57,7 @@ redesign 2.6) answers status, handoff and questions, and close --abandon drops
|
|
|
57
57
|
it; every other command refuses it.
|
|
58
58
|
"""
|
|
59
59
|
|
|
60
|
-
# pylint: disable=
|
|
61
|
-
import json
|
|
60
|
+
# pylint: disable=wrong-import-position
|
|
62
61
|
import os
|
|
63
62
|
import sys
|
|
64
63
|
|
|
@@ -68,26 +67,7 @@ try:
|
|
|
68
67
|
except ImportError:
|
|
69
68
|
sys.exit("rr_state/ is missing beside state.py: re-run `npx riskrails`")
|
|
70
69
|
fs.ENTRY = os.path.abspath(__file__) # R6a: the one path the package prints and compares
|
|
71
|
-
|
|
72
|
-
# The names the suites reach as state_mod.<name> (R6) and json_loads_capped, which
|
|
73
|
-
# tests/test_dialog_contract.py:176,189 reach as module.json_loads_capped over (dialog, state_mod);
|
|
74
|
-
# json and os are imported above for the same reason: the suites patch state_mod.json.dump and state_mod.os.
|
|
75
70
|
from rr_state.cli import main
|
|
76
|
-
from rr_state.fs import current_tree, read_session, session_path, state_path
|
|
77
|
-
from rr_state.runtime import AGENT_MARKERS
|
|
78
|
-
from rr_state.journal import append_journal, journal_line, read_journal
|
|
79
|
-
from rr_state.task import TASK_ID_RE, abandon_old, load, next_task_id, sealed_task_ids, valid_task_id
|
|
80
|
-
from rr_state.policy import NEVER_DOCUMENTS, is_document, mode_for, rescore_weight
|
|
81
|
-
from rr_state.locks import LOCK_STALE
|
|
82
|
-
from rr_state.questions import parse_questions
|
|
83
|
-
from rr_state.questions_cmd import close_gate_from_file, sync_answers
|
|
84
|
-
from rr_state.ledger import LEDGER_LINE_MAX, LEDGER_TYPES, live_picks, read_ledger
|
|
85
|
-
from rr_state.dialog import DIALOG_TOOL, JSON_DEPTH_MAX, QUESTION_MAX, SESSION_ENV, json_loads_capped
|
|
86
|
-
from rr_state.dialog import dialog_on, dialog_payload, dialog_policy, dialog_tool_verified, task_facts
|
|
87
|
-
from rr_state.labels import LABEL_MAX, validate_label
|
|
88
|
-
from rr_state.gate import confirm_names
|
|
89
|
-
from rr_state.profile import jwt_plan_type, read_json_object, read_profile, read_quota
|
|
90
|
-
from rr_state.close import archive_v7
|
|
91
71
|
|
|
92
72
|
if __name__ == "__main__":
|
|
93
73
|
main()
|
package/skills/ai-task/weight.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Weight — six axes, 0 to 2 each, from .ai/policies/weight.json.
|
|
1
|
+
"""Weight — six axes, 0 to 2 each, from the plugin's weight table with .ai/policies/weight.project.json laid over it.
|
|
2
2
|
|
|
3
3
|
Pure computation: it reads the declared files, the tree listing and `git grep`,
|
|
4
4
|
and writes nothing. A measured value is a floor: the session may raise an axis
|
|
@@ -39,15 +39,10 @@ class Weight:
|
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
def load_policy(root):
|
|
42
|
-
"""(policy,
|
|
43
|
-
|
|
44
|
-
own = os.path.join(root, ".ai", "policies", "weight.json")
|
|
45
|
-
|
|
46
|
-
with open(path, encoding="utf-8") as fh:
|
|
47
|
-
policy = json.load(fh)
|
|
48
|
-
if not isinstance(policy, dict) or not all(isinstance(policy.get(k), dict) for k in ("axes", "modes")):
|
|
49
|
-
raise ValueError("%s has no axes or modes table" % path)
|
|
50
|
-
return policy, path
|
|
42
|
+
"""(policy, [shipped, project]): the shipped table with the project's weight.project.json laid over it."""
|
|
43
|
+
import weight_overlay # pylint: disable=import-outside-toplevel # beside this file; update.py loads this file by path
|
|
44
|
+
own = os.path.join(root, ".ai", "policies", "weight.project.json")
|
|
45
|
+
return weight_overlay.load(SHIPPED, own), [SHIPPED, own]
|
|
51
46
|
|
|
52
47
|
|
|
53
48
|
@functools.lru_cache(maxsize=None)
|
|
@@ -268,14 +263,19 @@ def render(result, long=False):
|
|
|
268
263
|
return "\n".join(lines)
|
|
269
264
|
|
|
270
265
|
|
|
271
|
-
def mirror(
|
|
272
|
-
"""weight.md for the
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
266
|
+
def mirror(source):
|
|
267
|
+
"""weight.md for `source`: a policy dict (the effective table, sha256 of its canonical JSON) or
|
|
268
|
+
the path of one JSON file (sha256 of its bytes)."""
|
|
269
|
+
if isinstance(source, dict):
|
|
270
|
+
policy, origin = source, "the plugin's weight table plus weight.project.json"
|
|
271
|
+
raw = json.dumps(source, sort_keys=True, ensure_ascii=False).encode("utf-8")
|
|
272
|
+
else:
|
|
273
|
+
with open(source, "rb") as fh:
|
|
274
|
+
raw = fh.read()
|
|
275
|
+
policy, origin = json.loads(raw.decode("utf-8")), "weight.json"
|
|
276
276
|
out = ["# Weight table", "",
|
|
277
|
-
"<!-- generated by `state.py weight --mirror` from
|
|
278
|
-
" sha256: %s -->" % hashlib.sha256(raw).hexdigest(), "",
|
|
277
|
+
"<!-- generated by `state.py weight --mirror` from %s; edit the JSON and regenerate."
|
|
278
|
+
" sha256: %s -->" % (origin, hashlib.sha256(raw).hexdigest()), "",
|
|
279
279
|
"Six axes, 0–2 each; W is their sum. Why each rule is what it is: the plugin's `docs/weight.md`.", "",
|
|
280
280
|
"| Axis | Measured by | Table |", "|---|---|---|"]
|
|
281
281
|
for axis in AXES:
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""The weight table a project sees: the plugin's shipped table with the project's
|
|
2
|
+
weight.project.json laid over it (R9). weight.py's `load_policy` calls `load`.
|
|
3
|
+
|
|
4
|
+
Objects merge recursively, any other value (a list too) replaces. A file that is not
|
|
5
|
+
a JSON object, or a result without the axes and modes tables, raises ValueError
|
|
6
|
+
naming the file; nothing falls back to a default.
|
|
7
|
+
"""
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _table(path):
|
|
13
|
+
"""One JSON object from `path`; ValueError naming the file when it is not one."""
|
|
14
|
+
try:
|
|
15
|
+
with open(path, encoding="utf-8") as fh:
|
|
16
|
+
table = json.load(fh)
|
|
17
|
+
if isinstance(table, dict):
|
|
18
|
+
return table
|
|
19
|
+
raise ValueError("not a JSON object")
|
|
20
|
+
except ValueError as exc:
|
|
21
|
+
raise ValueError("%s: %s" % (path, exc)) from exc
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _overlay(base, top):
|
|
25
|
+
"""`top` laid over `base`: dicts merge, any other value (a list too) replaces."""
|
|
26
|
+
for key, value in top.items():
|
|
27
|
+
base[key] = _overlay(base[key], value) if isinstance(value, dict) and isinstance(base.get(key), dict) else value
|
|
28
|
+
return base
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def load(shipped, own):
|
|
32
|
+
"""The table at `shipped`, with `own` laid over it when that file exists."""
|
|
33
|
+
policy = _table(shipped)
|
|
34
|
+
if os.path.isfile(own):
|
|
35
|
+
_overlay(policy, _table(own))
|
|
36
|
+
if not all(isinstance(policy.get(k), dict) for k in ("axes", "modes")):
|
|
37
|
+
raise ValueError("%s has no axes or modes table" % own)
|
|
38
|
+
return policy
|