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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +50 -84
- package/agents/ai-reviewer.md.tmpl +1 -1
- package/ai/using-riskrails.md +0 -3
- package/bin/menu.js +116 -0
- package/bin/riskrails.js +132 -79
- package/hooks/ai-path-guard-defaults.json +1 -1
- package/hooks/context-guard.py +2 -4
- package/hooks/guard/dialog.py +14 -2
- 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/scaffold-ai.sh +2 -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/safety.md +4 -3
- 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 +2 -2
- package/skills/ai-task/rr_state/dialog.py +2 -4
- package/skills/ai-task/rr_state/dialog_cmd.py +183 -6
- package/skills/ai-task/rr_state/fs.py +5 -13
- package/skills/ai-task/rr_state/labels.py +14 -2
- package/skills/ai-task/rr_state/memory_cmd.py +7 -4
- 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/8ec171f0fbdf38cc6996d496f01491fd1f21f04a435a118ad979b127a8be06f1 +85 -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 +34 -14
- package/skills/project-update/render_instructions.py +12 -45
- package/skills/project-update/update.py +757 -1172
- package/skills/rr-setup/SKILL.md +114 -97
- package/skills/rr-setup/scaffold_ai.py +15 -9
- package/skills/rr-setup/setup.py +462 -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
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
|
-
|
|
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(/<!--
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
835
|
-
retire(root,
|
|
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
|
|
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}
|
|
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
|
-
|
|
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
|
|
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.
|
|
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)$",
|
package/hooks/context-guard.py
CHANGED
|
@@ -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: # "/
|
|
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 `/
|
|
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"
|
package/hooks/guard/dialog.py
CHANGED
|
@@ -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
|
|
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
|
|
package/hooks/guard/paths.py
CHANGED
|
@@ -672,9 +672,8 @@ class PathGuard: # pylint: disable=too-many-instance-
|
|
|
672
672
|
# -------------------------------------------------------------- the task
|
|
673
673
|
|
|
674
674
|
def _state_file(self):
|
|
675
|
-
"""
|
|
676
|
-
|
|
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 != "":
|
package/hooks/guard/scope.py
CHANGED
|
@@ -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
|
|
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)
|