riskrails 4.1.0 → 4.3.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 +25 -15
- package/agents/ai-reviewer.md.tmpl +14 -1
- package/ai/agents/implementer.md +6 -0
- package/ai/agents/reviewer.md +8 -1
- package/bin/ansi.js +12 -0
- package/bin/menu.js +71 -12
- package/bin/riskrails.js +240 -14
- package/bin/uninstall.js +115 -0
- package/bin/wizard.js +72 -0
- package/hooks/AGENTS.md +0 -6
- package/hooks/CLAUDE.md +0 -6
- package/hooks/context-guard.py +11 -1
- package/hooks/dialog-gate.py +3 -2
- package/hooks/guard/decision.py +4 -3
- package/hooks/guard/dialog.py +12 -2
- package/hooks/guard/ere.py +29 -10
- package/hooks/guard/git.py +2 -1
- package/hooks/guard/paths.py +48 -1406
- package/hooks/guard/paths_base.py +66 -0
- package/hooks/guard/paths_bash.py +176 -0
- package/hooks/guard/paths_classify.py +205 -0
- package/hooks/guard/paths_fs.py +137 -0
- package/hooks/guard/paths_human.py +105 -0
- package/hooks/guard/paths_interp.py +162 -0
- package/hooks/guard/paths_movers.py +393 -0
- package/hooks/guard/paths_shell.py +288 -0
- package/hooks/runtime-gate.py +87 -0
- package/hooks/update_notice.py +23 -8
- package/package.json +1 -1
- package/skills/AGENTS.md +0 -6
- package/skills/CLAUDE.md +0 -6
- package/skills/ai-init/templates/.ai/policies/coding.md +9 -0
- package/skills/ai-init/templates/gitignore.snippet +2 -0
- package/skills/ai-task/memory_cleanup.py +213 -0
- package/skills/ai-task/memory_compact.py +25 -0
- package/skills/ai-task/memory_config.py +120 -0
- package/skills/ai-task/memory_export.py +246 -0
- package/skills/ai-task/memory_format.py +7 -3
- package/skills/ai-task/memory_hook.py +172 -0
- package/skills/ai-task/memory_io.py +122 -0
- package/skills/ai-task/memory_migrate.py +187 -0
- package/skills/ai-task/memory_packages.py +83 -0
- package/skills/ai-task/memory_risks.py +84 -0
- package/skills/ai-task/memory_status.py +76 -0
- package/skills/ai-task/memory_sync.py +20 -1481
- package/skills/ai-task/memory_verify.py +316 -0
- package/skills/ai-task/rr_state/cli.py +1 -0
- package/skills/ai-task/rr_state/close.py +2 -2
- package/skills/ai-task/rr_state/dialog.py +32 -7
- package/skills/ai-task/rr_state/dialog_cmd.py +38 -5
- package/skills/ai-task/rr_state/gate.py +10 -4
- package/skills/ai-task/rr_state/labels.py +33 -4
- package/skills/ai-task/rr_state/memory_cmd.py +84 -64
- package/skills/ai-task/rr_state/questions_cmd.py +5 -4
- package/skills/ai-task/rr_state/report.py +73 -10
- package/skills/ai-task/rr_state/report_cmd.py +2 -0
- package/skills/ai-task/rr_state/review.py +18 -14
- package/skills/ai-task/rr_state/start.py +12 -9
- package/skills/ai-task/rr_state/steps.py +16 -7
- package/skills/ai-task/rr_state/verify.py +17 -12
- package/skills/project-update/history/blobs/dac90781793b8a1c8e5f3e0307665b6394d922140c3831c5508d019f79e1ed0a +41 -0
- package/skills/project-update/history/blobs/fa0cb370b024e1a30e1355912032657b08f609e47773a603c193b1e609b0f997 +8 -0
- package/skills/project-update/history/index.json +8 -4
- package/skills/project-update/rr_update/__init__.py +1 -0
- package/skills/project-update/rr_update/apply.py +188 -0
- package/skills/project-update/rr_update/blocks.py +38 -0
- package/skills/project-update/rr_update/build.py +96 -0
- package/skills/project-update/rr_update/cleanup.py +194 -0
- package/skills/project-update/rr_update/cli.py +102 -0
- package/skills/project-update/rr_update/files.py +106 -0
- package/skills/project-update/rr_update/gate.py +60 -0
- package/skills/project-update/rr_update/hints.py +128 -0
- package/skills/project-update/rr_update/history.py +117 -0
- package/skills/project-update/rr_update/kept.py +120 -0
- package/skills/project-update/rr_update/paths.py +108 -0
- package/skills/project-update/rr_update/plan.py +123 -0
- package/skills/project-update/rr_update/report.py +51 -0
- package/skills/project-update/rr_update/rules.py +178 -0
- package/skills/project-update/rr_update/shared.py +74 -0
- package/skills/project-update/rr_update/siblings.py +37 -0
- package/skills/project-update/rr_update/weights.py +171 -0
- package/skills/project-update/update.py +8 -1682
- package/skills/rr-build/SKILL.md +5 -2
- package/skills/rr-setup/SKILL.md +4 -2
- package/skills/rr-setup/setup.py +13 -12
- package/skills/rr-ship/SKILL.md +2 -2
- package/skills/rr-spec/SKILL.md +6 -0
- package/skills/rr-status/SKILL.md +2 -0
- package/skills/rr-status/status.py +5 -3
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""The awk segmenter of the retired shell guard: shell_segments cuts a command into its segments,
|
|
2
|
+
words and redirections, with the keyword, flag and redirection tables it reads.
|
|
3
|
+
|
|
4
|
+
R16: every name here is core (no file, clock or subprocess access).
|
|
5
|
+
Moved from paths.py in WP5 (small-oop-modules), unchanged.
|
|
6
|
+
"""
|
|
7
|
+
import re
|
|
8
|
+
from guard.common import chomp_all
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_KEYWORDS = {"if", "then", "else", "elif", "do", "while", "until", "!", "{", "}", "time", "exec",
|
|
12
|
+
"command", "nohup", "builtin", "env", "sudo"}
|
|
13
|
+
_FLAGGED = {"time", "exec", "command", "env", "sudo"}
|
|
14
|
+
_ASSIGN = re.compile(r"[A-Za-z_][A-Za-z0-9_]*\+?=")
|
|
15
|
+
_SPECIAL_OUT = re.compile(r"""['$"#<(\n`)&;|\\]""")
|
|
16
|
+
_SPECIAL_D = re.compile(r'["$`\n\\]')
|
|
17
|
+
_SPECIAL_A = re.compile(r"['\n\\]")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def shell_segments(cmd):
|
|
21
|
+
"""The awk segmenter of shell_segments, character for character: the
|
|
22
|
+
simple commands of a shell line, verb first, one per output line."""
|
|
23
|
+
src = cmd + "\n"
|
|
24
|
+
n = len(src)
|
|
25
|
+
out = []
|
|
26
|
+
cur = []
|
|
27
|
+
stk, qst = [], []
|
|
28
|
+
q = ""
|
|
29
|
+
nh = 0
|
|
30
|
+
hd, hs = [], []
|
|
31
|
+
i = 0
|
|
32
|
+
|
|
33
|
+
def emit():
|
|
34
|
+
s = "".join(cur)
|
|
35
|
+
cur.clear()
|
|
36
|
+
while True:
|
|
37
|
+
s = s.lstrip(" \t")
|
|
38
|
+
if s == "":
|
|
39
|
+
return
|
|
40
|
+
qq = ""
|
|
41
|
+
k = 0
|
|
42
|
+
ln = len(s)
|
|
43
|
+
while k < ln:
|
|
44
|
+
c = s[k]
|
|
45
|
+
if qq:
|
|
46
|
+
if c == qq:
|
|
47
|
+
qq = ""
|
|
48
|
+
elif c in "\"'":
|
|
49
|
+
qq = c
|
|
50
|
+
elif c in " \t":
|
|
51
|
+
break
|
|
52
|
+
k += 1
|
|
53
|
+
w = s[:k]
|
|
54
|
+
if w in _KEYWORDS:
|
|
55
|
+
s = s[k:]
|
|
56
|
+
if w in _FLAGGED:
|
|
57
|
+
while True:
|
|
58
|
+
s = s.lstrip(" \t")
|
|
59
|
+
if not s.startswith("-"):
|
|
60
|
+
break
|
|
61
|
+
j = 0
|
|
62
|
+
while j < len(s) and s[j] not in " \t":
|
|
63
|
+
j += 1
|
|
64
|
+
s = s[j:]
|
|
65
|
+
continue
|
|
66
|
+
if _ASSIGN.match(w):
|
|
67
|
+
s = s[k:]
|
|
68
|
+
continue
|
|
69
|
+
break
|
|
70
|
+
out.append(s)
|
|
71
|
+
|
|
72
|
+
def cur_ends(chars):
|
|
73
|
+
return bool(cur) and cur[-1][-1:] in chars and cur[-1][-1:] != ""
|
|
74
|
+
|
|
75
|
+
while i < n:
|
|
76
|
+
c = src[i]
|
|
77
|
+
d = src[i + 1] if i + 1 < n else ""
|
|
78
|
+
if q == "s":
|
|
79
|
+
j = src.find("'", i)
|
|
80
|
+
k = src.find("\n", i)
|
|
81
|
+
if j < 0:
|
|
82
|
+
j = n
|
|
83
|
+
if 0 <= k < j:
|
|
84
|
+
cur.append(src[i:k] + " ")
|
|
85
|
+
i = k + 1
|
|
86
|
+
continue
|
|
87
|
+
cur.append(src[i:j + 1])
|
|
88
|
+
i = j + 1
|
|
89
|
+
if j < n:
|
|
90
|
+
q = ""
|
|
91
|
+
continue
|
|
92
|
+
if q == "a":
|
|
93
|
+
m = _SPECIAL_A.search(src, i)
|
|
94
|
+
j = m.start() if m else n
|
|
95
|
+
if j > i:
|
|
96
|
+
cur.append(src[i:j])
|
|
97
|
+
i = j
|
|
98
|
+
continue
|
|
99
|
+
if c == "\\":
|
|
100
|
+
cur.append(c + d)
|
|
101
|
+
i += 2
|
|
102
|
+
continue
|
|
103
|
+
if c == "'":
|
|
104
|
+
q = ""
|
|
105
|
+
cur.append(" " if c == "\n" else c)
|
|
106
|
+
i += 1
|
|
107
|
+
continue
|
|
108
|
+
if c == "\\":
|
|
109
|
+
if d == "\n":
|
|
110
|
+
i += 2
|
|
111
|
+
continue
|
|
112
|
+
cur.append(c + d)
|
|
113
|
+
i += 2
|
|
114
|
+
continue
|
|
115
|
+
if q == "d":
|
|
116
|
+
m = _SPECIAL_D.search(src, i)
|
|
117
|
+
j = m.start() if m else n
|
|
118
|
+
if j > i:
|
|
119
|
+
cur.append(src[i:j])
|
|
120
|
+
i = j
|
|
121
|
+
continue
|
|
122
|
+
if c == '"':
|
|
123
|
+
q = ""
|
|
124
|
+
cur.append(c)
|
|
125
|
+
i += 1
|
|
126
|
+
continue
|
|
127
|
+
if c == "$" and d == "(":
|
|
128
|
+
stk.append("p")
|
|
129
|
+
qst.append(q)
|
|
130
|
+
q = ""
|
|
131
|
+
emit()
|
|
132
|
+
i += 2
|
|
133
|
+
continue
|
|
134
|
+
if c == "`":
|
|
135
|
+
stk.append("b")
|
|
136
|
+
qst.append(q)
|
|
137
|
+
q = ""
|
|
138
|
+
emit()
|
|
139
|
+
i += 1
|
|
140
|
+
continue
|
|
141
|
+
if c == "$":
|
|
142
|
+
cur.append(c)
|
|
143
|
+
i += 1
|
|
144
|
+
continue
|
|
145
|
+
cur.append(" " if c == "\n" else c)
|
|
146
|
+
i += 1
|
|
147
|
+
continue
|
|
148
|
+
# outside quotes
|
|
149
|
+
m = _SPECIAL_OUT.search(src, i)
|
|
150
|
+
j = m.start() if m else n
|
|
151
|
+
if j > i:
|
|
152
|
+
cur.append(src[i:j])
|
|
153
|
+
i = j
|
|
154
|
+
continue
|
|
155
|
+
if c == "'":
|
|
156
|
+
q = "s"
|
|
157
|
+
cur.append(c)
|
|
158
|
+
i += 1
|
|
159
|
+
continue
|
|
160
|
+
if c == "$" and d == "'":
|
|
161
|
+
q = "a"
|
|
162
|
+
cur.append(c + d)
|
|
163
|
+
i += 2
|
|
164
|
+
continue
|
|
165
|
+
if c == '"':
|
|
166
|
+
q = "d"
|
|
167
|
+
cur.append(c)
|
|
168
|
+
i += 1
|
|
169
|
+
continue
|
|
170
|
+
if c == "#" and (not cur or "".join(cur) == "" or cur_ends(" \t")):
|
|
171
|
+
k = src.find("\n", i)
|
|
172
|
+
i = k if k >= 0 else n
|
|
173
|
+
continue
|
|
174
|
+
if c == "<" and d == "<" and src[i + 2:i + 3] == "<":
|
|
175
|
+
cur.append("<<<")
|
|
176
|
+
i += 3
|
|
177
|
+
continue
|
|
178
|
+
if (c == "$" and d == "(" and src[i + 2:i + 3] == "(") or (c == "(" and d == "("):
|
|
179
|
+
j = i + (3 if c == "$" else 2)
|
|
180
|
+
depth = 2
|
|
181
|
+
while j < n and depth > 0:
|
|
182
|
+
e = src[j]
|
|
183
|
+
if e == "(":
|
|
184
|
+
depth += 1
|
|
185
|
+
elif e == ")":
|
|
186
|
+
depth -= 1
|
|
187
|
+
j += 1
|
|
188
|
+
cur.append(src[i:j])
|
|
189
|
+
i = j
|
|
190
|
+
continue
|
|
191
|
+
if c == "<" and d == "<":
|
|
192
|
+
j = i + 2
|
|
193
|
+
strip = 0
|
|
194
|
+
if src[j:j + 1] == "-":
|
|
195
|
+
strip = 1
|
|
196
|
+
j += 1
|
|
197
|
+
while src[j:j + 1] in (" ", "\t") and j < n:
|
|
198
|
+
j += 1
|
|
199
|
+
w = []
|
|
200
|
+
wq = ""
|
|
201
|
+
while j < n:
|
|
202
|
+
e = src[j]
|
|
203
|
+
if wq:
|
|
204
|
+
if e == wq:
|
|
205
|
+
wq = ""
|
|
206
|
+
else:
|
|
207
|
+
w.append(e)
|
|
208
|
+
j += 1
|
|
209
|
+
continue
|
|
210
|
+
if e in "'\"":
|
|
211
|
+
wq = e
|
|
212
|
+
j += 1
|
|
213
|
+
continue
|
|
214
|
+
if e == "\\":
|
|
215
|
+
w.append(src[j + 1:j + 2])
|
|
216
|
+
j += 2
|
|
217
|
+
continue
|
|
218
|
+
if e in " \t\n;&|<>()":
|
|
219
|
+
break
|
|
220
|
+
w.append(e)
|
|
221
|
+
j += 1
|
|
222
|
+
word = "".join(w)
|
|
223
|
+
if word:
|
|
224
|
+
nh += 1
|
|
225
|
+
hd.append(word)
|
|
226
|
+
hs.append(strip)
|
|
227
|
+
i = j
|
|
228
|
+
continue
|
|
229
|
+
if c == "\n":
|
|
230
|
+
emit()
|
|
231
|
+
i += 1
|
|
232
|
+
for h in range(nh):
|
|
233
|
+
while i < n:
|
|
234
|
+
e = src.find("\n", i)
|
|
235
|
+
line = src[i:e] if e >= 0 else src[i:]
|
|
236
|
+
i = e + 1 if e >= 0 else n
|
|
237
|
+
if hs[h]:
|
|
238
|
+
line = line.lstrip("\t")
|
|
239
|
+
if line == hd[h]:
|
|
240
|
+
break
|
|
241
|
+
nh = 0
|
|
242
|
+
hd, hs = [], []
|
|
243
|
+
continue
|
|
244
|
+
if c == "$" and d == "(":
|
|
245
|
+
stk.append("p")
|
|
246
|
+
qst.append(q)
|
|
247
|
+
emit()
|
|
248
|
+
i += 2
|
|
249
|
+
continue
|
|
250
|
+
if c == "$":
|
|
251
|
+
cur.append(c)
|
|
252
|
+
i += 1
|
|
253
|
+
continue
|
|
254
|
+
if c == "`":
|
|
255
|
+
if stk and stk[-1] == "b":
|
|
256
|
+
stk.pop()
|
|
257
|
+
q = qst.pop()
|
|
258
|
+
else:
|
|
259
|
+
stk.append("b")
|
|
260
|
+
qst.append(q)
|
|
261
|
+
emit()
|
|
262
|
+
i += 1
|
|
263
|
+
continue
|
|
264
|
+
if c == "(":
|
|
265
|
+
stk.append("p")
|
|
266
|
+
qst.append(q)
|
|
267
|
+
emit()
|
|
268
|
+
i += 1
|
|
269
|
+
continue
|
|
270
|
+
if c == ")":
|
|
271
|
+
if stk and stk[-1] == "p":
|
|
272
|
+
stk.pop()
|
|
273
|
+
q = qst.pop()
|
|
274
|
+
emit()
|
|
275
|
+
i += 1
|
|
276
|
+
continue
|
|
277
|
+
if c == "&" and (cur_ends("<>") or d == ">"):
|
|
278
|
+
cur.append(c)
|
|
279
|
+
i += 1
|
|
280
|
+
continue
|
|
281
|
+
if c in ";|&":
|
|
282
|
+
emit()
|
|
283
|
+
i += 1
|
|
284
|
+
continue
|
|
285
|
+
cur.append(c)
|
|
286
|
+
i += 1
|
|
287
|
+
emit()
|
|
288
|
+
return chomp_all("".join(s + "\n" for s in out))
|
package/hooks/runtime-gate.py
CHANGED
|
@@ -1160,6 +1160,90 @@ def git_branch(cwd):
|
|
|
1160
1160
|
return ""
|
|
1161
1161
|
|
|
1162
1162
|
|
|
1163
|
+
def read_json_file(path):
|
|
1164
|
+
"""The parsed JSON file, or None when it is missing, unreadable or not JSON."""
|
|
1165
|
+
try:
|
|
1166
|
+
with open(path, encoding="utf-8") as fh:
|
|
1167
|
+
return json.load(fh)
|
|
1168
|
+
except (OSError, ValueError, RecursionError):
|
|
1169
|
+
return None
|
|
1170
|
+
|
|
1171
|
+
|
|
1172
|
+
def still_waiting(decisions, path):
|
|
1173
|
+
"""The decisions guard/dialog.py `pending` keeps, from the consent ledger at path: not
|
|
1174
|
+
settled, and not every command pick of the latest dialog call used. A missing or
|
|
1175
|
+
torn ledger leaves them all."""
|
|
1176
|
+
used, settled, rounds = set(), set(), {}
|
|
1177
|
+
try:
|
|
1178
|
+
with open(path, encoding="utf-8") as fh:
|
|
1179
|
+
for raw in fh:
|
|
1180
|
+
try:
|
|
1181
|
+
entry = json.loads(raw)
|
|
1182
|
+
except (ValueError, RecursionError):
|
|
1183
|
+
continue
|
|
1184
|
+
kind = entry.get("type") if isinstance(entry, dict) and entry.get("v") == 1 else None
|
|
1185
|
+
if kind == "use":
|
|
1186
|
+
used.add(entry.get("id"))
|
|
1187
|
+
elif kind == "settle":
|
|
1188
|
+
settled.add(entry.get("decision"))
|
|
1189
|
+
elif kind == "pick" and isinstance(entry.get("order"), int) and not isinstance(entry["order"], bool):
|
|
1190
|
+
call, chain = rounds.get(entry.get("decision"), (None, []))
|
|
1191
|
+
if chain and call == entry.get("tool_use_id"):
|
|
1192
|
+
chain.append(entry)
|
|
1193
|
+
else:
|
|
1194
|
+
rounds[entry.get("decision")] = (entry.get("tool_use_id"), [entry])
|
|
1195
|
+
except OSError:
|
|
1196
|
+
return decisions
|
|
1197
|
+
out = []
|
|
1198
|
+
for decision in decisions:
|
|
1199
|
+
options = decision.get("options")
|
|
1200
|
+
commands = {o.get("n") for o in options if isinstance(o, dict) and o.get("kind") == "command"} \
|
|
1201
|
+
if isinstance(options, list) else set()
|
|
1202
|
+
picks = [p for p in rounds.get(decision.get("id"), (None, []))[1] if p.get("option") in commands]
|
|
1203
|
+
if decision.get("id") not in settled and not (picks and all(p.get("id") in used for p in picks)):
|
|
1204
|
+
out.append(decision)
|
|
1205
|
+
return out
|
|
1206
|
+
|
|
1207
|
+
|
|
1208
|
+
def waiting_ids(decisions, state):
|
|
1209
|
+
"""The ids that wait on the human, `D3 D4 Q2 G1`: at most three per kind, then `+n`."""
|
|
1210
|
+
state = state or {}
|
|
1211
|
+
asked, approval = state.get("questions"), state.get("human_approval")
|
|
1212
|
+
questions = asked.get("pending") if isinstance(asked, dict) else None
|
|
1213
|
+
approval = approval if isinstance(approval, dict) else {}
|
|
1214
|
+
kinds = ([d.get("id") for d in decisions],
|
|
1215
|
+
[q for q in questions if not str(q).startswith("G")] if isinstance(questions, list) else [],
|
|
1216
|
+
[approval["gate_id"]] if approval.get("requested_at") and approval.get("gate_id") else [])
|
|
1217
|
+
out = []
|
|
1218
|
+
for ids in kinds:
|
|
1219
|
+
ids = [str(i) for i in ids if i]
|
|
1220
|
+
out += ids[:3] + (["+%d" % (len(ids) - 3)] if len(ids) > 3 else [])
|
|
1221
|
+
return " ".join(out)
|
|
1222
|
+
|
|
1223
|
+
|
|
1224
|
+
def task_segment(cwd):
|
|
1225
|
+
"""`<task> · <mode> · <stage> waiting on you: D3 Q2`, or `riskrails · waiting on you: D3`
|
|
1226
|
+
with no task, read from the .ai/state files at or above cwd; "" when nothing is in flight
|
|
1227
|
+
or waits."""
|
|
1228
|
+
here = os.path.abspath(cwd)
|
|
1229
|
+
while not os.path.isdir(os.path.join(here, ".ai", "state")):
|
|
1230
|
+
parent = os.path.dirname(here)
|
|
1231
|
+
if parent == here:
|
|
1232
|
+
return ""
|
|
1233
|
+
here = parent
|
|
1234
|
+
folder = os.path.join(here, ".ai", "state")
|
|
1235
|
+
state = read_json_file(os.path.join(folder, "state.json"))
|
|
1236
|
+
state = state if isinstance(state, dict) and state.get("task_id") else None
|
|
1237
|
+
decisions = read_json_file(os.path.join(folder, "decision.json"))
|
|
1238
|
+
opened = decisions.get("open") if isinstance(decisions, dict) else None
|
|
1239
|
+
opened = [d for d in opened if isinstance(d, dict)] if isinstance(opened, list) else []
|
|
1240
|
+
waiting = waiting_ids(still_waiting(opened, os.path.join(folder, "consent.jsonl")) if opened else [], state)
|
|
1241
|
+
if not state:
|
|
1242
|
+
return "riskrails · waiting on you: " + waiting if waiting else ""
|
|
1243
|
+
return " · ".join(str(state.get(k)) for k in ("task_id", "mode", "stage")) \
|
|
1244
|
+
+ (" waiting on you: " + waiting if waiting else "")
|
|
1245
|
+
|
|
1246
|
+
|
|
1163
1247
|
def second_line(payload):
|
|
1164
1248
|
"""`user@host:cwd | branch | 5h:12% (↻14:00) 7d:41% (↻Fri 03:04) | $1.23`, the
|
|
1165
1249
|
colours of a Debian colour PS1; each part after the prompt is skipped when its
|
|
@@ -1180,6 +1264,9 @@ def second_line(payload):
|
|
|
1180
1264
|
branch = git_branch(cwd) if os.path.isdir(cwd) else ""
|
|
1181
1265
|
if branch:
|
|
1182
1266
|
out += "%s\033[35m%s%s" % (SL_SEP, branch, SL_RESET)
|
|
1267
|
+
segment = task_segment(cwd)
|
|
1268
|
+
if segment:
|
|
1269
|
+
out += "%s\033[36m%s%s" % (SL_SEP, segment, SL_RESET)
|
|
1183
1270
|
limits = payload.get("rate_limits")
|
|
1184
1271
|
limits = limits if isinstance(limits, dict) else {}
|
|
1185
1272
|
parts = []
|
package/hooks/update_notice.py
CHANGED
|
@@ -80,8 +80,9 @@ def spawn_refresh(path):
|
|
|
80
80
|
pass
|
|
81
81
|
|
|
82
82
|
|
|
83
|
-
def notice(runtime, home, plugin_root, now):
|
|
84
|
-
"""(systemMessage, additionalContext) when the cache knows a newer release, else ("", "").
|
|
83
|
+
def notice(runtime, home, plugin_root, now, dialog=False):
|
|
84
|
+
"""(systemMessage, additionalContext) when the cache knows a newer release, else ("", "").
|
|
85
|
+
dialog: the caller's answer to "is the dialog on"; the message then names the dialog first."""
|
|
85
86
|
if disabled():
|
|
86
87
|
return "", ""
|
|
87
88
|
riskrails = os.path.join(home, "riskrails")
|
|
@@ -95,12 +96,26 @@ def notice(runtime, home, plugin_root, now):
|
|
|
95
96
|
have, new = parse(installed), parse(latest)
|
|
96
97
|
if not have or not new or new <= have:
|
|
97
98
|
return "", ""
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
99
|
+
if dialog:
|
|
100
|
+
message = (f'RiskRails {latest} is available (installed {installed}). Say "update RiskRails" and the '
|
|
101
|
+
f"session asks you in the dialog — or run {COMMAND} in a terminal; "
|
|
102
|
+
"then /rr-setup --update in each project.")
|
|
103
|
+
else:
|
|
104
|
+
message = (f"RiskRails {latest} is available (installed {installed}). Update: {COMMAND} "
|
|
105
|
+
f"— inside Claude Code or Codex CLI: ! {COMMAND} — then /rr-setup --update in each project.")
|
|
106
|
+
if dialog:
|
|
107
|
+
context = (f"RiskRails {latest} is available on npm; {installed} is installed. When the user asks to "
|
|
108
|
+
"update, open the install decision — state.py decision open --kind install "
|
|
109
|
+
f'--command "npx riskrails@{latest} --yes" — send the dialog it prints and run the '
|
|
110
|
+
"command the user picks, verbatim, once; never run the update without that pick — "
|
|
111
|
+
"installing is the user's act. A task in flight refuses it (INSTALL_DURING_TASK): close "
|
|
112
|
+
"the task first. Afterwards the user starts a new session and runs /rr-setup --update "
|
|
113
|
+
"in each project. Mention this once to the user if it is relevant.")
|
|
114
|
+
else:
|
|
115
|
+
context = (f"RiskRails {latest} is available on npm; {installed} is installed. The user updates it "
|
|
116
|
+
f"with `{COMMAND}` in a terminal, or `! {COMMAND}` inside Claude Code or Codex CLI, "
|
|
117
|
+
"then `/rr-setup --update` in each project. Mention this once to the user if it is "
|
|
118
|
+
"relevant; never run the update yourself — installing is the user's act.")
|
|
104
119
|
return message, context
|
|
105
120
|
|
|
106
121
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "riskrails",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"description": "RiskRails: a weight-based engineering pipeline for Claude Code and Codex — scoped steps, enforcement hooks, adversarial review, model routing.",
|
|
5
5
|
"keywords": ["claude-code", "codex", "sdlc", "agents", "guardrails", "code-review"],
|
|
6
6
|
"homepage": "https://github.com/vanssata/claude-agentic-sdlc-module#readme",
|
package/skills/AGENTS.md
CHANGED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
<!-- riskrails:rule:skills:start -->
|
|
2
|
-
# Skills rules
|
|
3
|
-
- Weigh a change by what it does to a user project's decisions, never by the skill being a process (constitution C6). The mode is what W gives (`state.py weight`); the rows below set the behaviour axis.
|
|
4
|
-
- behaviour 0: skill or agent text, `usage-report`, no `state.py` refusal moves. behaviour 1: adds a `state.py` check, a sensor or a refusal. behaviour 2: weakens a refusal, changes the approval path (`approve`, `questions --sync`, `AI_UNATTENDED`, the review verdict transitions) or changes install and migration (the `project-update` merge; a migration also raises reversibility through its hint) — declare it, `--declare behaviour=2:"<the effect>"`. Start such a task with `start --mode full --by <name>`: its weight lands in guarded, and these effects keep a STRONG review and the gate.
|
|
5
|
-
- A change to a shipped template or to `ai/` runs `tools/build-template-history.py` in the same task, or `tests/test_project_update.py` (`test_history_covers_every_template`) goes red.
|
|
6
|
-
<!-- riskrails:rule:skills:end -->
|
package/skills/CLAUDE.md
CHANGED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
<!-- riskrails:rule:skills:start -->
|
|
2
|
-
# Skills rules
|
|
3
|
-
- Weigh a change by what it does to a user project's decisions, never by the skill being a process (constitution C6). The mode is what W gives (`state.py weight`); the rows below set the behaviour axis.
|
|
4
|
-
- behaviour 0: skill or agent text, `usage-report`, no `state.py` refusal moves. behaviour 1: adds a `state.py` check, a sensor or a refusal. behaviour 2: weakens a refusal, changes the approval path (`approve`, `questions --sync`, `AI_UNATTENDED`, the review verdict transitions) or changes install and migration (the `project-update` merge; a migration also raises reversibility through its hint) — declare it, `--declare behaviour=2:"<the effect>"`. Start such a task with `start --mode full --by <name>`: its weight lands in guarded, and these effects keep a STRONG review and the gate.
|
|
5
|
-
- A change to a shipped template or to `ai/` runs `tools/build-template-history.py` in the same task, or `tests/test_project_update.py` (`test_history_covers_every_template`) goes red.
|
|
6
|
-
<!-- riskrails:rule:skills:end -->
|
|
@@ -18,6 +18,15 @@
|
|
|
18
18
|
install and reviewable in git.
|
|
19
19
|
- Dead code is deleted only in a task that is about deleting it, with evidence
|
|
20
20
|
that it is dead.
|
|
21
|
+
- A function does one thing and its name says which. When the name needs
|
|
22
|
+
"and", split it.
|
|
23
|
+
- Inputs come in as parameters and the result goes out as the return value;
|
|
24
|
+
files, the network and the clock stay at the edge, in the caller.
|
|
25
|
+
- An error message says what failed, what was expected, what was found, and
|
|
26
|
+
the way out.
|
|
27
|
+
|
|
28
|
+
In existing code the file's conventions win; the last three lines hold for the
|
|
29
|
+
functions you add.
|
|
21
30
|
|
|
22
31
|
## Comments
|
|
23
32
|
|