riskrails 3.0.1 → 3.2.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 +29 -15
- package/ai/policies/model-routing.md +7 -7
- package/ai/policies/production.md +3 -3
- package/ai/policies/release.md +2 -2
- package/ai/policies/review-economy.md +27 -30
- package/bin/riskrails.js +114 -23
- package/hooks/ai-path-guard-defaults.json +3 -2
- package/hooks/context-guard.py +15 -12
- package/hooks/dialog-gate.py +1 -1
- package/hooks/guard/common.py +2 -0
- package/hooks/guard/dialog.py +107 -27
- package/hooks/guard/git.py +12 -3
- package/hooks/guard/paths.py +150 -37
- package/hooks/project-scaffold.py +3 -6
- package/hooks/runtime-gate.py +134 -33
- package/hooks/update_notice.py +22 -16
- package/instructions/routing.md +1 -1
- package/instructions/stub.md +3 -3
- package/package.json +1 -1
- package/skills/AGENTS.md +1 -1
- package/skills/CLAUDE.md +1 -1
- package/skills/ai-init/templates/.ai/AGENTS.md +1 -1
- package/skills/ai-init/templates/.ai/VERSION +1 -1
- package/skills/ai-init/templates/.ai/policies/security.md +7 -1
- package/skills/ai-init/templates/.ai/policies/testing.md +19 -6
- package/skills/ai-init/templates/.ai/state/README.md +4 -4
- package/skills/ai-init/templates/AGENTS.block.md +2 -2
- package/skills/ai-init/templates/CLAUDE.block.md +2 -2
- package/skills/ai-init/templates/GEMINI.block.md +2 -2
- package/skills/ai-init/templates/gitignore.snippet +1 -0
- package/skills/ai-init/templates/junie-guidelines.block.md +2 -2
- package/skills/ai-task/rr_state/__init__.py +1 -0
- package/skills/ai-task/rr_state/cli.py +210 -0
- package/skills/ai-task/rr_state/close.py +171 -0
- package/skills/ai-task/rr_state/dialog.py +541 -0
- package/skills/ai-task/rr_state/dialog_cmd.py +164 -0
- package/skills/ai-task/rr_state/fs.py +220 -0
- package/skills/ai-task/rr_state/gate.py +125 -0
- package/skills/ai-task/rr_state/gate_cmd.py +128 -0
- package/skills/ai-task/rr_state/handoff.py +226 -0
- package/skills/ai-task/rr_state/journal.py +126 -0
- package/skills/ai-task/rr_state/labels.py +99 -0
- package/skills/ai-task/rr_state/ledger.py +145 -0
- package/skills/ai-task/rr_state/locks.py +84 -0
- package/skills/ai-task/rr_state/memory_cmd.py +310 -0
- package/skills/ai-task/rr_state/policy.py +378 -0
- package/skills/ai-task/rr_state/profile.py +373 -0
- package/skills/ai-task/rr_state/questions.py +336 -0
- package/skills/ai-task/rr_state/questions_cmd.py +321 -0
- package/skills/ai-task/rr_state/report.py +271 -0
- package/skills/ai-task/rr_state/report_cmd.py +230 -0
- package/skills/ai-task/rr_state/review.py +165 -0
- package/skills/ai-task/rr_state/runtime.py +80 -0
- package/skills/ai-task/rr_state/start.py +257 -0
- package/skills/ai-task/rr_state/steps.py +131 -0
- package/skills/ai-task/rr_state/task.py +396 -0
- package/skills/ai-task/rr_state/verify.py +231 -0
- package/skills/ai-task/sensors.py +37 -4
- package/skills/ai-task/state.py +24 -5138
- package/skills/project-init/templates/constitution.md +1 -1
- package/skills/project-init/templates/sdlc-README.md +20 -14
- package/skills/project-update/adopt.py +38 -24
- package/skills/project-update/history/blobs/00289f98434fed230eebe768dfb3425f83d737866a3aebc97aa95167c4fa0c6e +131 -0
- package/skills/project-update/history/blobs/079a91f976ad69b5cf49ccb9a28849576e4735ddd9485fc26599e528a69fd2f4 +30 -0
- package/skills/project-update/history/blobs/07ff7fa6f93d471ff533f553b4e523e4105ccde1f3dab70132c5237728f68422 +13 -0
- package/skills/project-update/history/blobs/0ef4a360c76da290173dea0f8073949cb0f8113710a38df95acae5d96bdd1286 +123 -0
- package/skills/project-update/history/blobs/18c7a2753ce9cb6f6d3d1e9d5d1125d75950b879d56b7325225b200869631b29 +15 -0
- package/skills/project-update/history/blobs/25d4f2a86deb5e2574bb3210b67bb24fcc4afb19f93a7b65a057daa874a9d18e +1 -0
- package/skills/project-update/history/blobs/48d9950f16e31d55475f566a80dc95f64b35f1c804d2f51729e313a4f29bac8d +13 -0
- package/skills/project-update/history/blobs/5a93f78b4ed4d416c171139b2ce610a117f9c1dd46fbb56ca86729cb4c53d10c +6 -0
- package/skills/project-update/history/blobs/6a3be0e624e5ad91e44d08857b7183ee53b7903e1e8d56ff196dbc9dde271e4a +75 -0
- package/skills/project-update/history/blobs/7170553410bba904ffb7edb4b231e3b3579b85542d84de717299980938f82c9d +13 -0
- package/skills/project-update/history/blobs/774b1e3a04ef0d49d33c44a2cfecd92eb6f6b738a12b6a681103ba7fc13e7b86 +67 -0
- package/skills/project-update/history/blobs/7f063e79195a5c85c37b7260dd20c184d3b9bea85e37202e0bbca73ad84bba49 +13 -0
- package/skills/project-update/history/blobs/a51517e13e57420d2d88a6bcfb4055d762d4a0d10a8f88450929c5c4572213a9 +67 -0
- package/skills/project-update/history/blobs/c8573ca1f810ce6c4ac49456a4c1725b93e0d3b74c54ce95d46d8747777895f0 +13 -0
- package/skills/project-update/history/blobs/d6e5b34d475e7187d2a8cb2aac7e7f1a427142baf6461bda387d70ceb4d8a8c1 +30 -0
- package/skills/project-update/history/blobs/e120ff4e32b2e950fc94ada4e787d57dc9c790da37223e5a202d74f7e3aadf80 +13 -0
- package/skills/project-update/history/index.json +38 -16
- package/skills/project-update/migrations/0003_context_diet.py +1 -1
- package/skills/project-update/migrations/0007_weight_model.py +130 -23
- package/skills/project-update/migrations/0011_retire_risk_tiers.py +37 -0
- package/skills/project-update/migrations/README.md +16 -1
- package/skills/project-update/migrations/__init__.py +6 -1
- package/skills/project-update/render_instructions.py +1 -1
- package/skills/project-update/update.py +569 -106
- package/skills/rr-build-all/SKILL.md +2 -2
- package/skills/rr-plan/SKILL.md +3 -1
- package/skills/rr-review/SKILL.md +13 -8
- package/skills/rr-setup/SKILL.md +24 -4
- package/skills/rr-setup/setup.py +7 -1
- package/skills/rr-spec/SKILL.md +4 -2
- package/skills/rr-status/status.py +36 -9
- package/skills/usage-report/usage-report.py +4 -2
- package/skills/ai-init/templates/.ai/policies/risk-tiers.json +0 -628
- package/skills/ai-init/templates/.ai/policies/risk-tiers.md +0 -180
|
@@ -54,7 +54,7 @@ TEST_KEYS = ("timeout_seconds", "env_retries", "log_max_bytes")
|
|
|
54
54
|
def _loads(raw):
|
|
55
55
|
try:
|
|
56
56
|
doc = json.loads(raw, object_pairs_hook=OrderedDict)
|
|
57
|
-
except (TypeError, ValueError):
|
|
57
|
+
except (TypeError, ValueError, RecursionError): # nested past the parser's limit: edit_json holds the schema
|
|
58
58
|
return None
|
|
59
59
|
return doc if isinstance(doc, dict) else None
|
|
60
60
|
|
|
@@ -111,21 +111,17 @@ def _hint(doc, axis, paths, note):
|
|
|
111
111
|
_add(hint.setdefault("paths", []), paths)
|
|
112
112
|
|
|
113
113
|
|
|
114
|
-
def
|
|
115
|
-
"""
|
|
116
|
-
|
|
117
|
-
with. A scalar is carried only when the old table changed it and `base`
|
|
118
|
-
still holds the shipped weight value — a project's own weight.json edit
|
|
119
|
-
wins. `base` is not changed, and converting twice adds nothing new."""
|
|
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."""
|
|
120
117
|
shipped_examples, shipped_scopes, shipped_scalars, template = shipped
|
|
121
|
-
|
|
122
|
-
axes = new.setdefault("axes", OrderedDict())
|
|
118
|
+
out = []
|
|
123
119
|
for tier, entry in (old.get("tiers") or {}).items():
|
|
124
120
|
if tier in EXAMPLE_MODE and isinstance(entry, dict):
|
|
125
121
|
extra = [e for e in entry.get("examples") or []
|
|
126
122
|
if isinstance(e, str) and e not in shipped_examples.get(tier, set())]
|
|
127
123
|
if extra:
|
|
128
|
-
|
|
124
|
+
out.append(("list", ("examples", EXAMPLE_MODE[tier]), extra))
|
|
129
125
|
for scope in old.get("path_scopes") or []:
|
|
130
126
|
if not isinstance(scope, dict):
|
|
131
127
|
continue
|
|
@@ -136,23 +132,41 @@ def convert(old, base, shipped):
|
|
|
136
132
|
continue
|
|
137
133
|
if name in DOMAIN or (name not in HINTS and name not in INFRA | NEEDS_STEP
|
|
138
134
|
and scope.get("min_tier") in ("T5", "T6")):
|
|
139
|
-
|
|
135
|
+
out.append(("list", ("axes", "domain", "modules"), paths))
|
|
140
136
|
elif name in HINTS:
|
|
141
|
-
|
|
137
|
+
out.append(("hint", HINTS[name], (paths, "%s (from risk-tiers.json)" % name)))
|
|
142
138
|
elif name in INFRA | NEEDS_STEP:
|
|
143
|
-
rev = axes.setdefault("reversibility", OrderedDict())
|
|
144
139
|
two = [p for p in paths if name in INFRA and "prod" in p]
|
|
145
|
-
|
|
146
|
-
|
|
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]))
|
|
147
142
|
for key, value in _scalars(old).items():
|
|
148
143
|
if value in shipped_scalars.get(key, []) or _get(base, key) != _get(template, key):
|
|
149
144
|
continue
|
|
150
145
|
if key == "tests.max_suite_runs" and isinstance(value, int):
|
|
151
146
|
value = min(value, 2)
|
|
152
|
-
|
|
153
|
-
|
|
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)
|
|
154
168
|
else:
|
|
155
|
-
|
|
169
|
+
node[path[-1]] = value
|
|
156
170
|
return new
|
|
157
171
|
|
|
158
172
|
|
|
@@ -168,19 +182,112 @@ def _mode(state):
|
|
|
168
182
|
return state
|
|
169
183
|
|
|
170
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
|
+
|
|
171
263
|
def plan(ctx):
|
|
172
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
|
|
173
269
|
shipped = _shipped(ctx)
|
|
174
270
|
before = ctx.read(NEW)
|
|
175
271
|
if before is None:
|
|
176
272
|
template = ctx.template(NEW)
|
|
177
273
|
doc = convert(old, _loads(template), shipped) if old is not None else None
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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)
|
|
182
281
|
elif old is not None:
|
|
183
|
-
|
|
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))
|
|
184
291
|
after = ctx.read(NEW)
|
|
185
292
|
if old is not None and _loads(after) != _loads(before or ctx.template(NEW)):
|
|
186
293
|
ctx.hint("%s now carries this project's risk-tier edits: run `state.py weight --mirror` to "
|
|
@@ -0,0 +1,37 @@
|
|
|
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)
|
|
@@ -21,7 +21,17 @@ this directory, named `NNNN_slug.py`, numbered contiguously from `0001`, with
|
|
|
21
21
|
moves a path a task's step may touch must patch it, or the scope guard will
|
|
22
22
|
stop that task on a file that no longer exists.
|
|
23
23
|
- Operations are planned, never written: the dry run must list everything.
|
|
24
|
-
Paths are project-relative and normalised, and content is `bytes
|
|
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.
|
|
25
35
|
- `plan(ctx)` runs again on every run until the schema version advances, and a
|
|
26
36
|
conflict or an unconfirmed deletion holds it back. Every `fn` must therefore be
|
|
27
37
|
idempotent: append a line only when it is not already there, or the next run
|
|
@@ -45,6 +55,11 @@ own text instead of matching a pattern against the human's prose:
|
|
|
45
55
|
else.
|
|
46
56
|
- `ctx.hint(text)` — say something to the human that is not an operation on a
|
|
47
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.
|
|
48
63
|
- `ctx.template(path)` — the bytes the plugin ships today at that path, from the
|
|
49
64
|
scaffold or the shared tree. `ctx.is_shipped(path, data)` — whether these bytes
|
|
50
65
|
are a version ever shipped there (CRLF counts; `index.json` alone answers).
|
|
@@ -29,7 +29,12 @@ NAME_RE = re.compile(r"(\d{4})_[a-z0-9_]+\.py")
|
|
|
29
29
|
# A successor key here joins the rename chain, so an edited file still merges
|
|
30
30
|
# against the version it was installed from. None means the template is simply
|
|
31
31
|
# gone: the key is not an orphan, and there is nothing to merge against.
|
|
32
|
-
RETIRED = {
|
|
32
|
+
RETIRED = {
|
|
33
|
+
# Migration 0011: weight.json replaced the tier policy; the project's copies
|
|
34
|
+
# are proposed for deletion and the plugin no longer ships either file.
|
|
35
|
+
"ai-init/.ai/policies/risk-tiers.json": None,
|
|
36
|
+
"ai-init/.ai/policies/risk-tiers.md": None,
|
|
37
|
+
}
|
|
33
38
|
|
|
34
39
|
|
|
35
40
|
class SchemaError(Exception):
|
|
@@ -166,7 +166,7 @@ def vocabulary(source, runtime):
|
|
|
166
166
|
|
|
167
167
|
# The budgets an installed plugin uses: install.sh copies skills/, not
|
|
168
168
|
# instructions/, so runtimes.json is only there in a checkout. Kept equal to
|
|
169
|
-
# `_budgets` in instructions/runtimes.json by tests/
|
|
169
|
+
# `_budgets` in instructions/runtimes.json by tests/test_project_update.py.
|
|
170
170
|
DEFAULT_BUDGETS = {"global": 2560, "project": 2048, "skeleton": 2048, "skeleton-sdlc": 2048}
|
|
171
171
|
|
|
172
172
|
|