riskrails 3.2.2 → 4.1.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 (149) 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 +50 -84
  5. package/agents/ai-reviewer.md.tmpl +1 -1
  6. package/ai/using-riskrails.md +0 -3
  7. package/bin/menu.js +116 -0
  8. package/bin/riskrails.js +132 -79
  9. package/hooks/ai-path-guard-defaults.json +1 -1
  10. package/hooks/context-guard.py +2 -4
  11. package/hooks/guard/dialog.py +14 -2
  12. package/hooks/guard/paths.py +3 -6
  13. package/hooks/guard/scope.py +1 -3
  14. package/hooks/runtime-gate.py +18 -58
  15. package/instructions/routing.md +2 -2
  16. package/package.json +1 -1
  17. package/scripts/ai-path +1 -1
  18. package/scripts/resolve-profile.py +0 -3
  19. package/skills/ai-audit/templates/ai-sdlc-adoption-plan.md +1 -1
  20. package/skills/ai-audit/templates/play-scoring-rubric.md +1 -1
  21. package/skills/ai-init/scaffold-ai.sh +2 -1
  22. package/skills/ai-init/templates/.ai/AGENTS.md +6 -6
  23. package/skills/ai-init/templates/.ai/VERSION +1 -1
  24. package/skills/ai-init/templates/.ai/policies/safety.md +4 -3
  25. package/skills/ai-init/templates/.ai/policies/security.md +1 -2
  26. package/skills/ai-init/templates/.ai/policies/testing.md +5 -12
  27. package/skills/ai-init/templates/.ai/policies/verification.md +18 -0
  28. package/skills/ai-init/templates/.ai/policies/weight.json +0 -1
  29. package/skills/ai-init/templates/.ai/policies/weight.md +1 -2
  30. package/skills/ai-init/templates/.ai/policies/weight.project.json +4 -0
  31. package/skills/ai-init/templates/.ai/project/architecture.md +1 -1
  32. package/skills/ai-init/templates/.ai/project/initial-assessment.md +2 -2
  33. package/skills/ai-init/templates/.ai/project/overview.md +1 -1
  34. package/skills/ai-init/templates/.ai/rules/README.md +2 -2
  35. package/skills/ai-init/templates/.ai/state/README.md +1 -1
  36. package/skills/ai-task/memory_store.py +8 -9
  37. package/skills/ai-task/rr_state/cli.py +2 -2
  38. package/skills/ai-task/rr_state/dialog.py +2 -4
  39. package/skills/ai-task/rr_state/dialog_cmd.py +183 -6
  40. package/skills/ai-task/rr_state/fs.py +5 -13
  41. package/skills/ai-task/rr_state/labels.py +14 -2
  42. package/skills/ai-task/rr_state/memory_cmd.py +7 -4
  43. package/skills/ai-task/rr_state/profile.py +2 -4
  44. package/skills/ai-task/rr_state/questions_cmd.py +1 -1
  45. package/skills/ai-task/rr_state/report.py +3 -4
  46. package/skills/ai-task/rr_state/runtime.py +1 -1
  47. package/skills/ai-task/rr_state/start.py +7 -7
  48. package/skills/ai-task/sensors.py +10 -11
  49. package/skills/ai-task/state.py +4 -24
  50. package/skills/ai-task/weight.py +17 -17
  51. package/skills/ai-task/weight_overlay.py +38 -0
  52. package/skills/project-init/templates/constitution.md +2 -2
  53. package/skills/project-update/history/blobs/00f5e27ce2d66e3a1d8b222b013ae8fa1fed32612d91a551e0c661cee3ef9c2d +34 -0
  54. package/skills/project-update/history/blobs/0a6b1b6dd06a35582b2e9989a1f41c40abc29254b8e482714fcc6f0a7b16c406 +32 -0
  55. package/skills/project-update/history/blobs/0aeb04b04262683cac6513545683486a73dc473b447bba43b5aaab18403957a6 +34 -0
  56. package/skills/project-update/history/blobs/0ce913a28996f546f72d570b0e5a376362e993ce4d805bc4aae57d38caa44212 +41 -0
  57. package/skills/project-update/history/blobs/0ea9e36446b624b73b2a821231b2ee66a357b43d584a5b23da48b3c800855e65 +41 -0
  58. package/skills/project-update/history/blobs/0fb1d68678a1349195b2e79433bb20174d25874e3bfd0b70410bb4e428b4e4b8 +35 -0
  59. package/skills/project-update/history/blobs/119b2b9d0f9c647a243540724edac6b29afa020c69aa205797bcfa9eb06bc021 +35 -0
  60. package/skills/project-update/history/blobs/1d0fe3ee5fb8260ba610942d4bbdfff7410fbe80ed826ac07f32985031dc2b3f +34 -0
  61. package/skills/project-update/history/blobs/1de9a37ed6402893dcab6e61ddbf15ffd35f15a6d84b18e675a0c5bf4e981f85 +42 -0
  62. package/skills/project-update/history/blobs/1f44e1a236233dcbab58064f39b69f798b35d1d9a29f8c615b5b12854ce633cb +15 -0
  63. package/skills/project-update/history/blobs/1f824645299579cbdd8141642ddbb4bca9fd5fe9d3112f036d8221f0811e0abf +40 -0
  64. package/skills/project-update/history/blobs/235fb88393cfe20dd58b35fe9785cc45baf5ffe21a13d926652d76ec68cea8b7 +35 -0
  65. package/skills/project-update/history/blobs/3918b4ee011d9477b9d2779e2893ab599749c406d4b09439a9748b64fbff58f3 +18 -0
  66. package/skills/project-update/history/blobs/449273200dfec2a56885c881ff34d40e8ecc00da53c24c2d09b7fbeb1004d255 +40 -0
  67. package/skills/project-update/history/blobs/4c45aee9b9fc5f6b4e1d9739e434c4094975a5a8dec6b9703fb25f3080680c77 +124 -0
  68. package/skills/project-update/history/blobs/4dd3fa12bc45acc8c836ab65d790248ca7504a021bca6677499d17bb3d798ed9 +64 -0
  69. package/skills/project-update/history/blobs/51e3f15095d7123ea8566b512479ee10a6cb2f336b7352a259a083a887bd7116 +33 -0
  70. package/skills/project-update/history/blobs/5497a204967bea7dab29079a6e1dfa3d8713b2d2cb294c2779378039f03a55fc +36 -0
  71. package/skills/project-update/history/blobs/60c4d7e7fde7049d92d84295fc13ca9a91eb2de423be08f093b3ee5d5e817ff8 +64 -0
  72. package/skills/project-update/history/blobs/6221b7c14cc04634e485e562d464c6860d45666a7f63e293c06bb2944ff556cc +27 -0
  73. package/skills/project-update/history/blobs/637971cfb844ef4285e2886a4b1c834fb2ed5c46e773adb88c2c147fea626ad3 +33 -0
  74. package/skills/project-update/history/blobs/65af2282eea2a51fcc27a4acf2c26c727b0961c30e1be13176369200c460c219 +34 -0
  75. package/skills/project-update/history/blobs/66ecd73e303e7bbc1aa8ee72435d0d10812ff7b2eff0adac59bbf0a303e2cc01 +41 -0
  76. package/skills/project-update/history/blobs/6a43bf32461b22b87fafe2865529dc04fa66cd00182a2a7598aa071667ca255a +35 -0
  77. package/skills/project-update/history/blobs/6d6dee8634abf9ac057bb206828b83a893a2afcf4f86bbe69e839a3b47d0adde +48 -0
  78. package/skills/project-update/history/blobs/7224ede803ea81e65a456e21b6028a0e848e8a880456c6503eebe7534ca4f6aa +74 -0
  79. package/skills/project-update/history/blobs/765a39a202617096c232017822ae738d37e4405430c71700dc7bc14bf1ba794e +33 -0
  80. package/skills/project-update/history/blobs/77577440a933d1cabd8c6f4a6401cf361a784aa09a25500a6f7b44ead8ab5e55 +41 -0
  81. package/skills/project-update/history/blobs/7bae90789625d6fb9071ecb412d14a8d1eb0bc5659d8d284c4eaf213e1b5de56 +67 -0
  82. package/skills/project-update/history/blobs/7bcdf9a0e6b85277b9b60669150224b9414d2e32821ff06f4d148eb1d18785bb +42 -0
  83. package/skills/project-update/history/blobs/7f834103e9d97aea44e76717bcdcbbe349c560e5b81a42ce85a1cec341041281 +4 -0
  84. package/skills/project-update/history/blobs/7fb3acb0211818efebc2234863db0b8c846022a7280a6d2d69f267dbf63db48c +34 -0
  85. package/skills/project-update/history/blobs/87f0855859ff68cc6c988cc0b631ee7f56af7fbba062172bf80e01f4982cc054 +35 -0
  86. package/skills/project-update/history/blobs/8abab74759766d02b5cb7b0c3e59a3d440b25a454216cd8fea325c23fd822d36 +33 -0
  87. package/skills/project-update/history/blobs/8ec171f0fbdf38cc6996d496f01491fd1f21f04a435a118ad979b127a8be06f1 +85 -0
  88. package/skills/project-update/history/blobs/a1fb50e6c86fae1679ef3351296fd6713411a08cf8dd1790a4fd05fae8688164 +1 -0
  89. package/skills/project-update/history/blobs/a3df7268feb0780be0d2cf9acced6ed81780a7790f36fe024b6ea0b1bd6eb7a6 +35 -0
  90. package/skills/project-update/history/blobs/b28721136482dafb9fd2801af340364cb97e9f6eaa987908606c786045b8d334 +28 -0
  91. package/skills/project-update/history/blobs/b3726422d767f9219fea633bea77f0c99ad9af843c591e6e928e1ccd2a47f374 +27 -0
  92. package/skills/project-update/history/blobs/b3839425f9d0ab2efa42a6f5e9488ec2d26ca5fb8dcd694b8ab098e10743ca46 +32 -0
  93. package/skills/project-update/history/blobs/ba14a457e83f9dde65e4190fdfab83b912f7398c1442a2a1cb3d89bfac741cf3 +8 -0
  94. package/skills/project-update/history/blobs/bceb7189444bb6291b262ca08e34db01010dda2f6e64d696231ec3c343c95c66 +34 -0
  95. package/skills/project-update/history/blobs/bf82a2bb4fce1bd349ff66118930a01d61a7af8e2bd8d200c721d43afc58c713 +34 -0
  96. package/skills/project-update/history/blobs/c4dbeb180292a6199ad73a3be861ece07c1f8bf6bf0cc7ca204412bc8e1bb1d8 +31 -0
  97. package/skills/project-update/history/blobs/c644838866df2001a42501a988dd7b77aacf862bf09d22b465f3cd8a22c6b6eb +33 -0
  98. package/skills/project-update/history/blobs/c980d74f76a4bdfb38c94b573ac10a54eb50b850b052e15fa6f2442889b36ccb +32 -0
  99. package/skills/project-update/history/blobs/ceed2f3bd937f862d0e6ca59ce1b1e3603e19a0d7f85d90d6dfeb7bf690153be +29 -0
  100. package/skills/project-update/history/blobs/d51ea2e893482a20c5882056cf8043327ca1dc1c957927f6746929a47ee8368e +45 -0
  101. package/skills/project-update/history/blobs/d78f5e0bb1380259fce2cce009785a9a11f6de8b87a160080b9872c059f95060 +34 -0
  102. package/skills/project-update/history/blobs/d97f46a2cca020406252b1f7baa4184617fa71faca3885e2379c44ae4e1d9327 +67 -0
  103. package/skills/project-update/history/blobs/da3ecd4c4c414c93d04488685931df39a56bf959bfeb9c45cb1aea2c13bd991b +38 -0
  104. package/skills/project-update/history/blobs/e129bf71a5a922e24b2c1bc8daa8a5d2b5589b0069770e17ce17e7db5ed4e7a4 +27 -0
  105. package/skills/project-update/history/blobs/e504529519a371892838725f8b89b5d32e3300e345bcbf2643a3a1a48434b3dd +24 -0
  106. package/skills/project-update/history/blobs/e9e62d03ca70b3dffccef8dceeb214173352cfef005145e519f0e037993f1c03 +36 -0
  107. package/skills/project-update/history/blobs/effefbaaf4beb8f1ba55081d6b7e74bf25ebb91045dd10613418f17b95fa7182 +33 -0
  108. package/skills/project-update/history/blobs/f6af4581bafbf62b257d57e06c3ae2c5998aeca608ee7df037fcc3c3e4b563a9 +34 -0
  109. package/skills/project-update/history/blobs/fc35045777d29630303430752a651b3e553580a581036a02cc103eb96a46f4b0 +34 -0
  110. package/skills/project-update/history/blobs/fdc21345c77f92a42d244aa104f393f439a0ff9981a9fd462bd837e8a47931bb +48 -0
  111. package/skills/project-update/history/index.json +34 -14
  112. package/skills/project-update/render_instructions.py +12 -45
  113. package/skills/project-update/update.py +757 -1172
  114. package/skills/rr-setup/SKILL.md +114 -97
  115. package/skills/rr-setup/scaffold_ai.py +15 -9
  116. package/skills/rr-setup/setup.py +462 -164
  117. package/skills/rr-status/status.py +7 -10
  118. package/skills/usage-report/usage-report.py +4 -5
  119. package/skills/ai-audit/SKILL.md +0 -9
  120. package/skills/ai-init/SKILL.md +0 -9
  121. package/skills/ai-status/SKILL.md +0 -9
  122. package/skills/ai-task/SKILL.md +0 -9
  123. package/skills/project-init/SKILL.md +0 -9
  124. package/skills/project-update/SKILL.md +0 -9
  125. package/skills/project-update/adopt-map.json +0 -91
  126. package/skills/project-update/adopt.py +0 -2225
  127. package/skills/project-update/migrations/0001_schema_version.py +0 -13
  128. package/skills/project-update/migrations/0002_task_journal.py +0 -123
  129. package/skills/project-update/migrations/0003_context_diet.py +0 -65
  130. package/skills/project-update/migrations/0004_deterministic_gates.py +0 -57
  131. package/skills/project-update/migrations/0005_runtimes_and_plans.py +0 -35
  132. package/skills/project-update/migrations/0006_shared_files.py +0 -154
  133. package/skills/project-update/migrations/0007_weight_model.py +0 -295
  134. package/skills/project-update/migrations/0008_renames.py +0 -43
  135. package/skills/project-update/migrations/0009_state_file.py +0 -25
  136. package/skills/project-update/migrations/0010_dialog_switch.py +0 -30
  137. package/skills/project-update/migrations/0011_retire_risk_tiers.py +0 -37
  138. package/skills/project-update/migrations/README.md +0 -76
  139. package/skills/project-update/migrations/__init__.py +0 -186
  140. package/skills/sdlc-intent/SKILL.md +0 -9
  141. package/skills/sdlc-plan/SKILL.md +0 -9
  142. package/skills/sdlc-spec/SKILL.md +0 -9
  143. package/skills/usage-report/SKILL.md +0 -10
  144. /package/{ai/workflows/refactoring.md → skills/project-update/history/blobs/36cb593293496fbcf38df828a9b9b2bc0d5a53333f584a5319ebc3d799016e24} +0 -0
  145. /package/{ai/workflows/feature.md → skills/project-update/history/blobs/56e16072c2f3edc6c8897c690d09a11d12fc414c3e3f361f359f7dc609a21282} +0 -0
  146. /package/{ai/workflows/hotfix.md → skills/project-update/history/blobs/8cbefbf1aa412bae3e5019acd40b8fe0598fceba6c781906b3bc6dbd014f6b23} +0 -0
  147. /package/{ai/workflows/bugfix.md → skills/project-update/history/blobs/af56dc16e44ae1d8c3dac4c3ca82e2163b719553c02ea455e77430bab9a14089} +0 -0
  148. /package/{ai/workflows/investigation.md → skills/project-update/history/blobs/b9e09cb24958cd6831589997558974bd6356305306e8f9685e226bcdc30d60ac} +0 -0
  149. /package/{ai/agents/manager.md → skills/project-update/history/blobs/d0cf6388f35a92a0cf607d33909278382f6d7b6a4c32e281b9f9e0ea652c7b91} +0 -0
package/bin/riskrails.js CHANGED
@@ -37,6 +37,12 @@
37
37
  // variables CLAUDE_DIR and CODEX_DIR mean the same). Naming exactly one of them under
38
38
  // --target auto installs that runtime alone.
39
39
  // --dry-run prints everything that would be written, per runtime, and writes nothing.
40
+ // On a terminal each choice no flag names is an arrow-key menu, in this order: runtime (only
41
+ // without --target, --home and --codex-home), plan, fable (max, max20, team-max),
42
+ // context, statusline, codex plan. The proposal above is highlighted and Enter
43
+ // confirms it (an undetected plan proposes pro); ↑/↓ or k/j, Home/End and 1-9 move;
44
+ // Esc, q or Ctrl-C stop with nothing written (exit 130). RISKRAILS_MENU=off or 0, or
45
+ // TERM=dumb: the typed plan question instead. --yes or no terminal: no question.
40
46
  //
41
47
  // This is the port of the install.sh that shipped up to 2.0.2: the same tree, the same
42
48
  // messages, the same backups (backups/<date>/<path> under the runtime's home). JSON merges
@@ -65,6 +71,8 @@ const HOME = env.HOME || os.homedir();
65
71
  let PLAN = '', FABLE = 'auto', DRY = false, TARGET = 'auto', PLAN_GIVEN = false;
66
72
  let CODEX_PLAN = '', CODEX_PLAN_GIVEN = false, YES = false;
67
73
  let CONTEXT = 'default', STATUSLINE_CONTEXT = 'yes', STATUSLINE = 'riskrails', STATUSLINE_GIVEN = false;
74
+ // A flag that names a choice skips its menu on a terminal (bin/menu.js).
75
+ let TARGET_GIVEN = false, FABLE_GIVEN = false, CONTEXT_GIVEN = false;
68
76
 
69
77
  function version() {
70
78
  try { return JSON.parse(fs.readFileSync(path.join(SRC, 'package.json'), 'utf8')).version || 'unknown'; }
@@ -90,11 +98,11 @@ function usage() {
90
98
  return v;
91
99
  };
92
100
  switch (a) {
93
- case '--target': TARGET = take(); break;
101
+ case '--target': TARGET = take(); TARGET_GIVEN = true; break;
94
102
  case '--plan': PLAN = take(); PLAN_GIVEN = true; break;
95
- case '--fable': FABLE = take(); PLAN_GIVEN = true; break;
103
+ case '--fable': FABLE = take(); PLAN_GIVEN = true; FABLE_GIVEN = true; break;
96
104
  case '--codex-plan': CODEX_PLAN = take(); CODEX_PLAN_GIVEN = true; break;
97
- case '--context': CONTEXT = take(); break;
105
+ case '--context': CONTEXT = take(); CONTEXT_GIVEN = true; break;
98
106
  case '--statusline-context': STATUSLINE_CONTEXT = take(); break;
99
107
  case '--statusline': STATUSLINE = take(); STATUSLINE_GIVEN = true; break;
100
108
  case '--home': CLAUDE_DIR = path.resolve(take()); CLAUDE_DIR_GIVEN = true; break;
@@ -152,7 +160,6 @@ const exists = (p) => fs.existsSync(p); // follows symlink
152
160
  const isDir = (p) => { try { return fs.statSync(p).isDirectory(); } catch (e) { return false; } };
153
161
  const isFile = (p) => { try { return fs.statSync(p).isFile(); } catch (e) { return false; } };
154
162
  const isLink = (p) => { try { return fs.lstatSync(p).isSymbolicLink(); } catch (e) { return false; } };
155
- const lexists = (p) => { try { fs.lstatSync(p); return true; } catch (e) { return false; } };
156
163
  const read = (p) => fs.readFileSync(p, 'utf8');
157
164
  const readJSON = (p) => JSON.parse(read(p));
158
165
  const pretty = (v) => JSON.stringify(v, null, 2);
@@ -261,6 +268,51 @@ function ask(prompt) {
261
268
  }
262
269
  const interactive = () => !YES && process.stdin.isTTY === true;
263
270
 
271
+ // A choice on a terminal, as an arrow-key menu: bin/menu.js runs as a child that owns the
272
+ // terminal (raw mode, the menu on stderr) and writes the picked value to stdout. null means
273
+ // no menu — not a terminal, RISKRAILS_MENU=off|0, TERM=dumb, or the child could not take the
274
+ // terminal (exit 3) — and the caller asks as before. An abort exits 130 here, before anything
275
+ // is written: every menu runs in claudeRender/codexRender, ahead of the apply steps.
276
+ const menuOff = () => !interactive() || ['off', '0'].includes(env.RISKRAILS_MENU) || env.TERM === 'dumb';
277
+ function choose(title, items, initial, why) {
278
+ if (menuOff() || initial < 0) return null;
279
+ const spec = JSON.stringify({ title, why: why || '', items, initial });
280
+ const r = spawnSync(process.execPath, [path.join(__dirname, 'menu.js')],
281
+ { stdio: ['inherit', 'pipe', 'inherit'], env: { ...env, RISKRAILS_MENU_SPEC: spec }, encoding: 'utf8' });
282
+ if (r.status === 0 && items.some((it) => it.value === r.stdout)) return r.stdout;
283
+ if (r.status === 130 || r.signal) {
284
+ if (r.signal) err(`riskrails: the menu was stopped by ${r.signal}; run \`stty sane\` if the terminal misbehaves`);
285
+ die('riskrails: aborted — nothing was written', 130);
286
+ }
287
+ return null;
288
+ }
289
+ const pick = (values, notes) => values.map((v, i) => ({ value: v, label: v, note: notes[i] || '' }));
290
+
291
+ // --context on a terminal, asked once: by the Claude branch, else before the Codex plan.
292
+ let CONTEXT_ASKED = false;
293
+ function askContext() {
294
+ if (CONTEXT_GIVEN || CONTEXT_ASKED) return;
295
+ CONTEXT_ASKED = true;
296
+ const c = choose('context', pick(['default', 'economy'], ["each runtime's own context window and compaction",
297
+ "the plan's autoCompactWindow; context-guard also holds a prompt back past 120% of it"]), 0);
298
+ if (c) CONTEXT = c;
299
+ }
300
+ // --statusline on a terminal: the mode riskrails/statusline.json records is the proposal, read as
301
+ // statuslineGate reads it. Only a different pick counts as given, so Enter keeps today's run.
302
+ function askStatusline() {
303
+ if (STATUSLINE_GIVEN) return;
304
+ let mode = '';
305
+ try {
306
+ const r = readJSON(path.join(CLAUDE_DIR, 'riskrails', 'statusline.json'));
307
+ if (isObj(r) && ['riskrails', 'own'].includes(r.mode)) mode = r.mode;
308
+ } catch (e) { /* none yet */ }
309
+ const MODES = ['riskrails', 'own'];
310
+ const v = choose('statusline', pick(MODES, ["the plugin's statusline over your own command (kept, --statusline own brings it back)",
311
+ 'your own statusline command, behind the quota check']), MODES.indexOf(mode || 'riskrails'),
312
+ mode ? 'recorded by the previous install' : '');
313
+ if (v && v !== (mode || 'riskrails')) { STATUSLINE = v; STATUSLINE_GIVEN = true; }
314
+ }
315
+
264
316
  // ------------------------------------------------------------------ runtime detection
265
317
  let DO_CLAUDE = false, DO_CODEX = false;
266
318
  const skippedRuntime = (rt, dir) => err(`${rt}: ${dir} found but no ${rt} on PATH — skipped (--target ${rt} for it alone, --target both for both)`);
@@ -288,7 +340,16 @@ Code or Codex first, or name the runtime yourself:
288
340
  npx riskrails --target both # both, whether or not they are detected`);
289
341
  process.exit(1);
290
342
  }
291
- if (TARGET === 'auto') out('detected runtime:' + (DO_CLAUDE ? ' claude' : '') + (DO_CODEX ? ' codex' : ''));
343
+ // On a terminal, with nothing naming the runtime, the detected set is the menu's first proposal.
344
+ let RUNTIME_PICKED = false;
345
+ if (TARGET === 'auto' && !TARGET_GIVEN && !CLAUDE_DIR_GIVEN && !CODEX_DIR_GIVEN) {
346
+ const RUNTIMES = ['claude', 'codex', 'both'];
347
+ const detected = DO_CLAUDE && DO_CODEX ? 'both' : DO_CLAUDE ? 'claude' : 'codex';
348
+ const rt = choose('runtime', pick(RUNTIMES, ['Claude Code', 'Codex', 'Claude Code and Codex']),
349
+ RUNTIMES.indexOf(detected), `detected: ${detected}`);
350
+ if (rt && rt !== detected) { DO_CLAUDE = rt !== 'codex'; DO_CODEX = rt !== 'claude'; RUNTIME_PICKED = true; }
351
+ }
352
+ if (TARGET === 'auto' && !RUNTIME_PICKED) out('detected runtime:' + (DO_CLAUDE ? ' claude' : '') + (DO_CODEX ? ' codex' : ''));
292
353
 
293
354
  const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'riskrails-'));
294
355
  process.on('exit', () => rmrf(TMP));
@@ -311,7 +372,7 @@ function renderStub(scope, runtime, dst, vars, kind = 'block') {
311
372
  // block_size — what the always-loaded block costs, and how much of the file is the user's own.
312
373
  function blockSize(file, budget) {
313
374
  const text = read(file);
314
- const m = text.match(/<!-- (riskrails|claude-agentic|[Cc]odex-agentic):start -->[\s\S]*?<!-- \1:end -->/);
375
+ const m = text.match(/<!-- riskrails:start -->[\s\S]*?<!-- riskrails:end -->/);
315
376
  const block = m ? Buffer.byteLength(m[0]) : 0;
316
377
  out(`${path.basename(file)}: managed block ${spaced(block)} B (budget ${spaced(budget)}), file ${spaced(Buffer.byteLength(text))} B — the rest is yours`);
317
378
  }
@@ -358,12 +419,10 @@ function writeProfile(home, profile) {
358
419
  }
359
420
 
360
421
  function prevPlan(home) {
361
- for (const name of ['riskrails', 'claude-agentic']) { // before 3.4b it lived under claude-agentic/ (R35)
362
- try {
363
- const plan = readJSON(path.join(home, name, 'profile.json')).plan;
364
- return plan === null || plan === undefined || plan === false ? '' : String(plan);
365
- } catch (e) { /* next */ }
366
- }
422
+ try {
423
+ const plan = readJSON(path.join(home, 'riskrails', 'profile.json')).plan;
424
+ return plan === null || plan === undefined || plan === false ? '' : String(plan);
425
+ } catch (e) { /* none */ }
367
426
  return '';
368
427
  }
369
428
 
@@ -392,21 +451,37 @@ function detectPlan(runtime) {
392
451
  return {};
393
452
  }
394
453
 
454
+ // The plan question on a terminal: the plan the previous install recorded, else the detected
455
+ // one, is proposed and Enter confirms it; a Team seat, or a plan nothing detected, is typed.
456
+ // On a terminal with the menu on, the six plans are a menu instead: the proposal is the same,
457
+ // and without one it is team-pro for a Team org, else pro (Enter installs it rather than exiting).
458
+ const CLAUDE_PLANS = ['pro', 'team-pro', 'team-max', 'max', 'max20', 'balanced-max'];
459
+ const CLAUDE_PLAN_NOTES = ['Pro', 'Team Standard seat', 'Team Premium seat', 'Max',
460
+ "Max 20x: max's settings, larger budgets", 'Max account, only the session runs Opus 5.5 (200k window)'];
461
+ function pickClaudePlan(prev, detected, from, org) {
462
+ const proposed = prev || detected;
463
+ const first = proposed || (from === 'team' ? 'team-pro' : 'pro');
464
+ const menuWhy = prev ? 'recorded by the previous install' : detected ? `detected from ${from}`
465
+ : from === 'team' ? `Team org ${org}, seat unknown` : `could not detect the plan, organizationType='${org}'`;
466
+ const picked = choose('plan', pick(CLAUDE_PLANS, CLAUDE_PLAN_NOTES), CLAUDE_PLANS.indexOf(first), menuWhy);
467
+ if (picked !== null) return picked;
468
+ if (proposed) {
469
+ const why = prev ? 'recorded by the previous install' : `detected from ${from}`;
470
+ return ask(`plan: ${proposed} (${why}) — Enter to confirm, or type pro/team-pro/team-max/max/max20/balanced-max: `) || proposed;
471
+ } else if (from === 'team') {
472
+ return ask(`Team org detected (${org}) but not the seat. Enter plan [team-pro/team-max]: `);
473
+ } else {
474
+ return ask(`Could not detect plan (organizationType='${org}'). Enter plan [pro/team-pro/team-max/max/max20]: `);
475
+ }
476
+ }
477
+
395
478
  function claudeRender() {
396
479
  const PREV_PLAN = prevPlan(CLAUDE_DIR);
397
480
  if (!PLAN) {
398
481
  const live = detectPlan('claude');
399
482
  const org = live.org || '', detected = live.plan || '', from = live.from || '';
400
483
  if (interactive()) {
401
- const proposed = PREV_PLAN || detected;
402
- if (proposed) {
403
- const why = PREV_PLAN ? 'recorded by the previous install' : `detected from ${from}`;
404
- PLAN = ask(`plan: ${proposed} (${why}) — Enter to confirm, or type pro/team-pro/team-max/max/max20/balanced-max: `) || proposed;
405
- } else if (from === 'team') {
406
- PLAN = ask(`Team org detected (${org}) but not the seat. Enter plan [team-pro/team-max]: `);
407
- } else {
408
- PLAN = ask(`Could not detect plan (organizationType='${org}'). Enter plan [pro/team-pro/team-max/max/max20]: `);
409
- }
484
+ PLAN = pickClaudePlan(PREV_PLAN, detected, from, org);
410
485
  } else if (detected) {
411
486
  PLAN = detected; err(`detected plan: ${PLAN} (${from})`);
412
487
  } else if (from === 'team') {
@@ -428,6 +503,14 @@ function claudeRender() {
428
503
  if (['balanced_max', 'balancedmax'].includes(PLAN)) PLAN = 'balanced-max';
429
504
  if (!plans[PLAN]) die(`--plan must be pro, team-pro, team-max, max, max20 or balanced-max (got '${PLAN}')`, 2);
430
505
  const [TIER, PROFILE_NAME, PLAN_LABEL] = plans[PLAN];
506
+ // The option menus on a terminal, each skipped by its own flag: fable, context, statusline.
507
+ if (!FABLE_GIVEN && TIER === 'max' && PLAN !== 'balanced-max') {
508
+ const f = choose('fable', pick(['yes', 'no'], ['Fable 5.1 pinned on architect, the runtime gate on',
509
+ 'no Fable anywhere in this install']), 0, `auto on ${PLAN}`);
510
+ if (f === 'no') FABLE = 'no';
511
+ }
512
+ askContext();
513
+ askStatusline();
431
514
  if (FABLE === 'auto') FABLE = TIER === 'max' && PLAN !== 'balanced-max' ? 'yes' : 'no';
432
515
  else if (!['yes', 'no'].includes(FABLE)) die('--fable must be auto|yes|no', 2);
433
516
  if ((TIER === 'pro' || PLAN === 'balanced-max') && FABLE === 'yes') {
@@ -748,15 +831,13 @@ function claudeDryRun() {
748
831
  if (C.TIER === 'max') out(` bin: claude-1m (opus[1m]${FABLE === 'yes' ? ' / fable[1m]' : ''}, pins the model at launch; compacts near ${C.ONE_M_AT})`);
749
832
  const plugin = claudePlugin(CLAUDE_DIR);
750
833
  skillsLine(plugin);
751
- out(' retire: skills/status (now skills/rr-status), into backups/<date>/ if present');
834
+ out(' retire: skills/{status,ai-status,sdlc-intent,sdlc-plan,sdlc-spec}, into backups/<date>/ if present');
752
835
  shimLine('settings.json');
753
836
  pluginLine(plugin, true);
754
- homeLine(CLAUDE_DIR);
755
837
  sharedLine();
756
838
  out(' config: ai-git-guard.json (only if absent)');
757
839
  out(' routing: riskrails/routing.md');
758
840
  out(' profile: riskrails/profile.json');
759
- out('== would migrate: the claude-routing managed block, if present, into this one');
760
841
  }
761
842
 
762
843
  // Every backup goes to one dated directory, <root>/backups/<date>/<path>: a backup beside
@@ -780,18 +861,6 @@ function retire(root, rel) { // move what nothing installs
780
861
  out(`retired: ${rel} -> backups/${BACKUP_DATE}/${rel}`);
781
862
  }
782
863
 
783
- // move_home — R35: the install home was <root>/claude-agentic/ before 3.4b. It moves to
784
- // <root>/riskrails/ and the old path becomes a symlink to it for one version. A real old
785
- // home beside an existing new one (or a link there, dangling or not) is retired.
786
- function moveHome(root) {
787
- const old = path.join(root, 'claude-agentic'), nw = path.join(root, 'riskrails');
788
- if (!isDir(old) || isLink(old)) return;
789
- if (lexists(nw)) retire(root, 'claude-agentic');
790
- else { fs.renameSync(old, nw); out('moved: claude-agentic/ -> riskrails/'); }
791
- fs.symlinkSync('riskrails', old);
792
- out('linked: claude-agentic -> riskrails (for one version)');
793
- }
794
-
795
864
  // sweep_bak — every *.bak an older installer left beside a live file moves into
796
865
  // backups/<date>/, keeping its path. Only the directories and root files this installer writes.
797
866
  function sweepBak(root, ...names) {
@@ -808,6 +877,8 @@ function sweepBak(root, ...names) {
808
877
  }
809
878
  }
810
879
 
880
+ const RETIRED_SKILLS = ['status', 'ai-status', 'sdlc-intent', 'sdlc-plan', 'sdlc-spec'];
881
+
811
882
  // The seven agents the 2026-09-02 install shipped, and the nine redesign 2.4 removed (with
812
883
  // the Codex -strong/-balanced twins). Moved, not deleted: one mv out of backups/ restores one.
813
884
  const STALE_AGENTS = ['code-implementer', 'qa-runner', 'qa-fixer', 'db-migrator', 'doc-lookup', 'log-inspector',
@@ -831,8 +902,8 @@ function installFile(src, dst, root = CLAUDE_DIR) { // back up on a real change
831
902
  // their scripts (state.py, ai-path, ...) through $AI_HOME/skills/ as on the npx route.
832
903
  function installSkills(root, plugin) {
833
904
  const backups = path.join(root, 'backups', BACKUP_DATE, 'skills');
834
- // /status became /rr-status (redesign 2.3); a leftover would load as a second status command.
835
- retire(root, 'skills/status');
905
+ // /status became /rr-status (2.3) and the ai-status and sdlc-* aliases ended in 4.0: a leftover would load as a second command.
906
+ for (const n of RETIRED_SKILLS) retire(root, `skills/${n}`);
836
907
  for (const name of ls(path.join(SRC, 'skills')).filter((n) => isDir(path.join(SRC, 'skills', n)))) {
837
908
  const d = path.join(SRC, 'skills', name), dst = path.join(root, 'skills', name);
838
909
  // A developer who ran the scripts in place leaves __pycache__ behind; it is not copied.
@@ -879,43 +950,15 @@ function installShared(root) {
879
950
  installFile(path.join(SRC, 'scripts/resolve-profile.py'), path.join(home, 'scripts/resolve-profile.py'), root);
880
951
  }
881
952
 
882
- function homeLine(root) {
883
- const old = path.join(root, 'claude-agentic');
884
- if (isDir(old) && !isLink(old)) out(' home: claude-agentic/ -> riskrails/, the old path a symlink for one version');
885
- }
886
953
  const sharedLine = () => out(` shared: riskrails/ai (${countFiles(path.join(SRC, 'ai'))} files), riskrails/ai-path`);
887
954
 
888
- // managed_block — replace the one managed block, or append it; migrates the predecessor
889
- // plugin's block and an unmarked legacy section on the way. Backs up a non-empty target first.
955
+ // managed_block — replace the one managed block, or append it. Backs up a non-empty target first.
890
956
  function managedBlock(target, blockFile, label) {
891
957
  if (!exists(target)) fs.writeFileSync(target, '');
892
958
  if (fs.statSync(target).size > 0) backupFile(target, path.dirname(target));
893
959
  const block = read(blockFile).replace(/^\n+|\n+$/g, '');
894
960
  let text = read(target);
895
961
  const start = '<!-- riskrails:start -->', end = '<!-- riskrails:end -->';
896
- const notes = [];
897
- const routing = text.match(/<!-- claude-routing:start -->[\s\S]*?<!-- claude-routing:end -->\n?/);
898
- if (routing) {
899
- text = text.slice(0, routing.index) + text.slice(routing.index + routing[0].length);
900
- notes.push('migrated the claude-routing block');
901
- }
902
- const legacy = text.match(/^# Model allocation by task and scope[\s\S]*?(?=^# |(?![\s\S]))/m);
903
- if (legacy && !text.slice(0, legacy.index).includes(start)) {
904
- text = text.slice(0, legacy.index) + text.slice(legacy.index + legacy[0].length);
905
- notes.push("migrated an unmarked 'Model allocation' section");
906
- }
907
- // A legacy claude-agentic (before 3.4b) or Codex-agentic pair is the managed block under its
908
- // old name: rename it, never append beside it.
909
- if (!text.includes(start) && !text.includes(end)) {
910
- const pair = text.match(/<!-- (claude-agentic|[Cc]odex-agentic):start -->[\s\S]*?<!-- \1:end -->/);
911
- if (pair) {
912
- const name = pair[1];
913
- text = text.slice(0, pair.index)
914
- + pair[0].replace(`<!-- ${name}:start -->`, start).replace(`<!-- ${name}:end -->`, end)
915
- + text.slice(pair.index + pair[0].length);
916
- notes.push(`renamed the legacy ${name} markers`);
917
- }
918
- }
919
962
  let nw, action;
920
963
  if (text.includes(start) && text.includes(end)) {
921
964
  const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -928,7 +971,7 @@ function managedBlock(target, blockFile, label) {
928
971
  }
929
972
  nw = nw.replace(/\n{4,}/g, '\n\n\n');
930
973
  fs.writeFileSync(target, nw);
931
- out(`${label}: ${action}${notes.length ? ' + ' + notes.join(' + ') : ''}`);
974
+ out(`${label}: ${action}`);
932
975
  }
933
976
 
934
977
  // The three shell guards 3.5 retired: ai-guard.py runs all of them in one process. Their
@@ -1114,7 +1157,6 @@ const loadJSONFile = (file) => JSON.parse(read(file));
1114
1157
  const writeJSONFile = (file, doc) => { fs.writeFileSync(file + '.tmp', pretty(doc) + '\n'); fs.renameSync(file + '.tmp', file); };
1115
1158
 
1116
1159
  function claudeApply() {
1117
- moveHome(CLAUDE_DIR);
1118
1160
  for (const d of ['agents', 'hooks/lib', 'hooks/guard', 'skills']) mkdirp(path.join(CLAUDE_DIR, d));
1119
1161
  sweepBak(CLAUDE_DIR, 'settings.json', 'CLAUDE.md');
1120
1162
 
@@ -1308,22 +1350,35 @@ function claudeSummary() {
1308
1350
  // ~/.codex/auth.json carries chatgpt_plan_type.
1309
1351
  const X = {}; // the Codex branch's rendered values
1310
1352
 
1353
+ // The Codex plan question on a terminal: the previous install's plan, else the detected one,
1354
+ // is proposed and Enter confirms it; without either the plan is typed.
1355
+ const CODEX_PLANS = ['plus', 'pro', 'balanced-max'];
1356
+ const CODEX_PLAN_NOTES = ['ChatGPT Plus', 'ChatGPT Pro', 'only the session runs the strong model'];
1357
+ function pickCodexPlan(prev, det, why, detected) {
1358
+ const proposed = prev || det;
1359
+ const menuWhy = prev ? 'recorded by the previous install' : det ? `detected from ${why}`
1360
+ : `could not detect the ChatGPT plan, chatgpt_plan_type='${detected}'`;
1361
+ const picked = choose('codex plan', pick(CODEX_PLANS, CODEX_PLAN_NOTES), CODEX_PLANS.indexOf(proposed || 'pro'), menuWhy);
1362
+ if (picked !== null) return picked;
1363
+ if (proposed) {
1364
+ const w = prev ? 'recorded by the previous install' : `detected from ${why}`;
1365
+ return ask(`codex plan: ${proposed} (${w}) — Enter to confirm, or type plus/pro/balanced-max: `) || proposed;
1366
+ } else {
1367
+ return ask(`Could not detect the ChatGPT plan (chatgpt_plan_type='${detected}'). Enter plan [plus/pro]: `);
1368
+ }
1369
+ }
1370
+
1311
1371
  function codexRender() {
1312
1372
  const prev = prevPlan(CODEX_DIR);
1313
1373
  // BalancedMax is one routing idea on both runtimes: --plan balanced-max selects the Codex
1314
1374
  // balanced-max profile too, unless --codex-plan says otherwise.
1315
1375
  if (!CODEX_PLAN && PLAN === 'balanced-max') CODEX_PLAN = 'balanced-max';
1376
+ askContext();
1316
1377
  if (!CODEX_PLAN) {
1317
1378
  const live = detectPlan('codex');
1318
1379
  const detected = live.raw || '', det = live.plan || '', why = live.from || '';
1319
1380
  if (interactive()) {
1320
- const proposed = prev || det;
1321
- if (proposed) {
1322
- const w = prev ? 'recorded by the previous install' : `detected from ${why}`;
1323
- CODEX_PLAN = ask(`codex plan: ${proposed} (${w}) — Enter to confirm, or type plus/pro/balanced-max: `) || proposed;
1324
- } else {
1325
- CODEX_PLAN = ask(`Could not detect the ChatGPT plan (chatgpt_plan_type='${detected}'). Enter plan [plus/pro]: `);
1326
- }
1381
+ CODEX_PLAN = pickCodexPlan(prev, det, why, detected);
1327
1382
  } else if (det) {
1328
1383
  CODEX_PLAN = det; err(`detected codex plan: ${CODEX_PLAN} (${why})`);
1329
1384
  } else if (prev) {
@@ -1390,10 +1445,9 @@ function codexDryRun() {
1390
1445
  out(` hooks: ${codexHookFiles().map((f) => path.basename(f)).join(',')}`);
1391
1446
  const plugin = codexPlugin(CODEX_DIR);
1392
1447
  skillsLine(plugin);
1393
- out(' retire: skills/status (now skills/rr-status), into backups/<date>/ if present');
1448
+ out(' retire: skills/{status,ai-status,sdlc-intent,sdlc-plan,sdlc-spec}, into backups/<date>/ if present');
1394
1449
  shimLine('hooks.json');
1395
1450
  pluginLine(plugin, true);
1396
- homeLine(CODEX_DIR);
1397
1451
  sharedLine();
1398
1452
  out(' config: ai-git-guard.json (only if absent)');
1399
1453
  out(' routing: riskrails/routing.md');
@@ -1404,7 +1458,6 @@ function codexDryRun() {
1404
1458
  }
1405
1459
 
1406
1460
  function codexApply() {
1407
- moveHome(CODEX_DIR);
1408
1461
  for (const d of ['agents', 'hooks/lib', 'hooks/guard', 'skills']) mkdirp(path.join(CODEX_DIR, d));
1409
1462
  sweepBak(CODEX_DIR, 'hooks.json', 'config.toml', 'AGENTS.md');
1410
1463
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "_comment": "Shipped defaults for ai-path-guard. Do not edit this file; a project overrides it in .ai/policies/path-guard.json (patterns are unioned, allow wins over every other list). Patterns are POSIX extended regular expressions matched against the absolute path. deny_patterns are secrets: refused for read and write. protected_config_patterns are always on and refuse writes: the task's state and journal, task.md, the compact record (.ai/memory/, .ai/local/memory/), and the guards' own configuration, engine and state.py under .claude/ or .codex/, in a project or in the user's home, and the riskrails plugin's cache folder, which carries the same code on the plugin route. Also the installer's statusline record (.claude/riskrails/statusline.json), which keeps the statusline command it replaced. A project's dependency_instruction_patterns, from before 3.4, are read as deny_patterns. task_protected_patterns are on only while a task is in flight (.ai/state/state.json, or else current.json, exists and has not reached \"done\"): the runtime's settings, Claude's record of installed plugins and the pipeline's policies, so a run cannot change the rules it is being judged by. Reading anything but a secret is allowed.",
2
+ "_comment": "Shipped defaults for ai-path-guard. Do not edit this file; a project overrides it in .ai/policies/path-guard.json (patterns are unioned, allow wins over every other list). Patterns are POSIX extended regular expressions matched against the absolute path. deny_patterns are secrets: refused for read and write. protected_config_patterns are always on and refuse writes: the task's state and journal, task.md, the compact record (.ai/memory/, .ai/local/memory/), and the guards' own configuration, engine and state.py under .claude/ or .codex/, in a project or in the user's home, and the riskrails plugin's cache folder, which carries the same code on the plugin route. Also the installer's statusline record (.claude/riskrails/statusline.json), which keeps the statusline command it replaced. task_protected_patterns are on only while a task is in flight (.ai/state/state.json, or else current.json, exists and has not reached \"done\"): the runtime's settings, Claude's record of installed plugins and the pipeline's policies, so a run cannot change the rules it is being judged by. Reading anything but a secret is allowed.",
3
3
  "deny_patterns": [
4
4
  "(^|/)\\.env$",
5
5
  "(^|/)\\.env\\.(local|prod|production|staging|live)$",
@@ -284,7 +284,7 @@ def prompt_text(message):
284
284
  text = REMINDER.sub("", content).strip()
285
285
  # only a wrapper at the very start is a slash command: a prompt may quote these tags
286
286
  command = COMMAND.search(text) if text.startswith(("<command-name>", "<command-message>")) else None
287
- if command: # "/ai-task fix the export" is an instruction; a bare "/model" is not
287
+ if command: # "/rr-build fix the export" is an instruction; a bare "/model" is not
288
288
  name, args = command.group(1).strip(), (COMMAND_ARGS.search(text) or [None, ""])[1].strip()
289
289
  return f"{name} {args}" if args else None
290
290
  # local command output, caveats, interruptions
@@ -360,7 +360,7 @@ def ai_root(cwd):
360
360
  """The project the session is in, or None.
361
361
 
362
362
  The walk stops at the repository it starts in, and never leaves $HOME. An
363
- unbounded walk would let one `/ai-init` in a home directory adopt every
363
+ unbounded walk would let one `/rr-setup` in a home directory adopt every
364
364
  session on the machine: a nested, unrelated checkout would have its prompts
365
365
  recorded into the ancestor's session.json and be handed the ancestor's
366
366
  handoff at startup. cwd falls back to the process's own, which is where the
@@ -390,8 +390,6 @@ def task_in_flight(root):
390
390
  if not root:
391
391
  return False
392
392
  state = os.path.join(root, ".ai", "state", "state.json")
393
- if not os.path.isfile(state):
394
- state = os.path.join(root, ".ai", "state", "current.json") # R34: legacy name, one version
395
393
  try:
396
394
  with open(state, encoding="utf-8") as fh:
397
395
  return (json.load(fh) or {}).get("current_stage") != "done"
@@ -216,12 +216,16 @@ def validate_label(label):
216
216
  return None
217
217
  if state_label_ok(tokens):
218
218
  return None
219
- return "a label starts with git commit, git push, a human-only state.py command or npx riskrails@<v>"
219
+ return ("a label starts with git commit, git push, a human-only state.py command (approve, reject, "
220
+ "profile set, verify/review/start --by, memory enable) or npx riskrails@<v>")
220
221
 
221
222
 
222
223
  def state_label_ok(tokens):
223
224
  """python3 [-I|-S…] <…/state.py> approve|reject|profile set|verify --by|
224
- review --by|start --mode … --by."""
225
+ review --by|start --mode … --by|memory enable [--author S] [--period month|quarter|year]
226
+ [--index none|sqlite]: each option at most once, as two words, in any order, and no other
227
+ memory verb. rr_state/labels.py keeps the same rule; tests/test_dialog_contract.py pins the
228
+ two equal."""
225
229
  if not tokens or not INTERPRETER_RE.match(tokens[0]):
226
230
  return False
227
231
  i = 1
@@ -236,6 +240,14 @@ def state_label_ok(tokens):
236
240
  return True
237
241
  if rest[:1] in (["verify"], ["review"]):
238
242
  return "--by" in rest
243
+ if rest[:2] == ["memory", "enable"]:
244
+ # The pick is the exact command the human read: no `--opt=value`, no abbreviation argparse would
245
+ # take, no positional, an option once, a value of its own kind (an author never starts with '-').
246
+ names, values = rest[2::2], rest[3::2]
247
+ kinds = {"--author": None, "--period": ("month", "quarter", "year"), "--index": ("none", "sqlite")}
248
+ return (len(names) == len(values) == len(set(names)) and set(names) <= set(kinds)
249
+ and all((v[:1] not in ("", "-")) if kinds[n] is None else v in kinds[n]
250
+ for n, v in zip(names, values)))
239
251
  return rest[:1] == ["start"] and "--mode" in rest and "--by" in rest
240
252
 
241
253
 
@@ -672,9 +672,8 @@ class PathGuard: # pylint: disable=too-many-instance-
672
672
  # -------------------------------------------------------------- the task
673
673
 
674
674
  def _state_file(self):
675
- """R34: state.json, or the legacy current.json for one version."""
676
- new = self.root + "/.ai/state/state.json"
677
- return new if is_file(new) else self.root + "/.ai/state/current.json"
675
+ """The task's state file."""
676
+ return self.root + "/.ai/state/state.json"
678
677
 
679
678
  def _task_line(self):
680
679
  loaded = load_json_file(self._state_file())
@@ -720,9 +719,7 @@ class PathGuard: # pylint: disable=too-many-instance-
720
719
  if doc is not None and not isinstance(doc, dict):
721
720
  continue
722
721
  for tag, key in (("d", "deny_patterns"), ("a", "allow_patterns"),
723
- ("p", "protected_config_patterns"), ("k", "task_protected_patterns"),
724
- # A project's list from before 3.4 keeps its read and write deny.
725
- ("d", "dependency_instruction_patterns")):
722
+ ("p", "protected_config_patterns"), ("k", "task_protected_patterns")):
726
723
  v = doc.get(key) if doc else None
727
724
  for item in jq_iter(v if v is not None and v is not False else []):
728
725
  if is_jq_string(item) and item != "":
@@ -3,7 +3,7 @@ MultiEdit, NotebookEdit and Codex's apply_patch.
3
3
 
4
4
  During the implementation stage the approved plan names the files a step may
5
5
  touch; an edit outside them is refused with SCOPE_CHANGE_REQUIRED. Inert unless
6
- .ai/state/state.json (or, for one version, the legacy current.json) exists and
6
+ .ai/state/state.json exists and
7
7
  its stage is "implementation".
8
8
 
9
9
  Bash semantics kept: a plan glob is matched as `[[ $rel == $pattern ]]` — "*"
@@ -334,8 +334,6 @@ def check(ctx):
334
334
  if root is None:
335
335
  return
336
336
  state = root + "/.ai/state/state.json"
337
- if not is_file(state):
338
- state = root + "/.ai/state/current.json"
339
337
  if not is_file(state):
340
338
  return
341
339
  step_id, task_id, allowed, forbidden, reason = read_state(state)