riskrails 3.2.2 → 4.0.1
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 +40 -83
- package/agents/ai-reviewer.md.tmpl +1 -1
- package/ai/using-riskrails.md +0 -3
- package/bin/riskrails.js +13 -59
- package/hooks/ai-path-guard-defaults.json +1 -1
- package/hooks/context-guard.py +2 -4
- package/hooks/guard/paths.py +3 -6
- package/hooks/guard/scope.py +1 -3
- package/hooks/runtime-gate.py +18 -58
- package/instructions/routing.md +2 -2
- package/package.json +1 -1
- package/scripts/ai-path +1 -1
- package/scripts/resolve-profile.py +0 -3
- package/skills/ai-audit/templates/ai-sdlc-adoption-plan.md +1 -1
- package/skills/ai-audit/templates/play-scoring-rubric.md +1 -1
- package/skills/ai-init/templates/.ai/AGENTS.md +6 -6
- package/skills/ai-init/templates/.ai/VERSION +1 -1
- package/skills/ai-init/templates/.ai/policies/security.md +1 -2
- package/skills/ai-init/templates/.ai/policies/testing.md +5 -12
- package/skills/ai-init/templates/.ai/policies/verification.md +18 -0
- package/skills/ai-init/templates/.ai/policies/weight.json +0 -1
- package/skills/ai-init/templates/.ai/policies/weight.md +1 -2
- package/skills/ai-init/templates/.ai/policies/weight.project.json +4 -0
- package/skills/ai-init/templates/.ai/project/architecture.md +1 -1
- package/skills/ai-init/templates/.ai/project/initial-assessment.md +2 -2
- package/skills/ai-init/templates/.ai/project/overview.md +1 -1
- package/skills/ai-init/templates/.ai/rules/README.md +2 -2
- package/skills/ai-init/templates/.ai/state/README.md +1 -1
- package/skills/ai-task/memory_store.py +8 -9
- package/skills/ai-task/rr_state/cli.py +1 -1
- package/skills/ai-task/rr_state/dialog.py +2 -4
- package/skills/ai-task/rr_state/fs.py +5 -13
- package/skills/ai-task/rr_state/profile.py +2 -4
- package/skills/ai-task/rr_state/questions_cmd.py +1 -1
- package/skills/ai-task/rr_state/report.py +3 -4
- package/skills/ai-task/rr_state/runtime.py +1 -1
- package/skills/ai-task/rr_state/start.py +7 -7
- package/skills/ai-task/sensors.py +10 -11
- package/skills/ai-task/state.py +4 -24
- package/skills/ai-task/weight.py +17 -17
- package/skills/ai-task/weight_overlay.py +38 -0
- package/skills/project-init/templates/constitution.md +2 -2
- package/skills/project-update/history/blobs/00f5e27ce2d66e3a1d8b222b013ae8fa1fed32612d91a551e0c661cee3ef9c2d +34 -0
- package/skills/project-update/history/blobs/0a6b1b6dd06a35582b2e9989a1f41c40abc29254b8e482714fcc6f0a7b16c406 +32 -0
- package/skills/project-update/history/blobs/0aeb04b04262683cac6513545683486a73dc473b447bba43b5aaab18403957a6 +34 -0
- package/skills/project-update/history/blobs/0ce913a28996f546f72d570b0e5a376362e993ce4d805bc4aae57d38caa44212 +41 -0
- package/skills/project-update/history/blobs/0ea9e36446b624b73b2a821231b2ee66a357b43d584a5b23da48b3c800855e65 +41 -0
- package/skills/project-update/history/blobs/0fb1d68678a1349195b2e79433bb20174d25874e3bfd0b70410bb4e428b4e4b8 +35 -0
- package/skills/project-update/history/blobs/119b2b9d0f9c647a243540724edac6b29afa020c69aa205797bcfa9eb06bc021 +35 -0
- package/skills/project-update/history/blobs/1d0fe3ee5fb8260ba610942d4bbdfff7410fbe80ed826ac07f32985031dc2b3f +34 -0
- package/skills/project-update/history/blobs/1de9a37ed6402893dcab6e61ddbf15ffd35f15a6d84b18e675a0c5bf4e981f85 +42 -0
- package/skills/project-update/history/blobs/1f44e1a236233dcbab58064f39b69f798b35d1d9a29f8c615b5b12854ce633cb +15 -0
- package/skills/project-update/history/blobs/1f824645299579cbdd8141642ddbb4bca9fd5fe9d3112f036d8221f0811e0abf +40 -0
- package/skills/project-update/history/blobs/235fb88393cfe20dd58b35fe9785cc45baf5ffe21a13d926652d76ec68cea8b7 +35 -0
- package/skills/project-update/history/blobs/3918b4ee011d9477b9d2779e2893ab599749c406d4b09439a9748b64fbff58f3 +18 -0
- package/skills/project-update/history/blobs/449273200dfec2a56885c881ff34d40e8ecc00da53c24c2d09b7fbeb1004d255 +40 -0
- package/skills/project-update/history/blobs/4c45aee9b9fc5f6b4e1d9739e434c4094975a5a8dec6b9703fb25f3080680c77 +124 -0
- package/skills/project-update/history/blobs/4dd3fa12bc45acc8c836ab65d790248ca7504a021bca6677499d17bb3d798ed9 +64 -0
- package/skills/project-update/history/blobs/51e3f15095d7123ea8566b512479ee10a6cb2f336b7352a259a083a887bd7116 +33 -0
- package/skills/project-update/history/blobs/5497a204967bea7dab29079a6e1dfa3d8713b2d2cb294c2779378039f03a55fc +36 -0
- package/skills/project-update/history/blobs/60c4d7e7fde7049d92d84295fc13ca9a91eb2de423be08f093b3ee5d5e817ff8 +64 -0
- package/skills/project-update/history/blobs/6221b7c14cc04634e485e562d464c6860d45666a7f63e293c06bb2944ff556cc +27 -0
- package/skills/project-update/history/blobs/637971cfb844ef4285e2886a4b1c834fb2ed5c46e773adb88c2c147fea626ad3 +33 -0
- package/skills/project-update/history/blobs/65af2282eea2a51fcc27a4acf2c26c727b0961c30e1be13176369200c460c219 +34 -0
- package/skills/project-update/history/blobs/66ecd73e303e7bbc1aa8ee72435d0d10812ff7b2eff0adac59bbf0a303e2cc01 +41 -0
- package/skills/project-update/history/blobs/6a43bf32461b22b87fafe2865529dc04fa66cd00182a2a7598aa071667ca255a +35 -0
- package/skills/project-update/history/blobs/6d6dee8634abf9ac057bb206828b83a893a2afcf4f86bbe69e839a3b47d0adde +48 -0
- package/skills/project-update/history/blobs/7224ede803ea81e65a456e21b6028a0e848e8a880456c6503eebe7534ca4f6aa +74 -0
- package/skills/project-update/history/blobs/765a39a202617096c232017822ae738d37e4405430c71700dc7bc14bf1ba794e +33 -0
- package/skills/project-update/history/blobs/77577440a933d1cabd8c6f4a6401cf361a784aa09a25500a6f7b44ead8ab5e55 +41 -0
- package/skills/project-update/history/blobs/7bae90789625d6fb9071ecb412d14a8d1eb0bc5659d8d284c4eaf213e1b5de56 +67 -0
- package/skills/project-update/history/blobs/7bcdf9a0e6b85277b9b60669150224b9414d2e32821ff06f4d148eb1d18785bb +42 -0
- package/skills/project-update/history/blobs/7f834103e9d97aea44e76717bcdcbbe349c560e5b81a42ce85a1cec341041281 +4 -0
- package/skills/project-update/history/blobs/7fb3acb0211818efebc2234863db0b8c846022a7280a6d2d69f267dbf63db48c +34 -0
- package/skills/project-update/history/blobs/87f0855859ff68cc6c988cc0b631ee7f56af7fbba062172bf80e01f4982cc054 +35 -0
- package/skills/project-update/history/blobs/8abab74759766d02b5cb7b0c3e59a3d440b25a454216cd8fea325c23fd822d36 +33 -0
- package/skills/project-update/history/blobs/a1fb50e6c86fae1679ef3351296fd6713411a08cf8dd1790a4fd05fae8688164 +1 -0
- package/skills/project-update/history/blobs/a3df7268feb0780be0d2cf9acced6ed81780a7790f36fe024b6ea0b1bd6eb7a6 +35 -0
- package/skills/project-update/history/blobs/b28721136482dafb9fd2801af340364cb97e9f6eaa987908606c786045b8d334 +28 -0
- package/skills/project-update/history/blobs/b3726422d767f9219fea633bea77f0c99ad9af843c591e6e928e1ccd2a47f374 +27 -0
- package/skills/project-update/history/blobs/b3839425f9d0ab2efa42a6f5e9488ec2d26ca5fb8dcd694b8ab098e10743ca46 +32 -0
- package/skills/project-update/history/blobs/ba14a457e83f9dde65e4190fdfab83b912f7398c1442a2a1cb3d89bfac741cf3 +8 -0
- package/skills/project-update/history/blobs/bceb7189444bb6291b262ca08e34db01010dda2f6e64d696231ec3c343c95c66 +34 -0
- package/skills/project-update/history/blobs/bf82a2bb4fce1bd349ff66118930a01d61a7af8e2bd8d200c721d43afc58c713 +34 -0
- package/skills/project-update/history/blobs/c4dbeb180292a6199ad73a3be861ece07c1f8bf6bf0cc7ca204412bc8e1bb1d8 +31 -0
- package/skills/project-update/history/blobs/c644838866df2001a42501a988dd7b77aacf862bf09d22b465f3cd8a22c6b6eb +33 -0
- package/skills/project-update/history/blobs/c980d74f76a4bdfb38c94b573ac10a54eb50b850b052e15fa6f2442889b36ccb +32 -0
- package/skills/project-update/history/blobs/ceed2f3bd937f862d0e6ca59ce1b1e3603e19a0d7f85d90d6dfeb7bf690153be +29 -0
- package/skills/project-update/history/blobs/d51ea2e893482a20c5882056cf8043327ca1dc1c957927f6746929a47ee8368e +45 -0
- package/skills/project-update/history/blobs/d78f5e0bb1380259fce2cce009785a9a11f6de8b87a160080b9872c059f95060 +34 -0
- package/skills/project-update/history/blobs/d97f46a2cca020406252b1f7baa4184617fa71faca3885e2379c44ae4e1d9327 +67 -0
- package/skills/project-update/history/blobs/da3ecd4c4c414c93d04488685931df39a56bf959bfeb9c45cb1aea2c13bd991b +38 -0
- package/skills/project-update/history/blobs/e129bf71a5a922e24b2c1bc8daa8a5d2b5589b0069770e17ce17e7db5ed4e7a4 +27 -0
- package/skills/project-update/history/blobs/e504529519a371892838725f8b89b5d32e3300e345bcbf2643a3a1a48434b3dd +24 -0
- package/skills/project-update/history/blobs/e9e62d03ca70b3dffccef8dceeb214173352cfef005145e519f0e037993f1c03 +36 -0
- package/skills/project-update/history/blobs/effefbaaf4beb8f1ba55081d6b7e74bf25ebb91045dd10613418f17b95fa7182 +33 -0
- package/skills/project-update/history/blobs/f6af4581bafbf62b257d57e06c3ae2c5998aeca608ee7df037fcc3c3e4b563a9 +34 -0
- package/skills/project-update/history/blobs/fc35045777d29630303430752a651b3e553580a581036a02cc103eb96a46f4b0 +34 -0
- package/skills/project-update/history/blobs/fdc21345c77f92a42d244aa104f393f439a0ff9981a9fd462bd837e8a47931bb +48 -0
- package/skills/project-update/history/index.json +32 -13
- package/skills/project-update/render_instructions.py +12 -45
- package/skills/project-update/update.py +757 -1172
- package/skills/rr-setup/SKILL.md +42 -81
- package/skills/rr-setup/scaffold_ai.py +7 -7
- package/skills/rr-setup/setup.py +96 -164
- package/skills/rr-status/status.py +7 -10
- package/skills/usage-report/usage-report.py +4 -5
- package/skills/ai-audit/SKILL.md +0 -9
- package/skills/ai-init/SKILL.md +0 -9
- package/skills/ai-status/SKILL.md +0 -9
- package/skills/ai-task/SKILL.md +0 -9
- package/skills/project-init/SKILL.md +0 -9
- package/skills/project-update/SKILL.md +0 -9
- package/skills/project-update/adopt-map.json +0 -91
- package/skills/project-update/adopt.py +0 -2225
- package/skills/project-update/migrations/0001_schema_version.py +0 -13
- package/skills/project-update/migrations/0002_task_journal.py +0 -123
- package/skills/project-update/migrations/0003_context_diet.py +0 -65
- package/skills/project-update/migrations/0004_deterministic_gates.py +0 -57
- package/skills/project-update/migrations/0005_runtimes_and_plans.py +0 -35
- package/skills/project-update/migrations/0006_shared_files.py +0 -154
- package/skills/project-update/migrations/0007_weight_model.py +0 -295
- package/skills/project-update/migrations/0008_renames.py +0 -43
- package/skills/project-update/migrations/0009_state_file.py +0 -25
- package/skills/project-update/migrations/0010_dialog_switch.py +0 -30
- package/skills/project-update/migrations/0011_retire_risk_tiers.py +0 -37
- package/skills/project-update/migrations/README.md +0 -76
- package/skills/project-update/migrations/__init__.py +0 -186
- package/skills/sdlc-intent/SKILL.md +0 -9
- package/skills/sdlc-plan/SKILL.md +0 -9
- package/skills/sdlc-spec/SKILL.md +0 -9
- package/skills/usage-report/SKILL.md +0 -10
- /package/{ai/workflows/refactoring.md → skills/project-update/history/blobs/36cb593293496fbcf38df828a9b9b2bc0d5a53333f584a5319ebc3d799016e24} +0 -0
- /package/{ai/workflows/feature.md → skills/project-update/history/blobs/56e16072c2f3edc6c8897c690d09a11d12fc414c3e3f361f359f7dc609a21282} +0 -0
- /package/{ai/workflows/hotfix.md → skills/project-update/history/blobs/8cbefbf1aa412bae3e5019acd40b8fe0598fceba6c781906b3bc6dbd014f6b23} +0 -0
- /package/{ai/workflows/bugfix.md → skills/project-update/history/blobs/af56dc16e44ae1d8c3dac4c3ca82e2163b719553c02ea455e77430bab9a14089} +0 -0
- /package/{ai/workflows/investigation.md → skills/project-update/history/blobs/b9e09cb24958cd6831589997558974bd6356305306e8f9685e226bcdc30d60ac} +0 -0
- /package/{ai/agents/manager.md → skills/project-update/history/blobs/d0cf6388f35a92a0cf607d33909278382f6d7b6a4c32e281b9f9e0ea652c7b91} +0 -0
|
@@ -1,295 +0,0 @@
|
|
|
1
|
-
"""0007: the weight table carries the risk-tier table's edits; a task in flight gets a mode.
|
|
2
|
-
|
|
3
|
-
The six-axis weight model in .ai/policies/weight.json is what the schema-7
|
|
4
|
-
surface of state.py reads. This migration gives every project one — the shipped
|
|
5
|
-
table, created when the project has none — and converts into it every value the
|
|
6
|
-
project added on top of the risk-tier tables it was installed with:
|
|
7
|
-
|
|
8
|
-
tiers.T0-T2.examples -> examples.light, T3-T4 -> guarded, T5-T6 -> full
|
|
9
|
-
path_scopes money/identity/customer data -> axes.domain.modules
|
|
10
|
-
path_scopes auth/security -> the domain hint; pipeline -> the behaviour hint;
|
|
11
|
-
dependencies -> the novelty hint
|
|
12
|
-
path_scopes infra/k8s/deploy/production -> reversibility.irreversible when the
|
|
13
|
-
glob names prod, else reversibility.needs_step
|
|
14
|
-
path_scopes migrations/schema/config -> reversibility.needs_step
|
|
15
|
-
a scope this table does not know, with min_tier T5 or T6 -> axes.domain.modules
|
|
16
|
-
sensors.tests.* -> tests.* with max_suite_runs = min(old, 2), and
|
|
17
|
-
review_rounds_max, remediation_rounds -> carried — each only when the project
|
|
18
|
-
changed it and weight.json still holds the shipped value
|
|
19
|
-
|
|
20
|
-
Everything else (diff_budget, sensors, pipeline profiles, model tiers, the
|
|
21
|
-
escalation and downgrade rules) has no place in the weight table; its prose lives
|
|
22
|
-
in docs/weight.md.
|
|
23
|
-
|
|
24
|
-
risk-tiers.json and its mirror stay where they are, untouched: the old surface
|
|
25
|
-
reads them until it is removed, and the plugin still ships them, so moving or
|
|
26
|
-
deleting them here would only have the next run put them back (a template is
|
|
27
|
-
never shipped at a path a migration moved away). Their retirement belongs with
|
|
28
|
-
the template's.
|
|
29
|
-
|
|
30
|
-
A task in flight keeps running on the old surface: it gets a `mode` from its
|
|
31
|
-
tier and a `weight` that says it was migrated, never a schema-7 marker —
|
|
32
|
-
state.py decides by that marker.
|
|
33
|
-
"""
|
|
34
|
-
import json
|
|
35
|
-
from collections import OrderedDict
|
|
36
|
-
|
|
37
|
-
VERSION = 7
|
|
38
|
-
TITLE = "carry the risk-tier table's edits into weight.json, and give a task in flight a mode"
|
|
39
|
-
OLD = ".ai/policies/risk-tiers.json"
|
|
40
|
-
NEW = ".ai/policies/weight.json"
|
|
41
|
-
MOVES = []
|
|
42
|
-
|
|
43
|
-
TIER_MODE = {"T0": "light", "T1": "light", "T2": "light", "T3": "guarded", "T4": "guarded",
|
|
44
|
-
"T5": "full", "T6": "full_irreversible"}
|
|
45
|
-
EXAMPLE_MODE = dict(TIER_MODE, T6="full")
|
|
46
|
-
DOMAIN = {"payments", "money", "tax", "invoice", "identity", "customer_data", "personal_data"}
|
|
47
|
-
INFRA = {"infra", "k8s", "deploy", "production"}
|
|
48
|
-
NEEDS_STEP = {"migrations", "schema", "config"}
|
|
49
|
-
HINTS = {"auth": "domain", "security": "domain", "pipeline": "behaviour", "dependencies": "novelty"}
|
|
50
|
-
DROPPED = {"tests", "docs", "source"}
|
|
51
|
-
TEST_KEYS = ("timeout_seconds", "env_retries", "log_max_bytes")
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def _loads(raw):
|
|
55
|
-
try:
|
|
56
|
-
doc = json.loads(raw, object_pairs_hook=OrderedDict)
|
|
57
|
-
except (TypeError, ValueError, RecursionError): # nested past the parser's limit: edit_json holds the schema
|
|
58
|
-
return None
|
|
59
|
-
return doc if isinstance(doc, dict) else None
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def _shipped(ctx):
|
|
63
|
-
"""What every shipped risk-tiers.json said — per tier, per scope, and each
|
|
64
|
-
scalar a run carries — and the shipped weight table: only a value the
|
|
65
|
-
project changed on top of these is carried."""
|
|
66
|
-
examples, scopes, scalars = {}, {}, {}
|
|
67
|
-
for raw in ctx.shipped(OLD):
|
|
68
|
-
doc = _loads(raw) or {}
|
|
69
|
-
for key, value in _scalars(doc).items():
|
|
70
|
-
scalars.setdefault(key, []).append(value)
|
|
71
|
-
for tier, entry in (doc.get("tiers") or {}).items():
|
|
72
|
-
if isinstance(entry, dict):
|
|
73
|
-
examples.setdefault(tier, set()).update(e for e in entry.get("examples") or [] if isinstance(e, str))
|
|
74
|
-
for scope in doc.get("path_scopes") or []:
|
|
75
|
-
if isinstance(scope, dict):
|
|
76
|
-
scopes.setdefault(scope.get("scope"), set()).update(
|
|
77
|
-
p for p in scope.get("paths") or [] if isinstance(p, str))
|
|
78
|
-
return examples, scopes, scalars, _loads(ctx.template(NEW)) or {}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def _scalars(old):
|
|
82
|
-
"""{"tests.<key>" | "review_rounds_max" | "remediation_rounds": value} of an old table."""
|
|
83
|
-
out = {}
|
|
84
|
-
tests = (old.get("sensors") or {}).get("tests") if isinstance(old.get("sensors"), dict) else None
|
|
85
|
-
for key in TEST_KEYS + ("max_suite_runs",):
|
|
86
|
-
if isinstance(tests, dict) and key in tests:
|
|
87
|
-
out["tests." + key] = tests[key]
|
|
88
|
-
for key in ("review_rounds_max", "remediation_rounds"):
|
|
89
|
-
if isinstance(old.get(key), int):
|
|
90
|
-
out[key] = old[key]
|
|
91
|
-
return out
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def _get(doc, dotted):
|
|
95
|
-
for part in dotted.split("."):
|
|
96
|
-
doc = doc.get(part) if isinstance(doc, dict) else None
|
|
97
|
-
return doc
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def _add(items, values):
|
|
101
|
-
for value in values:
|
|
102
|
-
if value not in items:
|
|
103
|
-
items.append(value)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def _hint(doc, axis, paths, note):
|
|
107
|
-
hint = next((h for h in doc.setdefault("hints", []) if isinstance(h, dict) and h.get("axis") == axis), None)
|
|
108
|
-
if hint is None:
|
|
109
|
-
doc["hints"].append(OrderedDict([("paths", list(paths)), ("axis", axis), ("note", note)]))
|
|
110
|
-
else:
|
|
111
|
-
_add(hint.setdefault("paths", []), paths)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def _writes(old, base, shipped):
|
|
115
|
-
"""What converting `old` into `base` writes, in order: ("list", path, values) appends to a list,
|
|
116
|
-
("set", path, value) sets a scalar, ("hint", axis, (paths, note)) adds to a hint."""
|
|
117
|
-
shipped_examples, shipped_scopes, shipped_scalars, template = shipped
|
|
118
|
-
out = []
|
|
119
|
-
for tier, entry in (old.get("tiers") or {}).items():
|
|
120
|
-
if tier in EXAMPLE_MODE and isinstance(entry, dict):
|
|
121
|
-
extra = [e for e in entry.get("examples") or []
|
|
122
|
-
if isinstance(e, str) and e not in shipped_examples.get(tier, set())]
|
|
123
|
-
if extra:
|
|
124
|
-
out.append(("list", ("examples", EXAMPLE_MODE[tier]), extra))
|
|
125
|
-
for scope in old.get("path_scopes") or []:
|
|
126
|
-
if not isinstance(scope, dict):
|
|
127
|
-
continue
|
|
128
|
-
name = scope.get("scope")
|
|
129
|
-
paths = [p for p in scope.get("paths") or []
|
|
130
|
-
if isinstance(p, str) and p not in shipped_scopes.get(name, set())]
|
|
131
|
-
if not paths or name in DROPPED:
|
|
132
|
-
continue
|
|
133
|
-
if name in DOMAIN or (name not in HINTS and name not in INFRA | NEEDS_STEP
|
|
134
|
-
and scope.get("min_tier") in ("T5", "T6")):
|
|
135
|
-
out.append(("list", ("axes", "domain", "modules"), paths))
|
|
136
|
-
elif name in HINTS:
|
|
137
|
-
out.append(("hint", HINTS[name], (paths, "%s (from risk-tiers.json)" % name)))
|
|
138
|
-
elif name in INFRA | NEEDS_STEP:
|
|
139
|
-
two = [p for p in paths if name in INFRA and "prod" in p]
|
|
140
|
-
out.append(("list", ("axes", "reversibility", "irreversible"), two))
|
|
141
|
-
out.append(("list", ("axes", "reversibility", "needs_step"), [p for p in paths if p not in two]))
|
|
142
|
-
for key, value in _scalars(old).items():
|
|
143
|
-
if value in shipped_scalars.get(key, []) or _get(base, key) != _get(template, key):
|
|
144
|
-
continue
|
|
145
|
-
if key == "tests.max_suite_runs" and isinstance(value, int):
|
|
146
|
-
value = min(value, 2)
|
|
147
|
-
out.append(("set", tuple(key.split(".")), value))
|
|
148
|
-
return out
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def convert(old, base, shipped):
|
|
152
|
-
"""The weight table `base` plus what the old table `old` added on top of the
|
|
153
|
-
`shipped` (examples, scopes, scalars, weight template) it was installed
|
|
154
|
-
with. A scalar is carried only when the old table changed it and `base`
|
|
155
|
-
still holds the shipped weight value — a project's own weight.json edit
|
|
156
|
-
wins. `base` is not changed, and converting twice adds nothing new."""
|
|
157
|
-
new = json.loads(json.dumps(base), object_pairs_hook=OrderedDict)
|
|
158
|
-
new.setdefault("axes", OrderedDict())
|
|
159
|
-
for kind, path, value in _writes(old, base, shipped):
|
|
160
|
-
if kind == "hint":
|
|
161
|
-
_hint(new, path, *value)
|
|
162
|
-
continue
|
|
163
|
-
node = new
|
|
164
|
-
for part in path[:-1]:
|
|
165
|
-
node = node.setdefault(part, OrderedDict())
|
|
166
|
-
if kind == "list":
|
|
167
|
-
_add(node.setdefault(path[-1], []), value)
|
|
168
|
-
else:
|
|
169
|
-
node[path[-1]] = value
|
|
170
|
-
return new
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
def _mode(state):
|
|
174
|
-
"""The task in flight: a mode from its tier, a weight nobody measured. The
|
|
175
|
-
schema-7 marker is never added: the task finishes on the surface it began on."""
|
|
176
|
-
if state.get("schema") == VERSION:
|
|
177
|
-
return state
|
|
178
|
-
tier = state.get("risk_tier")
|
|
179
|
-
if tier in TIER_MODE:
|
|
180
|
-
state.setdefault("mode", TIER_MODE[tier])
|
|
181
|
-
state.setdefault("weight", {"w": None, "axes": {}, "unknown": ["migrated"]})
|
|
182
|
-
return state
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
def _kind(value):
|
|
186
|
-
"""the JSON name of a value's type"""
|
|
187
|
-
if value is None:
|
|
188
|
-
return "null"
|
|
189
|
-
if isinstance(value, bool): # before int: True is an int
|
|
190
|
-
return "boolean"
|
|
191
|
-
for types, name in (((int, float), "number"), (str, "string"), (list, "list"), (dict, "object")):
|
|
192
|
-
if isinstance(value, types):
|
|
193
|
-
return name
|
|
194
|
-
return type(value).__name__
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
def _an(word):
|
|
198
|
-
return word if word == "null" else ("an " if word[0] in "aeiou" else "a ") + word
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
def _bad(doc, key, want, what):
|
|
202
|
-
"""A reason when `doc[key]`, present and not null, is not a `want`; else None."""
|
|
203
|
-
value = doc.get(key)
|
|
204
|
-
if value is not None and not isinstance(value, want):
|
|
205
|
-
return "%s is %s, not %s" % (what, _an(_kind(value)), _an("object" if want is dict else "list"))
|
|
206
|
-
return None
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
def _old_problem(old):
|
|
210
|
-
"""Why the old table cannot be converted (a wrong type where convert reads), or None."""
|
|
211
|
-
if not isinstance(old, dict):
|
|
212
|
-
return "risk-tiers.json is %s, not an object" % _an(_kind(old))
|
|
213
|
-
why = _bad(old, "tiers", dict, "tiers") if old.get("tiers") else None # falsy: convert reads no tiers
|
|
214
|
-
for tier, entry in (old.get("tiers") or {}).items() if why is None else ():
|
|
215
|
-
if tier in EXAMPLE_MODE and isinstance(entry, dict) and entry.get("examples"): # convert reads only these
|
|
216
|
-
why = why or _bad(entry, "examples", (list, dict), "tiers.%s.examples" % tier) # an object iterates by key
|
|
217
|
-
scopes = old.get("path_scopes")
|
|
218
|
-
if scopes and not isinstance(scopes, (list, dict, str)): # an object or a string iterates, its members skipped
|
|
219
|
-
why = why or "path_scopes is %s, not a list" % _an(_kind(scopes))
|
|
220
|
-
for scope in scopes if isinstance(scopes, list) and not why else ():
|
|
221
|
-
if isinstance(scope, dict):
|
|
222
|
-
name = scope.get("scope")
|
|
223
|
-
if scope.get("paths"):
|
|
224
|
-
why = _bad(scope, "paths", (list, dict), "a path scope's paths") # an object iterates by key
|
|
225
|
-
# the name is hashed only against a str path
|
|
226
|
-
if not why and isinstance(name, (list, dict)) and any(isinstance(p, str) for p in scope.get("paths") or []):
|
|
227
|
-
why = "a path scope's name is %s, not a string" % _an(_kind(name))
|
|
228
|
-
if why:
|
|
229
|
-
break
|
|
230
|
-
return why
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
def _new_problem(doc, writes):
|
|
234
|
-
"""Why the weight table `doc` cannot take the `writes` of convert (a wrong type where convert writes), or None."""
|
|
235
|
-
if not isinstance(doc, dict):
|
|
236
|
-
return "weight.json is %s, not an object" % _an(_kind(doc))
|
|
237
|
-
for kind, path, value in writes:
|
|
238
|
-
if kind == "hint":
|
|
239
|
-
if "hints" not in doc:
|
|
240
|
-
continue
|
|
241
|
-
if not isinstance(doc["hints"], list):
|
|
242
|
-
return "hints is %s, not a list" % _an(_kind(doc["hints"]))
|
|
243
|
-
for i, hint in enumerate(doc["hints"]):
|
|
244
|
-
if isinstance(hint, dict) and hint.get("axis") == path:
|
|
245
|
-
if "paths" in hint and not isinstance(hint["paths"], list):
|
|
246
|
-
return "hints[%d].paths is %s, not a list" % (i, _an(_kind(hint["paths"])))
|
|
247
|
-
break
|
|
248
|
-
continue
|
|
249
|
-
node = doc
|
|
250
|
-
for i, part in enumerate(path):
|
|
251
|
-
if part not in node:
|
|
252
|
-
break
|
|
253
|
-
node = node[part]
|
|
254
|
-
what = ".".join(path[:i + 1])
|
|
255
|
-
if i < len(path) - 1:
|
|
256
|
-
if not isinstance(node, dict):
|
|
257
|
-
return "%s is %s, not an object" % (what, _an(_kind(node)))
|
|
258
|
-
elif kind == "list" and value and not isinstance(node, list):
|
|
259
|
-
return "%s is %s, not a list" % (what, _an(_kind(node)))
|
|
260
|
-
return None
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
def plan(ctx):
|
|
264
|
-
old = ctx.read_json(OLD)
|
|
265
|
-
why = _old_problem(old) if old is not None else None
|
|
266
|
-
if why:
|
|
267
|
-
ctx.conflict(OLD, "%s, so this migration could not run" % why)
|
|
268
|
-
old = None
|
|
269
|
-
shipped = _shipped(ctx)
|
|
270
|
-
before = ctx.read(NEW)
|
|
271
|
-
if before is None:
|
|
272
|
-
template = ctx.template(NEW)
|
|
273
|
-
doc = convert(old, _loads(template), shipped) if old is not None else None
|
|
274
|
-
try:
|
|
275
|
-
body = None if doc is None or doc == _loads(template) else \
|
|
276
|
-
(json.dumps(doc, indent=2, ensure_ascii=False) + "\n").encode()
|
|
277
|
-
except RecursionError: # a deep value carried from the old table: the default beside it holds the schema
|
|
278
|
-
ctx.conflict(OLD, "nested too deeply, so this migration could not run")
|
|
279
|
-
body = None
|
|
280
|
-
ctx.create(NEW, template if body is None else body)
|
|
281
|
-
elif old is not None:
|
|
282
|
-
try:
|
|
283
|
-
current = json.loads(before)
|
|
284
|
-
except (ValueError, RecursionError): # invalid or too deep: edit_json holds the schema
|
|
285
|
-
current = {}
|
|
286
|
-
why = _new_problem(current, _writes(old, current, shipped))
|
|
287
|
-
if why:
|
|
288
|
-
ctx.conflict(NEW, "%s, so this migration could not run" % why)
|
|
289
|
-
else:
|
|
290
|
-
ctx.edit_json(NEW, lambda new: convert(old, new, shipped))
|
|
291
|
-
after = ctx.read(NEW)
|
|
292
|
-
if old is not None and _loads(after) != _loads(before or ctx.template(NEW)):
|
|
293
|
-
ctx.hint("%s now carries this project's risk-tier edits: run `state.py weight --mirror` to "
|
|
294
|
-
"regenerate weight.md beside it" % NEW)
|
|
295
|
-
ctx.patch_state(_mode)
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
"""0008: the plugin is RiskRails.
|
|
2
|
-
|
|
3
|
-
A refactoring (R35); every decision the guards take stays the same.
|
|
4
|
-
|
|
5
|
-
* The state file does not move here. R34 is expand/contract: this version's
|
|
6
|
-
guards and state.py read `.ai/state/state.json` first and `current.json`
|
|
7
|
-
otherwise, and write the file where it is; a later migration moves it, once
|
|
8
|
-
a version that reads both names has shipped, so reverting the move still
|
|
9
|
-
leaves every task guarded.
|
|
10
|
-
* The markers of the managed block (`<!-- claude-agentic:start -->` / `:end`)
|
|
11
|
-
and of each rule block (`claude-agentic:rule:<slug>:start` / `:end`) in the
|
|
12
|
-
project's root instruction files take the name `riskrails`. Only the word
|
|
13
|
-
inside a marker changes: the text around the block, and the spacing inside
|
|
14
|
-
the marker, are left byte for byte as they are. Rule blocks in other
|
|
15
|
-
directories are renamed by the renderer the next time it splices them.
|
|
16
|
-
* `.ai/AGENTS.md` routes to `<home>/riskrails/ai` and `<home>/riskrails/ai-path`
|
|
17
|
-
instead of `<home>/claude-agentic/...`; the installer leaves a symlink at the
|
|
18
|
-
old home for one version.
|
|
19
|
-
"""
|
|
20
|
-
import re
|
|
21
|
-
|
|
22
|
-
VERSION = 8
|
|
23
|
-
TITLE = "rename the plugin's markers and routes to RiskRails"
|
|
24
|
-
MOVES = []
|
|
25
|
-
|
|
26
|
-
ROUTER = ".ai/AGENTS.md"
|
|
27
|
-
MARKER = re.compile(
|
|
28
|
-
rb"(<!--\s*)claude-agentic(:(?:start|end|rule:[a-z0-9][a-z0-9-]*:(?:start|end))\s*-->)")
|
|
29
|
-
ROUTE = re.compile(rb"(?<=/)claude-agentic(?=/ai(?:-path)?\b)")
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def rename_markers(data):
|
|
33
|
-
return MARKER.sub(rb"\1riskrails\2", data)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def rename_routes(data):
|
|
37
|
-
return ROUTE.sub(b"riskrails", data)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def plan(ctx):
|
|
41
|
-
for path in ctx.instruction_files():
|
|
42
|
-
ctx.edit_text(path, rename_markers, "claude-agentic markers renamed to riskrails")
|
|
43
|
-
ctx.edit_text(ROUTER, rename_routes, "routes to the riskrails home")
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
"""0009: the task's state file is `.ai/state/state.json`.
|
|
2
|
-
|
|
3
|
-
R34's contract half. 0008 shipped the expand half: the guards and state.py of
|
|
4
|
-
3.4b read `.ai/state/state.json` first and `current.json` otherwise, and write
|
|
5
|
-
the file where it is. This migration, run once such a version is installed,
|
|
6
|
-
moves a task in flight from `current.json` to `state.json`, and `state.py start`
|
|
7
|
-
opens every new task in `state.json`.
|
|
8
|
-
|
|
9
|
-
* The move goes through MOVES like any other: no task in flight means no
|
|
10
|
-
file, and nothing happens; a project that already holds `state.json` keeps
|
|
11
|
-
it (a `current.json` beside it is a conflict for a human, not overwritten).
|
|
12
|
-
* The file's bytes are not touched: every field of the task survives.
|
|
13
|
-
* Reverting to 3.4b after the move still guards the task, because that
|
|
14
|
-
version reads `state.json`.
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
VERSION = 9
|
|
18
|
-
TITLE = "the task's state file is state.json"
|
|
19
|
-
STATE_LEGACY = ".ai/state/current.json"
|
|
20
|
-
STATE_NEW = ".ai/state/state.json"
|
|
21
|
-
MOVES = [(STATE_LEGACY, STATE_NEW)]
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def plan(ctx):
|
|
25
|
-
ctx.move(STATE_LEGACY, STATE_NEW)
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
"""0010: the command-choice dialog is on (R36.1).
|
|
2
|
-
|
|
3
|
-
`.ai/policies/dialog.json` is the project's switch for the dialog: the human
|
|
4
|
-
picks a commit, push or approval command in the runtime's dialog and the agent
|
|
5
|
-
runs that command, verbatim and once. This migration writes the switch, on, as
|
|
6
|
-
the scaffold does for a new project.
|
|
7
|
-
|
|
8
|
-
* A project that already has the file keeps it, whatever it says: `create`
|
|
9
|
-
does not overwrite, and a human's `"enabled": false` stays off.
|
|
10
|
-
* A second run finds the file and does nothing, and prints nothing.
|
|
11
|
-
* The task's state is not touched: a task in flight survives.
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
VERSION = 10
|
|
15
|
-
TITLE = "the command-choice dialog switch"
|
|
16
|
-
SWITCH = ".ai/policies/dialog.json"
|
|
17
|
-
MOVES = []
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def plan(ctx):
|
|
21
|
-
if ctx.exists(SWITCH):
|
|
22
|
-
return
|
|
23
|
-
ctx.create(SWITCH, ctx.template(SWITCH))
|
|
24
|
-
ctx.hint("%s is new and the command-choice dialog is on; set \"enabled\": false in it to turn the dialog off"
|
|
25
|
-
% SWITCH)
|
|
26
|
-
ctx.hint("docs/sdlc/constitution.md C5 still says no agent commits, merges or deploys: if you keep the dialog "
|
|
27
|
-
"on, amend C5 to the new sentence (skills/project-init/templates/constitution.md, C5)")
|
|
28
|
-
if ctx.state is not None:
|
|
29
|
-
ctx.hint("a task is in flight: the dialog switch takes effect for it too; set \"enabled\": false in %s "
|
|
30
|
-
"to turn it off" % SWITCH)
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
"""0011: the old tier policy is retired (register #20, #50, #58, #62).
|
|
2
|
-
|
|
3
|
-
`.ai/policies/risk-tiers.json` and its generated mirror `risk-tiers.md` were
|
|
4
|
-
replaced by `.ai/policies/weight.json` (migration 0007 converted the project's
|
|
5
|
-
edits) and nothing in the plugin reads them any more. The plugin stops shipping
|
|
6
|
-
both, and this migration proposes deleting the project's copies.
|
|
7
|
-
|
|
8
|
-
* Each file the project has is proposed for deletion: a `delete?` item that
|
|
9
|
-
waits for --apply --confirm-delete NAME, with the original kept under
|
|
10
|
-
`.ai/reports/project-update-<date>/original/`. Git is the rollback.
|
|
11
|
-
* A copy the project edited is proposed too, and the reason says it was
|
|
12
|
-
edited: the human decides, and may keep the file by never confirming (the
|
|
13
|
-
schema then stays at 10) or by copying what they need into weight.json first.
|
|
14
|
-
* A project without the files gets nothing, and so does a second run.
|
|
15
|
-
* The templates are retired in `migrations.RETIRED`, so the template walk never
|
|
16
|
-
re-creates a deleted file.
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
VERSION = 11
|
|
20
|
-
TITLE = "retire risk-tiers.json and risk-tiers.md (weight.json replaced them)"
|
|
21
|
-
MOVES = []
|
|
22
|
-
|
|
23
|
-
PATHS = (".ai/policies/risk-tiers.json", ".ai/policies/risk-tiers.md")
|
|
24
|
-
REASON = "replaced by .ai/policies/weight.json; nothing reads it any more"
|
|
25
|
-
EDITED = REASON + " — your project edited this copy, check weight.json holds what you need"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def plan(ctx):
|
|
29
|
-
for path in PATHS:
|
|
30
|
-
if not ctx.exists(path):
|
|
31
|
-
continue
|
|
32
|
-
why = ctx.unsafe_target(path)
|
|
33
|
-
if why:
|
|
34
|
-
ctx.hint("%s: %s, so this update never deletes it — remove it by hand once weight.json holds "
|
|
35
|
-
"what you need" % (path, why))
|
|
36
|
-
continue
|
|
37
|
-
ctx.delete(path, REASON if ctx.is_shipped(path, ctx.read(path)) else EDITED)
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
# Writing a migration
|
|
2
|
-
|
|
3
|
-
For plugin developers; `/rr-setup --update` runs them. One module per version in
|
|
4
|
-
this directory, named `NNNN_slug.py`, numbered contiguously from `0001`, with
|
|
5
|
-
`VERSION` equal to the prefix, a one-line `TITLE`, a static `MOVES` list and
|
|
6
|
-
`plan(ctx)`.
|
|
7
|
-
|
|
8
|
-
- Every rename a migration performs belongs in `MOVES`, so the template history
|
|
9
|
-
follows the file and the project's edits merge at the new path. `ctx.move`
|
|
10
|
-
refuses a pair that is not there.
|
|
11
|
-
- A template renamed under `skills/project-init/templates/` changes its history
|
|
12
|
-
key without moving any project path; record it in `RETIRED` as
|
|
13
|
-
`{"project-init/old-name.md": "project-init/new-name.md"}`. `RETIRED` also
|
|
14
|
-
takes `None` for a template that is simply gone. The test suite fails on a
|
|
15
|
-
history key that is neither current, nor a `MOVES` source, nor listed there.
|
|
16
|
-
- A migration that deletes must retire the template in the same release,
|
|
17
|
-
otherwise the next run re-creates the file.
|
|
18
|
-
- `ctx.patch_state` is the only way to change a task in flight, and the only
|
|
19
|
-
writer of the task's state file (`.ai/state/state.json`, `current.json` in a
|
|
20
|
-
project not yet at schema 9) other than `state.py`. A migration that
|
|
21
|
-
moves a path a task's step may touch must patch it, or the scope guard will
|
|
22
|
-
stop that task on a file that no longer exists.
|
|
23
|
-
- Operations are planned, never written: the dry run must list everything.
|
|
24
|
-
Paths are project-relative and normalised, and content is `bytes`; the
|
|
25
|
-
`note` of `ctx.conflict` / `ctx.edit_text` and the `reason` of `ctx.delete` are
|
|
26
|
-
`str`. The type is checked when the operation is planned: any other type stops
|
|
27
|
-
the update with a SchemaError (exit 2). A call that changes nothing (an
|
|
28
|
-
`edit_text` whose `fn` returns its input, a `delete` of a path that is not
|
|
29
|
-
there) plans nothing and is not checked.
|
|
30
|
-
- `ctx.edit_json(path, fn)` hands `fn` a deep copy of the document and takes the
|
|
31
|
-
new one back. `fn` must not recurse in Python over the document (a file that
|
|
32
|
-
passed the deep copy can still be ~470-490 levels deep), or it guards
|
|
33
|
-
`RecursionError` itself; an unguarded `fn`'s `RecursionError` fails the run as
|
|
34
|
-
the migration's own bug, naming no file.
|
|
35
|
-
- `plan(ctx)` runs again on every run until the schema version advances, and a
|
|
36
|
-
conflict or an unconfirmed deletion holds it back. Every `fn` must therefore be
|
|
37
|
-
idempotent: append a line only when it is not already there, or the next run
|
|
38
|
-
appends it again.
|
|
39
|
-
|
|
40
|
-
## What a migration can ask the plugin
|
|
41
|
-
|
|
42
|
-
`ctx` carries what the plugin knows about itself, so a migration recognises its
|
|
43
|
-
own text instead of matching a pattern against the human's prose:
|
|
44
|
-
|
|
45
|
-
- `ctx.instruction_files()` — the root instruction files this project actually
|
|
46
|
-
has, one per runtime it declares (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`,
|
|
47
|
-
`.junie/guidelines.md`).
|
|
48
|
-
- `ctx.shipped(path)` — every version of every template the plugin ever
|
|
49
|
-
installed at that path, oldest first, following renames. A migration that
|
|
50
|
-
removes shipped text matches it **verbatim against one of these**; a regular
|
|
51
|
-
expression over the project's prose is how a migration eats a paragraph
|
|
52
|
-
somebody wrote.
|
|
53
|
-
- `ctx.block_status(path)` — `"none"`, `"shipped"` or `"edited"` for the managed
|
|
54
|
-
block. An edited block is never written over, by a migration or by anything
|
|
55
|
-
else.
|
|
56
|
-
- `ctx.hint(text)` — say something to the human that is not an operation on a
|
|
57
|
-
file. The right answer whenever the text is no longer the plugin's.
|
|
58
|
-
- `ctx.conflict(path, note)` — a file the migration cannot handle (wrong JSON
|
|
59
|
-
types, say) is a conflict: it holds the schema version, so the next run tries
|
|
60
|
-
again. `note` says why, and must be `str`: any other type stops the update
|
|
61
|
-
with a SchemaError (`%04d: <path> note must be str, not <type>`, exit 2), as
|
|
62
|
-
content that is not `bytes` does.
|
|
63
|
-
- `ctx.template(path)` — the bytes the plugin ships today at that path, from the
|
|
64
|
-
scaffold or the shared tree. `ctx.is_shipped(path, data)` — whether these bytes
|
|
65
|
-
are a version ever shipped there (CRLF counts; `index.json` alone answers).
|
|
66
|
-
- `ctx.require_shared(paths)` — refuse (exit 2, nothing written) when a shared
|
|
67
|
-
file those copies fall back to is missing from an installed home.
|
|
68
|
-
`ctx.unsafe_target(path)` — why a path must not be deleted (a symlink, or
|
|
69
|
-
outside the project). `ctx.override_note(path)` — the wording for an edited
|
|
70
|
-
shared copy. Words that name a runtime live in `update.py`, never in a
|
|
71
|
-
migration.
|
|
72
|
-
|
|
73
|
-
A migration that edits a file **outside `.ai/`** is editing the project's own
|
|
74
|
-
file: `apply` copies the original to
|
|
75
|
-
`.ai/reports/project-update-<date>/original/<path>` before the first write, so
|
|
76
|
-
what was there is recoverable without git.
|