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.
Files changed (92) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/README.md +25 -15
  5. package/agents/ai-reviewer.md.tmpl +14 -1
  6. package/ai/agents/implementer.md +6 -0
  7. package/ai/agents/reviewer.md +8 -1
  8. package/bin/ansi.js +12 -0
  9. package/bin/menu.js +71 -12
  10. package/bin/riskrails.js +240 -14
  11. package/bin/uninstall.js +115 -0
  12. package/bin/wizard.js +72 -0
  13. package/hooks/AGENTS.md +0 -6
  14. package/hooks/CLAUDE.md +0 -6
  15. package/hooks/context-guard.py +11 -1
  16. package/hooks/dialog-gate.py +3 -2
  17. package/hooks/guard/decision.py +4 -3
  18. package/hooks/guard/dialog.py +12 -2
  19. package/hooks/guard/ere.py +29 -10
  20. package/hooks/guard/git.py +2 -1
  21. package/hooks/guard/paths.py +48 -1406
  22. package/hooks/guard/paths_base.py +66 -0
  23. package/hooks/guard/paths_bash.py +176 -0
  24. package/hooks/guard/paths_classify.py +205 -0
  25. package/hooks/guard/paths_fs.py +137 -0
  26. package/hooks/guard/paths_human.py +105 -0
  27. package/hooks/guard/paths_interp.py +162 -0
  28. package/hooks/guard/paths_movers.py +393 -0
  29. package/hooks/guard/paths_shell.py +288 -0
  30. package/hooks/runtime-gate.py +87 -0
  31. package/hooks/update_notice.py +23 -8
  32. package/package.json +1 -1
  33. package/skills/AGENTS.md +0 -6
  34. package/skills/CLAUDE.md +0 -6
  35. package/skills/ai-init/templates/.ai/policies/coding.md +9 -0
  36. package/skills/ai-init/templates/gitignore.snippet +2 -0
  37. package/skills/ai-task/memory_cleanup.py +213 -0
  38. package/skills/ai-task/memory_compact.py +25 -0
  39. package/skills/ai-task/memory_config.py +120 -0
  40. package/skills/ai-task/memory_export.py +246 -0
  41. package/skills/ai-task/memory_format.py +7 -3
  42. package/skills/ai-task/memory_hook.py +172 -0
  43. package/skills/ai-task/memory_io.py +122 -0
  44. package/skills/ai-task/memory_migrate.py +187 -0
  45. package/skills/ai-task/memory_packages.py +83 -0
  46. package/skills/ai-task/memory_risks.py +84 -0
  47. package/skills/ai-task/memory_status.py +76 -0
  48. package/skills/ai-task/memory_sync.py +20 -1481
  49. package/skills/ai-task/memory_verify.py +316 -0
  50. package/skills/ai-task/rr_state/cli.py +1 -0
  51. package/skills/ai-task/rr_state/close.py +2 -2
  52. package/skills/ai-task/rr_state/dialog.py +32 -7
  53. package/skills/ai-task/rr_state/dialog_cmd.py +38 -5
  54. package/skills/ai-task/rr_state/gate.py +10 -4
  55. package/skills/ai-task/rr_state/labels.py +33 -4
  56. package/skills/ai-task/rr_state/memory_cmd.py +84 -64
  57. package/skills/ai-task/rr_state/questions_cmd.py +5 -4
  58. package/skills/ai-task/rr_state/report.py +73 -10
  59. package/skills/ai-task/rr_state/report_cmd.py +2 -0
  60. package/skills/ai-task/rr_state/review.py +18 -14
  61. package/skills/ai-task/rr_state/start.py +12 -9
  62. package/skills/ai-task/rr_state/steps.py +16 -7
  63. package/skills/ai-task/rr_state/verify.py +17 -12
  64. package/skills/project-update/history/blobs/dac90781793b8a1c8e5f3e0307665b6394d922140c3831c5508d019f79e1ed0a +41 -0
  65. package/skills/project-update/history/blobs/fa0cb370b024e1a30e1355912032657b08f609e47773a603c193b1e609b0f997 +8 -0
  66. package/skills/project-update/history/index.json +8 -4
  67. package/skills/project-update/rr_update/__init__.py +1 -0
  68. package/skills/project-update/rr_update/apply.py +188 -0
  69. package/skills/project-update/rr_update/blocks.py +38 -0
  70. package/skills/project-update/rr_update/build.py +96 -0
  71. package/skills/project-update/rr_update/cleanup.py +194 -0
  72. package/skills/project-update/rr_update/cli.py +102 -0
  73. package/skills/project-update/rr_update/files.py +106 -0
  74. package/skills/project-update/rr_update/gate.py +60 -0
  75. package/skills/project-update/rr_update/hints.py +128 -0
  76. package/skills/project-update/rr_update/history.py +117 -0
  77. package/skills/project-update/rr_update/kept.py +120 -0
  78. package/skills/project-update/rr_update/paths.py +108 -0
  79. package/skills/project-update/rr_update/plan.py +123 -0
  80. package/skills/project-update/rr_update/report.py +51 -0
  81. package/skills/project-update/rr_update/rules.py +178 -0
  82. package/skills/project-update/rr_update/shared.py +74 -0
  83. package/skills/project-update/rr_update/siblings.py +37 -0
  84. package/skills/project-update/rr_update/weights.py +171 -0
  85. package/skills/project-update/update.py +8 -1682
  86. package/skills/rr-build/SKILL.md +5 -2
  87. package/skills/rr-setup/SKILL.md +4 -2
  88. package/skills/rr-setup/setup.py +13 -12
  89. package/skills/rr-ship/SKILL.md +2 -2
  90. package/skills/rr-spec/SKILL.md +6 -0
  91. package/skills/rr-status/SKILL.md +2 -0
  92. 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))
@@ -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 = []
@@ -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
- message = (f"RiskRails {latest} is available (installed {installed}). Update: {COMMAND} "
99
- f"— inside Claude Code or Codex CLI: ! {COMMAND} — then /rr-setup --update in each project.")
100
- context = (f"RiskRails {latest} is available on npm; {installed} is installed. The user updates it "
101
- f"with `{COMMAND}` in a terminal, or `! {COMMAND}` inside Claude Code or Codex CLI, "
102
- "then `/rr-setup --update` in each project. Mention this once to the user if it is "
103
- "relevant; never run the update yourself — installing is the user's act.")
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.1.0",
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
 
@@ -4,3 +4,5 @@
4
4
  .ai/state/*.tmp
5
5
  .ai/state/*.jsonl
6
6
  .ai/local/
7
+ .ai/memory/
8
+ .ai/reports/