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,171 @@
|
|
|
1
|
+
"""The weight files: the effective table, weight.md, and the one-time split of the mixed testing.md and weight.json.
|
|
2
|
+
|
|
3
|
+
shell: effective_weight, weight_md, split_weight, unwritten, split_once
|
|
4
|
+
core: GENERATED_HEADER, FIELD_LINE, SPECIFICS_RE, split_testing, leaves, listed
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import tempfile
|
|
11
|
+
from collections import OrderedDict
|
|
12
|
+
|
|
13
|
+
from rr_update import files
|
|
14
|
+
from rr_update import paths
|
|
15
|
+
from rr_update import plan as plan_mod
|
|
16
|
+
from rr_update import siblings
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def effective_weight(plan):
|
|
20
|
+
"""The weight table as this run leaves it: the plugin's with weight.project.json laid over it."""
|
|
21
|
+
own = plan.read(paths.WEIGHT_PROJECT)
|
|
22
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
23
|
+
path = os.path.join(tmp, "weight.project.json")
|
|
24
|
+
if own is not None:
|
|
25
|
+
with open(path, "wb") as fh:
|
|
26
|
+
fh.write(own)
|
|
27
|
+
return siblings.weight_overlay().load(siblings.shipped_weight(), path)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
GENERATED_HEADER = b"generated by `state.py weight --mirror`"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def weight_md(plan, history):
|
|
34
|
+
"""weight.md is always rendered from the shipped table and the overlay (spec :161). A file that is
|
|
35
|
+
a rendering or a shipped version is replaced; one a human wrote is gated."""
|
|
36
|
+
try:
|
|
37
|
+
theirs = siblings.weight_module().mirror(effective_weight(plan)).encode("utf-8")
|
|
38
|
+
except (ValueError, OSError, KeyError, TypeError, AttributeError, RecursionError) as exc:
|
|
39
|
+
plan.hints.append("%s: not rendered (%s: %s); fix %s, then run this again"
|
|
40
|
+
% (paths.WEIGHT_MD, type(exc).__name__, exc, paths.WEIGHT_PROJECT))
|
|
41
|
+
return
|
|
42
|
+
ours = plan.read(paths.WEIGHT_MD)
|
|
43
|
+
if ours is None:
|
|
44
|
+
plan.add("create", paths.WEIGHT_MD, "rendered from the weight table", content=theirs)
|
|
45
|
+
elif ours == theirs:
|
|
46
|
+
return
|
|
47
|
+
elif GENERATED_HEADER in ours[:600] or history.shipped("ai-init/" + paths.WEIGHT_MD, ours):
|
|
48
|
+
plan.add("regenerate", paths.WEIGHT_MD, "rendered from the weight table", content=theirs)
|
|
49
|
+
else:
|
|
50
|
+
plan.gated("regenerate", paths.WEIGHT_MD, plan_mod.edited_note(plan, paths.WEIGHT_MD, ours, theirs), content=theirs)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------- the one-time split
|
|
54
|
+
FIELD_LINE = re.compile(r"(?:%s):" % "|".join(
|
|
55
|
+
("verify_command", "step_test_command", "e2e_command", "lint_command", "typecheck_command",
|
|
56
|
+
"single_test", "doc_tests", "bite_codes", "healthy_output", "runtime")))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
SPECIFICS_RE = re.compile(r"(?m)^## Project specifics[ \t]*$")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def split_testing(testing, template):
|
|
63
|
+
"""R11: verification.md from testing.md. The field lines are copied verbatim with their continuation
|
|
64
|
+
lines (indented, after a field line), then `## Project specifics` to the end of the file."""
|
|
65
|
+
lines = testing.decode("utf-8").splitlines(keepends=True)
|
|
66
|
+
fields, taking = [], False
|
|
67
|
+
for line in lines:
|
|
68
|
+
if FIELD_LINE.match(line):
|
|
69
|
+
fields.append(line)
|
|
70
|
+
taking = True
|
|
71
|
+
elif taking and line[:1] in (" ", "\t") and line.strip():
|
|
72
|
+
fields.append(line)
|
|
73
|
+
else:
|
|
74
|
+
taking = False
|
|
75
|
+
text = template.decode("utf-8")
|
|
76
|
+
if not fields:
|
|
77
|
+
return template, 0
|
|
78
|
+
head = text[:re.search(r"(?m)^verify_command:", text).start()]
|
|
79
|
+
tail = SPECIFICS_RE.search(testing.decode("utf-8"))
|
|
80
|
+
specifics = testing.decode("utf-8")[tail.start():] if tail else text[SPECIFICS_RE.search(text).start():]
|
|
81
|
+
body = "".join(fields)
|
|
82
|
+
return (head + body + ("" if body.endswith("\n") else "\n") + "\n" + specifics).encode("utf-8"), len(fields)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def leaves(value, prefix=()):
|
|
86
|
+
"""{path tuple: leaf} — objects are walked, a list or scalar (or an empty object) is one leaf."""
|
|
87
|
+
if isinstance(value, dict) and value:
|
|
88
|
+
out = {}
|
|
89
|
+
for k, v in value.items():
|
|
90
|
+
out.update(leaves(v, prefix + (k,)))
|
|
91
|
+
return out
|
|
92
|
+
return {prefix: value}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def split_weight(old, history, template):
|
|
96
|
+
"""R11: weight.project.json from the old weight.json: the leaves that differ from that leaf in every
|
|
97
|
+
version the plugin shipped (lists whole). -> (bytes, carried paths, uncarried paths that differ from today's table)."""
|
|
98
|
+
mine = json.loads(old.decode("utf-8"), object_pairs_hook=OrderedDict)
|
|
99
|
+
tables = []
|
|
100
|
+
for blob in history.versions("ai-init/" + paths.WEIGHT_JSON) + [files.read(siblings.shipped_weight())]:
|
|
101
|
+
try:
|
|
102
|
+
tables.append(leaves(json.loads(blob.decode("utf-8"))))
|
|
103
|
+
except (ValueError, RecursionError):
|
|
104
|
+
continue
|
|
105
|
+
today = tables[-1] if tables else {}
|
|
106
|
+
carried, uncarried = OrderedDict(), []
|
|
107
|
+
for path, value in leaves(mine).items():
|
|
108
|
+
if path[:1] in (("version",), ("_comment",)) or not path:
|
|
109
|
+
continue
|
|
110
|
+
if any(path in t and t[path] == value for t in tables):
|
|
111
|
+
if today.get(path, ...) != value:
|
|
112
|
+
uncarried.append(".".join(map(str, path)))
|
|
113
|
+
continue
|
|
114
|
+
here = carried
|
|
115
|
+
for part in path[:-1]:
|
|
116
|
+
here = here.setdefault(part, OrderedDict())
|
|
117
|
+
here[path[-1]] = value
|
|
118
|
+
doc = json.loads(template.decode("utf-8"), object_pairs_hook=OrderedDict)
|
|
119
|
+
doc.update(carried)
|
|
120
|
+
leaf_paths = [".".join(map(str, p)) for p in leaves(carried) if p]
|
|
121
|
+
return files.dump_json(doc), leaf_paths, uncarried
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def listed(names, limit=6):
|
|
125
|
+
shown = ", ".join(names[:limit])
|
|
126
|
+
return shown + (" and %d more" % (len(names) - limit) if len(names) > limit else "")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def unwritten(plan, history, target, template):
|
|
130
|
+
"""True when the new file is absent, or is still a shipped template (today's or any in the history index):
|
|
131
|
+
a scaffold run over an old tree can leave one, and it carries nothing of the project."""
|
|
132
|
+
if not plan.exists(target):
|
|
133
|
+
return True
|
|
134
|
+
data = plan.read(target)
|
|
135
|
+
return data is not None and (data.replace(b"\r\n", b"\n") == template.replace(b"\r\n", b"\n")
|
|
136
|
+
or history.shipped("ai-init/" + target, data))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def split_once(plan, history):
|
|
140
|
+
"""R11: when verification.md / weight.project.json are absent (or only a shipped template), make them from
|
|
141
|
+
testing.md / weight.json (once); with a project's own file present the old file is not read again.
|
|
142
|
+
The old weight.json is then REMOVED, gated."""
|
|
143
|
+
tpl = paths.TEMPLATES["ai-init"]
|
|
144
|
+
template = files.read(os.path.join(tpl, paths.VERIFICATION_MD))
|
|
145
|
+
if unwritten(plan, history, paths.VERIFICATION_MD, template):
|
|
146
|
+
absent = not plan.exists(paths.VERIFICATION_MD)
|
|
147
|
+
testing = plan.read(paths.TESTING_MD)
|
|
148
|
+
content, count = split_testing(testing, template) if testing is not None else (template, 0)
|
|
149
|
+
if count:
|
|
150
|
+
plan.add("split", paths.VERIFICATION_MD, "%d command line(s) of testing.md copied verbatim, with its "
|
|
151
|
+
"Project specifics" % count, content=content)
|
|
152
|
+
elif absent:
|
|
153
|
+
plan.add("create", paths.VERIFICATION_MD, content=content)
|
|
154
|
+
template = files.read(os.path.join(tpl, paths.WEIGHT_PROJECT))
|
|
155
|
+
if unwritten(plan, history, paths.WEIGHT_PROJECT, template):
|
|
156
|
+
absent = not plan.exists(paths.WEIGHT_PROJECT)
|
|
157
|
+
old = plan.read(paths.WEIGHT_JSON)
|
|
158
|
+
if old is None:
|
|
159
|
+
if absent:
|
|
160
|
+
plan.add("create", paths.WEIGHT_PROJECT, content=template)
|
|
161
|
+
else:
|
|
162
|
+
content, carried, uncarried = split_weight(old, history, template)
|
|
163
|
+
if absent or content != plan.read(paths.WEIGHT_PROJECT):
|
|
164
|
+
note = "%d leaf/leaves of weight.json that differ from the plugin's table carried (%s)" % (
|
|
165
|
+
len(carried), listed(carried) or "none")
|
|
166
|
+
if uncarried:
|
|
167
|
+
note += "; %d not carried, equal to an older shipped value: %s" % (len(uncarried), listed(uncarried))
|
|
168
|
+
plan.add("split", paths.WEIGHT_PROJECT, note, content=content)
|
|
169
|
+
if plan.exists(paths.WEIGHT_JSON):
|
|
170
|
+
why = "[split] superseded by %s; the plugin's table is read from the plugin" % paths.WEIGHT_PROJECT
|
|
171
|
+
plan.gated("delete", paths.WEIGHT_JSON, why, reason=why)
|