riskrails 4.0.1 → 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 +22 -13
- package/bin/menu.js +116 -0
- package/bin/riskrails.js +119 -20
- package/hooks/guard/dialog.py +14 -2
- package/package.json +1 -1
- package/skills/ai-init/scaffold-ai.sh +2 -1
- package/skills/ai-init/templates/.ai/policies/safety.md +4 -3
- package/skills/ai-task/rr_state/cli.py +1 -1
- package/skills/ai-task/rr_state/dialog_cmd.py +183 -6
- package/skills/ai-task/rr_state/labels.py +14 -2
- package/skills/ai-task/rr_state/memory_cmd.py +7 -4
- package/skills/project-update/history/blobs/8ec171f0fbdf38cc6996d496f01491fd1f21f04a435a118ad979b127a8be06f1 +85 -0
- package/skills/project-update/history/index.json +2 -1
- package/skills/rr-setup/SKILL.md +75 -19
- package/skills/rr-setup/scaffold_ai.py +8 -2
- package/skills/rr-setup/setup.py +370 -4
|
@@ -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()):
|
|
@@ -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)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Safety policy
|
|
2
|
+
|
|
3
|
+
This is the file every agent reads first. It is short on purpose. It binds every
|
|
4
|
+
agent in every runtime — Claude Code and Codex alike.
|
|
5
|
+
|
|
6
|
+
## The one rule
|
|
7
|
+
|
|
8
|
+
**Production behaviour is the source of truth until proven otherwise.** Code that
|
|
9
|
+
looks wrong, dead, or badly written may be load-bearing. The repository's history
|
|
10
|
+
is evidence; your intuition about how the code "should" look is not.
|
|
11
|
+
|
|
12
|
+
## What an agent must never do
|
|
13
|
+
|
|
14
|
+
- Change application code that the current task did not ask about.
|
|
15
|
+
- Rename, move, reorganise or "clean up" anything as a side effect.
|
|
16
|
+
- Remove code because it looks unused. Find the callers first; if there are none,
|
|
17
|
+
say so in the report and leave the code alone.
|
|
18
|
+
- Combine a refactoring with a feature change in the same step.
|
|
19
|
+
- Modify database schemas, dependencies or framework versions outside a task that
|
|
20
|
+
is explicitly about that.
|
|
21
|
+
- Weaken a test, a linter rule or a static-analysis level to make a check pass.
|
|
22
|
+
- Commit, push, merge or deploy on its own initiative.
|
|
23
|
+
- Read production secrets or customer data into the model context.
|
|
24
|
+
|
|
25
|
+
If you find a problem outside the current task: **write it down, do not fix it.**
|
|
26
|
+
Unrelated findings are recorded with `state.py memory risk add` (in files mode it
|
|
27
|
+
writes `.ai/project/known-risks.md`) or go into the report's "observations"
|
|
28
|
+
section, and the human decides whether they become their own task.
|
|
29
|
+
|
|
30
|
+
The compact record is the human's to switch: `state.py memory enable`, `disable`,
|
|
31
|
+
`cleanup`, `pack`, `redact` and `migrate --move-logs` take the human route, as
|
|
32
|
+
`approve` does. `enable` may also run as the command the human picked in the
|
|
33
|
+
`/rr-setup` dialog; the rest have no dialog route. `.ai/memory/` and
|
|
34
|
+
`.ai/local/memory/` are written by `state.py` alone — an agent never edits them
|
|
35
|
+
(`state.py memory --help`; the plugin's `docs/local-memory.md`).
|
|
36
|
+
|
|
37
|
+
## Scope
|
|
38
|
+
|
|
39
|
+
Every implementation step names the files it may touch. Editing anything else is
|
|
40
|
+
refused by `ai-scope-guard` and must be answered with `SCOPE_CHANGE_REQUIRED`:
|
|
41
|
+
stop, say which file you need and why, and let the plan be amended. A step whose
|
|
42
|
+
scope keeps growing is a step that was planned wrong.
|
|
43
|
+
|
|
44
|
+
`ai-git-guard`, `ai-path-guard` and `ai-scope-guard` all run in both runtimes. A
|
|
45
|
+
multi-file edit in one call — Codex's `apply_patch` — is checked path by path, so
|
|
46
|
+
a single out-of-scope or protected file rejects the whole patch. What a guard
|
|
47
|
+
refuses is not a hint: do not reword the command to get past it.
|
|
48
|
+
|
|
49
|
+
A guard is a backstop, not the policy. Some checks exist in only one runtime —
|
|
50
|
+
Codex has no hookable read tool, so the large-read cap is Claude-only there — and
|
|
51
|
+
an agent that behaves correctly only because a hook is watching is not behaving
|
|
52
|
+
correctly. The rules above hold whether or not a hook enforces them.
|
|
53
|
+
|
|
54
|
+
**The guards' threat model.** The guards defend against an agent's mistakes — a
|
|
55
|
+
careless `rm`, a write to a frozen policy during a task, a commit with a
|
|
56
|
+
secret — not against an agent that constructs a command to exhaust them. A
|
|
57
|
+
command built to time the guard out is visible in the transcript and in the
|
|
58
|
+
journal, and the human gate in full and full_irreversible mode is the boundary
|
|
59
|
+
for anything that matters.
|
|
60
|
+
Approval rests on a guard plus a ledger: a command the agent constructs to pass
|
|
61
|
+
for the human's pick is outside the threat model, and `state.py consent audit`
|
|
62
|
+
detects it afterwards.
|
|
63
|
+
|
|
64
|
+
## Evidence labels
|
|
65
|
+
|
|
66
|
+
Anything an agent writes into `.ai/project/` carries one of four labels:
|
|
67
|
+
|
|
68
|
+
- **KNOWN FACT** — read from the code, a config file, a migration or a test.
|
|
69
|
+
Cite `file:line`.
|
|
70
|
+
- **INFERENCE** — a conclusion drawn from facts. Say what it is drawn from.
|
|
71
|
+
- **UNKNOWN** — something that matters and could not be determined. Say what
|
|
72
|
+
would answer it.
|
|
73
|
+
- **RISK** — something that looks dangerous. Say what could go wrong.
|
|
74
|
+
|
|
75
|
+
Never present an inference as a fact. When documentation and code disagree,
|
|
76
|
+
record both under "Documented behaviour" and "Observed behaviour", with the
|
|
77
|
+
evidence for each, and let the human decide which is the bug.
|
|
78
|
+
|
|
79
|
+
## When in doubt
|
|
80
|
+
|
|
81
|
+
Stop and ask. An agent that pauses costs a message; an agent that guesses on a
|
|
82
|
+
payment path costs an incident.
|
|
83
|
+
|
|
84
|
+
Doubt on reversibility or domain is a blocker; other doubt is written to
|
|
85
|
+
`task.md` and the work continues.
|
|
@@ -171,7 +171,8 @@
|
|
|
171
171
|
"ae9a22907ba9acbb7e80fac1ba4e3d1ff09e5f1df5c4456afb9099d7090fcaa6",
|
|
172
172
|
"fae3064e648cdc55be4aeb1177525b302a5e34ebd752e4a904f47dc0f4557b96",
|
|
173
173
|
"e9adb7a9905eb719ac78ab96671f055da53e5001810f5ad6d6c94306b0323fe7",
|
|
174
|
-
"ae5ce131d6bae401199ff32d8c3dc4d8aec188010a9111624ac64506e2116ff4"
|
|
174
|
+
"ae5ce131d6bae401199ff32d8c3dc4d8aec188010a9111624ac64506e2116ff4",
|
|
175
|
+
"8ec171f0fbdf38cc6996d496f01491fd1f21f04a435a118ad979b127a8be06f1"
|
|
175
176
|
],
|
|
176
177
|
"ai-init/.ai/policies/security.md": [
|
|
177
178
|
"cc808d5b940109f1703abfb3ca0334a3fcfd34267feefb2323627d0decc8b89a",
|
package/skills/rr-setup/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: rr-setup
|
|
3
|
-
description: Set up and maintain the .ai/ layer of an existing production codebase. With no flag, scaffold docs/sdlc/, the runtime directory and .ai/, survey the code into .ai/project/ (light or full), write the Verification commands, the weight table's project entries and the constitution — read-only towards application code. --update regenerates a project's files from the installed plugin (a copy that is a shipped version is replaced, an edited one waits for the human, the project's own files are kept); run it right after installing a new riskrails; --audit scores the repository against the twelve plays and writes docs/ai-sdlc-adoption-plan.md. Use on "/rr-setup", "set up this project", "analyse this project for AI work", "update the rules", "обнови правилата", "audit our AI setup", or when /rr-status says the project is behind.
|
|
4
|
-
argument-hint: [--survey light|full] | --update [--apply] | --audit [context]
|
|
3
|
+
description: Set up and maintain the .ai/ layer of an existing production codebase. With no flag, scaffold docs/sdlc/, the runtime directory and .ai/, ask the open options (memory, runtimes, survey depth, verify command) in one dialog, survey the code into .ai/project/ (light or full), write the Verification commands, the weight table's project entries and the constitution — read-only towards application code. --options asks them all again; --update regenerates a project's files from the installed plugin (a copy that is a shipped version is replaced, an edited one waits for the human, the project's own files are kept); run it right after installing a new riskrails; --audit scores the repository against the twelve plays and writes docs/ai-sdlc-adoption-plan.md. Use on "/rr-setup", "set up this project", "analyse this project for AI work", "update the rules", "обнови правилата", "audit our AI setup", or when /rr-status says the project is behind.
|
|
4
|
+
argument-hint: [--survey light|full] | --options | --update [--apply] | --audit [context]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# /rr-setup $ARGUMENTS
|
|
@@ -17,29 +17,74 @@ UPDATE="python3 $AI_HOME/skills/project-update/update.py $PWD"
|
|
|
17
17
|
SETUP="python3 $AI_HOME/skills/rr-setup/setup.py"
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
`--update` and `--audit` go to their sections below; anything else
|
|
20
|
+
`--options`, `--update` and `--audit` go to their sections below; anything else
|
|
21
21
|
is **Setup**. In every mode: no application code changes — not a rename, not a
|
|
22
22
|
formatting fix. Problems you find are written down, never fixed. Never read
|
|
23
23
|
`.env`, secrets, dumps or production logs. Do not commit; suggest the `git add`.
|
|
24
24
|
|
|
25
25
|
## Setup
|
|
26
26
|
|
|
27
|
+
0. **Options.** Project memory, the runtimes, the survey depth and `verify_command`
|
|
28
|
+
are asked once, in one dialog, before any work; the picks feed steps 1, 3 and 5.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
"$AI_HOME/skills/ai-init/scaffold-ai.sh" "$PWD" --runtime none # 0a
|
|
32
|
+
$SETUP options --open # 0b; --options adds --all, a given --survey adds --skip survey
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**0a** makes `.ai/` (so `.ai/policies/dialog.json`) and its `.gitignore` lines, with no
|
|
36
|
+
instruction file and no runtime directory; only this script takes `none`
|
|
37
|
+
(`project-scaffold.sh` refuses it). If its output does **not** list
|
|
38
|
+
`.ai/project/overview.md`, the project was set up before.
|
|
39
|
+
|
|
40
|
+
**0b** prints one of three first lines:
|
|
41
|
+
- `OPTIONS_DIALOG D<n> …`, then state.py's `DIALOG_PENDING` line and a JSON: send that
|
|
42
|
+
JSON exactly as printed with the dialog tool the line names (AskUserQuestion; Codex:
|
|
43
|
+
request_user_input).
|
|
44
|
+
- `OPTIONS_FALLBACK <why>`: nothing was opened. Ask the numbered questions after it in
|
|
45
|
+
chat as printed, the answers being the picks; show its `memory is the human's` line
|
|
46
|
+
as printed and never run it.
|
|
47
|
+
- `OPTIONS_NONE`: nothing to ask.
|
|
48
|
+
|
|
49
|
+
Exit 6 (no first line, the reason on stderr): `DECISION_LIMIT` or `DECISION_DUPLICATE`
|
|
50
|
+
means a decision is already open — settle every one first (`$STATE decision show` and
|
|
51
|
+
send it, or `$STATE decision fallback --id <Dn> --why "<why>"`), then run 0b once more.
|
|
52
|
+
Any other refusal, or a second one: ask the questions of steps 1, 3 and 5 in prose and,
|
|
53
|
+
in a git work tree, give the human for their own terminal, with `$AI_HOME` and `$PWD`
|
|
54
|
+
substituted: `python3 "<home>/skills/ai-task/state.py" --root "<project>" memory enable
|
|
55
|
+
--period quarter [--index sqlite]`.
|
|
56
|
+
|
|
57
|
+
**0c** — the human answers. After a dialog, a memory command they picked is run first,
|
|
58
|
+
at once, verbatim and alone; it prints `memory: via dialog (consent C<n>, decision D<n>)`
|
|
59
|
+
and its pick settles its decision. Then `$STATE decision settle --id <Dn>` for each other
|
|
60
|
+
decision: it prints `D<n> settled: <label> (set)` and `SETUP_PICK <key>=<value>`, after
|
|
61
|
+
a decline `SETUP_PICK <key>=`. Apply each pick where its step needs it:
|
|
62
|
+
- `runtime=<v>` → step 1's two scaffolds with `--runtime <v>`;
|
|
63
|
+
- `verify=<cmd>` → `$SETUP verify --set "<cmd>"` after step 2; `keep` and `edit` → nothing;
|
|
64
|
+
- `survey=light` or `full` → step 3's depth; `keep` → no new survey;
|
|
65
|
+
- `memory=off` or `keep` → nothing: a picked command already ran.
|
|
66
|
+
|
|
67
|
+
An empty value (`verify=` is "Not now"; any key after a decline) or `other` → the step
|
|
68
|
+
as it was without the dialog: step 1 passes `auto`, step 3 surveys `light`, step 5 asks
|
|
69
|
+
in chat.
|
|
27
70
|
1. **Scaffold.** Both scripts are idempotent, never overwrite, and print what
|
|
28
|
-
they created; note it. They take `--runtime auto|claude|codex|both`
|
|
29
|
-
follows `CLAUDE.md`/`.claude/` and `AGENTS.md`/`.codex/`); pass
|
|
30
|
-
the repository is worked on from
|
|
71
|
+
they created; note it, 0a's too. They take `--runtime auto|claude|codex|both`
|
|
72
|
+
(`auto` follows `CLAUDE.md`/`.claude/` and `AGENTS.md`/`.codex/`); pass the
|
|
73
|
+
`runtime` pick, else `auto` — `both` when the repository is worked on from
|
|
74
|
+
both runtimes.
|
|
31
75
|
|
|
32
76
|
```bash
|
|
33
|
-
"$AI_HOME/hooks/project-scaffold.sh" "$PWD"
|
|
34
|
-
"$AI_HOME/skills/ai-init/scaffold-ai.sh" "$PWD"
|
|
77
|
+
"$AI_HOME/hooks/project-scaffold.sh" "$PWD" --runtime <pick> # docs/sdlc/, the runtime dir, .gitignore
|
|
78
|
+
"$AI_HOME/skills/ai-init/scaffold-ai.sh" "$PWD" --runtime <pick> # the instruction file; .ai/ is 0a's
|
|
35
79
|
```
|
|
36
80
|
|
|
37
|
-
If `.ai/project/overview.md
|
|
81
|
+
If 0a did **not** create `.ai/project/overview.md`, the project was set up
|
|
38
82
|
before: run **--update** below, then ask whether the survey should be
|
|
39
83
|
refreshed; a refresh never overwrites a `.ai/project/` file a human edited
|
|
40
|
-
without saying so first. If
|
|
41
|
-
wanted), fill its Commands / Conventions /
|
|
42
|
-
one line per command, five sentences of
|
|
84
|
+
without saying so first. If 0a and this step together created only an
|
|
85
|
+
instruction file (no `.ai/` wanted), fill its Commands / Conventions /
|
|
86
|
+
Architecture from the manifests — one line per command, five sentences of
|
|
87
|
+
architecture — and stop.
|
|
43
88
|
2. **Stack.** `$SETUP survey` first: the project's own toolbox
|
|
44
89
|
(`.claude/{skills,commands,rules,agents,hooks}`, `.codex/*`, `bin/*.sh`,
|
|
45
90
|
Makefile targets) with each item's purpose, the guards a project hook
|
|
@@ -51,13 +96,15 @@ formatting fix. Problems you find are written down, never fixed. Never read
|
|
|
51
96
|
(`phpunit.xml*`, `phpstan.neon*`, `playwright.config.*` …). For Symfony or
|
|
52
97
|
Sylius also: bundles, entities, state machines, Messenger, event
|
|
53
98
|
subscribers, payment, shipping, tax, promotions, ERP or fiscal integrations.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
99
|
+
Then write the `verify=<cmd>` pick, if any (0c).
|
|
100
|
+
3. **Survey.** The depth is the `survey` pick or `--survey light|full`. `light`,
|
|
101
|
+
the default, is for a developer who knows the project: ask for a ten-minute
|
|
102
|
+
brain dump (entry points, modules that matter, integrations,
|
|
103
|
+
unwritten business rules, areas nobody dares touch), confirm
|
|
57
104
|
each claim with `grep -n`, then fan out exactly **two** `ai-discovery` agents
|
|
58
105
|
in one message: (1) legacy, risks and code that looks unused; (2) tests,
|
|
59
106
|
CI/CD, data and configuration — how secrets are supplied, never their
|
|
60
|
-
values.
|
|
107
|
+
values. `full`, for a codebase new to you: one `Explore` for the
|
|
61
108
|
inventory, then six `ai-discovery` agents in one message — entry points;
|
|
62
109
|
domain modules; integrations and their failure handling; business rules and
|
|
63
110
|
legacy; tests and CI/CD; data and configuration. Hand each the slice of the
|
|
@@ -68,14 +115,15 @@ formatting fix. Problems you find are written down, never fixed. Never read
|
|
|
68
115
|
write `Documented behaviour / Observed behaviour / Evidence / Risk` and do
|
|
69
116
|
not resolve it.
|
|
70
117
|
5. **Policies.** Fill the project sections of `.ai/policies/coding.md` and
|
|
71
|
-
`testing.md` only if
|
|
118
|
+
`testing.md` only if 0a created them. The **Verification** block of
|
|
72
119
|
`testing.md` is the feedback loop every task runs:
|
|
73
120
|
- `verify_command` — the fast suite to the end, the e2e group **excluded**;
|
|
74
121
|
- `step_test_command` — the same runner on given paths (`{files}`);
|
|
75
122
|
- `e2e_command` — the end-to-end suite, or `none` when there is none;
|
|
76
123
|
- two or three lines of what healthy output looks like.
|
|
77
124
|
|
|
78
|
-
`verify_command` comes from the human:
|
|
125
|
+
`verify_command` comes from the human: the `verify=<cmd>` pick is their yes,
|
|
126
|
+
written at the end of step 2; without one, show the one `$SETUP survey`
|
|
79
127
|
proposed, ask, and only after a yes in chat write it with
|
|
80
128
|
`$SETUP verify --set "<command>"` (exit 3: one is already set — the human
|
|
81
129
|
edits it). `$SETUP check` then exits 0; `VERIFY_COMMAND_MISSING` (exit 1)
|
|
@@ -109,7 +157,7 @@ formatting fix. Problems you find are written down, never fixed. Never read
|
|
|
109
157
|
casually, the weight-table entries you chose, recommended next improvements —
|
|
110
158
|
recommendations only, none implemented. When the survey found a toolbox,
|
|
111
159
|
name each toolbox item the project keeps using; they stay where they are.
|
|
112
|
-
9. **Report.** Files created (a tree from the scaffold output), surprising
|
|
160
|
+
9. **Report.** Files created (a tree from the scaffold output, 0a's too), surprising
|
|
113
161
|
findings, the modes and what puts a change in each, the safety restrictions
|
|
114
162
|
in force (hooks versus policy), the tooling the workflows reuse, assumptions,
|
|
115
163
|
unresolved questions and who can answer them, the survey's superpowers
|
|
@@ -118,6 +166,14 @@ formatting fix. Problems you find are written down, never fixed. Never read
|
|
|
118
166
|
`/rr-plan <it>`. Claim nothing was validated that was not run. Suggest
|
|
119
167
|
`git add .ai .gitignore docs/sdlc` plus the instruction files created.
|
|
120
168
|
|
|
169
|
+
## --options
|
|
170
|
+
|
|
171
|
+
Reopens every option on a set-up project: 0a, then `$SETUP options --open --all`, then
|
|
172
|
+
send, settle and apply as in step 0 — a `survey` pick runs steps 3–4 at that depth, as a
|
|
173
|
+
refresh (step 1), and a `verify` command is written at once. Say what changed and stop;
|
|
174
|
+
no other step reruns. A project 0a just scaffolded (it listed
|
|
175
|
+
`.ai/project/overview.md`) continues as the plain **Setup**.
|
|
176
|
+
|
|
121
177
|
## --update
|
|
122
178
|
|
|
123
179
|
The script regenerates; you show it, confirm what is owed, and merge what it
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""Idempotent .ai/ scaffold for RiskRails.
|
|
3
|
-
scaffold_ai.py [project-dir] [--runtime auto|claude|codex|both] (default: $PWD, auto)
|
|
3
|
+
scaffold_ai.py [project-dir] [--runtime auto|claude|codex|both|none] (default: $PWD, auto)
|
|
4
4
|
skills/ai-init/scaffold-ai.sh execs it; the templates stay in skills/ai-init/.
|
|
5
5
|
|
|
6
6
|
Never overwrites an existing file. Run it as often as you like: it creates what
|
|
@@ -14,6 +14,10 @@ that points at it differs — CLAUDE.md for Claude Code, AGENTS.md for Codex.
|
|
|
14
14
|
A port of the shell script it replaced, kept to its bytes: the same files and
|
|
15
15
|
modes, the same listing in the same order, the same messages and exit codes
|
|
16
16
|
(tests/test_scaffold_differential.py runs the frozen shell beside it).
|
|
17
|
+
|
|
18
|
+
One value is new, and the shell refuses it: `--runtime none` (or AI_RUNTIMES=none) scaffolds .ai/ and its
|
|
19
|
+
.gitignore lines and no instruction file, so /rr-setup can open .ai/ before the project's runtimes are chosen.
|
|
20
|
+
Like `both` and `auto` it is a whole value: inside a list it is an unknown runtime, as in the shell.
|
|
17
21
|
"""
|
|
18
22
|
import errno
|
|
19
23
|
import json
|
|
@@ -143,7 +147,9 @@ def logical_cd(root):
|
|
|
143
147
|
|
|
144
148
|
|
|
145
149
|
def runtimes(runtime, root):
|
|
146
|
-
"""The runtimes in scope: what the project declares (auto),
|
|
150
|
+
"""The runtimes in scope: what the project declares (auto), the list given, or none at all."""
|
|
151
|
+
if runtime == "none":
|
|
152
|
+
return set()
|
|
147
153
|
if runtime == "both":
|
|
148
154
|
runtime = "claude,codex"
|
|
149
155
|
if runtime == "auto":
|