riskrails 3.0.0 → 3.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 +2 -2
- package/README.md +259 -812
- package/agents/ai-reviewer.md.tmpl +6 -4
- 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 +34 -32
- package/ai/using-riskrails.md +15 -0
- package/bin/claude-1m +15 -14
- package/bin/riskrails.js +113 -37
- package/codex/hooks.json +25 -2
- package/hooks/AGENTS.md +2 -2
- package/hooks/CLAUDE.md +2 -2
- package/hooks/ai-guard.py +29 -8
- package/hooks/ai-path-guard-defaults.json +3 -1
- package/hooks/codex-hooks.json +25 -2
- package/hooks/context-guard.py +252 -36
- package/hooks/dialog-gate.py +105 -0
- package/hooks/guard/common.py +7 -0
- package/hooks/guard/decision.py +123 -0
- package/hooks/guard/dialog.py +598 -0
- package/hooks/guard/git.py +49 -3
- package/hooks/guard/paths.py +89 -19
- package/hooks/hooks.json +37 -2
- package/hooks/memory-pre-push.py +28 -0
- package/hooks/project-scaffold.py +1 -5
- package/hooks/runtime-gate.py +65 -18
- package/hooks/update_notice.py +141 -0
- package/instructions/routing.md +4 -4
- package/instructions/stub.md +3 -3
- package/package.json +1 -1
- package/profiles/max.json +3 -1
- package/profiles/pro.json +3 -1
- package/scripts/merge-codex-config.py +134 -0
- package/scripts/resolve-profile.py +15 -3
- package/settings.common.json +39 -2
- package/skills/AGENTS.md +3 -3
- package/skills/CLAUDE.md +3 -3
- package/skills/ai-init/templates/.ai/AGENTS.md +3 -3
- package/skills/ai-init/templates/.ai/VERSION +1 -1
- package/skills/ai-init/templates/.ai/policies/dialog.json +1 -0
- package/skills/ai-init/templates/.ai/policies/safety.md +22 -2
- 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/policies/weight.json +1 -0
- package/skills/ai-init/templates/.ai/policies/weight.md +2 -2
- package/skills/ai-init/templates/.ai/state/README.md +9 -6
- 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/memory_compact.py +968 -0
- package/skills/ai-task/memory_format.py +775 -0
- package/skills/ai-task/memory_store.py +528 -0
- package/skills/ai-task/memory_sync.py +1571 -0
- package/skills/ai-task/sensors.py +25 -5
- package/skills/ai-task/state.py +999 -79
- package/skills/ai-task/weight.py +3 -3
- package/skills/project-init/templates/constitution.md +1 -1
- package/skills/project-init/templates/sdlc-README.md +20 -14
- 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/164cd34c182de88c33de7df35beb29200d792419e1c13a5f0b2104f944e57be7 +1 -0
- package/skills/project-update/history/blobs/18c7a2753ce9cb6f6d3d1e9d5d1125d75950b879d56b7325225b200869631b29 +15 -0
- package/skills/project-update/history/blobs/1d1d907449ec74594d0ee923ac142ae746b9aedf0a1eda9469f075a44d23e3d7 +13 -0
- package/skills/project-update/history/blobs/23a42febbacaa7b52a802d428033a99c0694391c81d51fe3b6e1dff608a641d5 +67 -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/5353ca529c46f2443e12947ec205a6887fb594a4de1f87f0eb1c0e6c0f826133 +49 -0
- package/skills/project-update/history/blobs/5a93f78b4ed4d416c171139b2ce610a117f9c1dd46fbb56ca86729cb4c53d10c +6 -0
- package/skills/project-update/history/blobs/5f65bc7ab3fc3c4e75ae05d49eaf4331fafc398c661abe33c69f7380a720344c +32 -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/7be527cd648b27cca6e267de30f8e33a2ce17d5631a82694f23ae5c0623efe1f +15 -0
- package/skills/project-update/history/blobs/7ee99edafe7cec45ff577376c0d0178fdb2690134d4656488f46e129aba3cc5a +67 -0
- package/skills/project-update/history/blobs/7f063e79195a5c85c37b7260dd20c184d3b9bea85e37202e0bbca73ad84bba49 +13 -0
- package/skills/project-update/history/blobs/8b3ef9586491a426767c01da5d01bdd6b0655c576c68c4cc6dfb093367817dc0 +67 -0
- package/skills/project-update/history/blobs/917df3320d778ddbaa5c5c7742bc4046bf803c36ed2b050f30844ed206783469 +1 -0
- package/skills/project-update/history/blobs/935af1019931058ec3a0b834fe230e127ec914f75edcc4a6c589cfe733b0b979 +13 -0
- package/skills/project-update/history/blobs/ae5ce131d6bae401199ff32d8c3dc4d8aec188010a9111624ac64506e2116ff4 +84 -0
- package/skills/project-update/history/blobs/c8573ca1f810ce6c4ac49456a4c1725b93e0d3b74c54ce95d46d8747777895f0 +13 -0
- package/skills/project-update/history/blobs/ca4499d829268c8766fb3b825a515e88dc55c237e9d202816b50959b4dac031f +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/blobs/e9adb7a9905eb719ac78ab96671f055da53e5001810f5ad6d6c94306b0323fe7 +74 -0
- package/skills/project-update/history/blobs/fae3064e648cdc55be4aeb1177525b302a5e34ebd752e4a904f47dc0f4557b96 +71 -0
- package/skills/project-update/history/index.json +60 -20
- package/skills/project-update/migrations/0009_state_file.py +25 -0
- package/skills/project-update/migrations/0010_dialog_switch.py +30 -0
- package/skills/project-update/migrations/0011_retire_risk_tiers.py +37 -0
- package/skills/project-update/migrations/README.md +2 -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 +385 -89
- package/skills/rr-build/SKILL.md +14 -5
- package/skills/rr-build-all/SKILL.md +190 -0
- package/skills/rr-plan/SKILL.md +16 -4
- package/skills/rr-review/SKILL.md +24 -9
- package/skills/rr-setup/SKILL.md +15 -2
- package/skills/rr-setup/setup.py +7 -1
- package/skills/rr-ship/SKILL.md +16 -8
- package/skills/rr-spec/SKILL.md +4 -2
- package/skills/rr-status/SKILL.md +2 -2
- package/skills/usage-report/usage-report.py +45 -10
- package/skills/ai-init/templates/.ai/policies/risk-tiers.json +0 -628
- package/skills/ai-init/templates/.ai/policies/risk-tiers.md +0 -180
package/bin/riskrails.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// RiskRails installer (Claude Code and/or Codex). Node 18+, stdlib only.
|
|
3
3
|
// npx riskrails [--target auto|claude|codex|both] [--plan pro|team-pro|team-max|max|max20|balanced-max]
|
|
4
4
|
// [--fable auto|yes|no] [--codex-plan plus|pro|balanced-max] [--dry-run]
|
|
5
|
+
// [--context default|economy] [--statusline-context yes|no]
|
|
5
6
|
// [--home DIR] [--codex-home DIR] [--yes] [--version]
|
|
6
7
|
// --target defaults to auto: each runtime is installed only if its executable is on PATH.
|
|
7
8
|
// --plan Claude only: pro, team-pro (Team Standard seat), team-max (Team Premium seat), max
|
|
@@ -16,6 +17,14 @@
|
|
|
16
17
|
// --fable Claude only. auto = yes on max, max20 and team-max, no on pro and team-pro.
|
|
17
18
|
// --codex-plan Codex only: plus, pro or balanced-max. Defaults to auto-detect from the ChatGPT
|
|
18
19
|
// login in ~/.codex/auth.json (chatgpt_plan_type); assumes pro when it cannot tell.
|
|
20
|
+
// --context default leaves each runtime its own context window and compaction, and
|
|
21
|
+
// context-guard only warns. economy writes the plan's autoCompactWindow (800 000 on
|
|
22
|
+
// Max, 300 000 on Pro) and the guard also holds a prompt back past 120% of it.
|
|
23
|
+
// Without economy, an autoCompactWindow equal to the plan's is removed; any other
|
|
24
|
+
// value is the user's and stays. Recorded in riskrails/profile.json as `context`.
|
|
25
|
+
// --statusline-context yes (default) shows the model and the context use in the
|
|
26
|
+
// statusline: a line before the user's own in Claude Code, `model-with-reasoning`
|
|
27
|
+
// and `context-used` in Codex's [tui] status_line. no adds nothing and removes nothing.
|
|
19
28
|
// --home / --codex-home the runtime homes (default ~/.claude and ~/.codex; the environment
|
|
20
29
|
// variables CLAUDE_DIR and CODEX_DIR mean the same). Naming exactly one of them under
|
|
21
30
|
// --target auto installs that runtime alone.
|
|
@@ -47,6 +56,7 @@ let CODEX_DIR = env.CODEX_DIR || path.join(os.homedir(), '.codex');
|
|
|
47
56
|
const HOME = env.HOME || os.homedir();
|
|
48
57
|
let PLAN = '', FABLE = 'auto', DRY = false, TARGET = 'auto', PLAN_GIVEN = false;
|
|
49
58
|
let CODEX_PLAN = '', CODEX_PLAN_GIVEN = false, YES = false;
|
|
59
|
+
let CONTEXT = 'default', STATUSLINE_CONTEXT = 'yes';
|
|
50
60
|
|
|
51
61
|
function version() {
|
|
52
62
|
try { return JSON.parse(fs.readFileSync(path.join(SRC, 'package.json'), 'utf8')).version || 'unknown'; }
|
|
@@ -76,6 +86,8 @@ function usage() {
|
|
|
76
86
|
case '--plan': PLAN = take(); PLAN_GIVEN = true; break;
|
|
77
87
|
case '--fable': FABLE = take(); PLAN_GIVEN = true; break;
|
|
78
88
|
case '--codex-plan': CODEX_PLAN = take(); CODEX_PLAN_GIVEN = true; break;
|
|
89
|
+
case '--context': CONTEXT = take(); break;
|
|
90
|
+
case '--statusline-context': STATUSLINE_CONTEXT = take(); break;
|
|
79
91
|
case '--home': CLAUDE_DIR = path.resolve(take()); CLAUDE_DIR_GIVEN = true; break;
|
|
80
92
|
case '--codex-home': CODEX_DIR = path.resolve(take()); CODEX_DIR_GIVEN = true; break;
|
|
81
93
|
case '--dry-run': DRY = true; break;
|
|
@@ -89,6 +101,10 @@ function usage() {
|
|
|
89
101
|
if (!['auto', 'claude', 'codex', 'both'].includes(TARGET)) {
|
|
90
102
|
die(`--target must be auto|claude|codex|both (got '${TARGET}')`, 2);
|
|
91
103
|
}
|
|
104
|
+
if (!['default', 'economy'].includes(CONTEXT)) die(`--context must be default|economy (got '${CONTEXT}')`, 2);
|
|
105
|
+
if (!['yes', 'no'].includes(STATUSLINE_CONTEXT)) {
|
|
106
|
+
die(`--statusline-context must be yes|no (got '${STATUSLINE_CONTEXT}')`, 2);
|
|
107
|
+
}
|
|
92
108
|
const major = Number(process.versions.node.split('.')[0]);
|
|
93
109
|
if (major < 18) die('riskrails needs Node 18 or newer (this is ' + process.versions.node + ')', 1);
|
|
94
110
|
|
|
@@ -281,7 +297,7 @@ let PLUGIN_VERSION = 'unknown';
|
|
|
281
297
|
try { PLUGIN_VERSION = readJSON(path.join(SRC, '.codex-plugin/plugin.json')).version || 'unknown'; } catch (e) { /* unknown */ }
|
|
282
298
|
function agenticProfile(runtime, profile, plan, label, fable) {
|
|
283
299
|
const p = JSON.parse(python([path.join(SRC, 'scripts/resolve-profile.py'), profile, '--src', SRC, '--plan', plan,
|
|
284
|
-
'--label', label, '--fable', fable, '--print', 'agentic']));
|
|
300
|
+
'--label', label, '--fable', fable, '--context', CONTEXT, '--print', 'agentic']));
|
|
285
301
|
p.runtime = runtime; p.plugin_version = PLUGIN_VERSION; p.written_at = utcStamp();
|
|
286
302
|
p.dialog = { tool: runtime === 'claude' ? 'AskUserQuestion' : codexDialogTool() };
|
|
287
303
|
p.set_by = { via: 'installer', at: p.written_at };
|
|
@@ -391,7 +407,9 @@ function claudeRender() {
|
|
|
391
407
|
|
|
392
408
|
// The profile's riskrails tables are the plugin's, not Claude Code's: the resolver
|
|
393
409
|
// prints the settings half alone, so they never reach settings.json.
|
|
394
|
-
|
|
410
|
+
// --context economy puts the plan's autoCompactWindow into that half; default leaves it out.
|
|
411
|
+
const profile = JSON.parse(python([path.join(SRC, 'scripts/resolve-profile.py'), PROFILE_NAME, '--src', SRC,
|
|
412
|
+
'--context', CONTEXT, '--print', 'settings']));
|
|
395
413
|
if (TIER === 'max' && FABLE === 'no') {
|
|
396
414
|
profile.availableModels = (profile.availableModels || []).filter((m) => !String(m).startsWith('fable'));
|
|
397
415
|
if (isObj(profile.modelSettings)) delete profile.modelSettings['claude-fable-5-1'];
|
|
@@ -404,18 +422,26 @@ function claudeRender() {
|
|
|
404
422
|
const fallbacks = Array.isArray(snippet.fallbackModel) ? snippet.fallbackModel : [snippet.fallbackModel];
|
|
405
423
|
const FALLBACK = fallbacks.join(', then ');
|
|
406
424
|
const EFFORT = String(snippet.effortLevel);
|
|
407
|
-
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
//
|
|
425
|
+
// The plan's own window (riskrails.economy), written only with --context economy; in
|
|
426
|
+
// default the runtime compacts against the model's window. Auto-compaction fires about
|
|
427
|
+
// 33k under the window; context-guard.py derives its thresholds from the same point and
|
|
428
|
+
// holds a prompt back only when a window is configured. Claude Code caps
|
|
429
|
+
// autoCompactWindow at the model's own window, so print the number that matches the
|
|
430
|
+
// session this install configures.
|
|
431
|
+
const COMPACT = CONTEXT === 'economy' ? Number(snippet.autoCompactWindow) || 0 : 0;
|
|
411
432
|
const MODEL_WINDOW = SESSION_MODEL.includes('[1m]') ? 1000000 : 200000;
|
|
412
|
-
const
|
|
413
|
-
const
|
|
414
|
-
const ONE_M_AT =
|
|
433
|
+
const fires = (window) => (COMPACT ? Math.min(COMPACT, window) : window) - 33000;
|
|
434
|
+
const COMPACT_AT = fires(MODEL_WINDOW);
|
|
435
|
+
const ONE_M_AT = fires(1000000);
|
|
436
|
+
const COMPACT_NOTE = COMPACT ? `\`autoCompactWindow\` ${COMPACT}` : 'the runtime default, `--context economy` sets a window';
|
|
415
437
|
const ONE_M_FABLE = FABLE === 'yes' ? ' and `claude-1m fable` for Fable 5.1 [1m]' : '';
|
|
416
|
-
let ONE_M_RULE =
|
|
438
|
+
let ONE_M_RULE = COMPACT
|
|
439
|
+
? `One \`autoCompactWindow\` of ${COMPACT} serves every model: Claude Code caps it at the model's own window, so this session compacts near ${COMPACT_AT} and a \`[1m]\` one near ${ONE_M_AT}, whether it was started with \`claude-1m\` (\`~/.claude/bin/claude-1m\`) for \`opus[1m]\`${ONE_M_FABLE} or switched to with \`/model\`. \`claude-1m\` pins the model at launch and \`CLAUDE_1M_COMPACT_WINDOW\` lowers the window for that one process; it is no longer what grants the large window. When a task inside an ordinary session needs one large-context read, say so and, once the user agrees, run \`claude-1m -p '<brief>'\` from Bash — its own process, only the answer comes back.`
|
|
440
|
+
: `Compaction is at the runtime default: each model compacts against its own window, so this session near ${COMPACT_AT} and a \`[1m]\` one near ${ONE_M_AT}, whether it was started with \`claude-1m\` (\`~/.claude/bin/claude-1m\`) for \`opus[1m]\`${ONE_M_FABLE} or switched to with \`/model\`. \`claude-1m\` pins the model at launch and \`CLAUDE_1M_COMPACT_WINDOW\` sets an earlier window for that one process. When a task inside an ordinary session needs one large-context read, say so and, once the user agrees, run \`claude-1m -p '<brief>'\` from Bash — its own process, only the answer comes back.`;
|
|
417
441
|
if (MODEL_WINDOW > 200000) {
|
|
418
|
-
ONE_M_RULE =
|
|
442
|
+
ONE_M_RULE = COMPACT
|
|
443
|
+
? `\`autoCompactWindow\` ${COMPACT} applies uncapped to this 1M session, so it compacts near ${COMPACT_AT}; a 200k model picked with \`/model opus\` is capped at its own window and compacts near 167000. For a small task a 200k session is cheaper per turn — say so and let the user switch. \`claude-1m\` (\`~/.claude/bin/claude-1m\`) still pins the model at launch${ONE_M_FABLE}, and \`CLAUDE_1M_COMPACT_WINDOW\` lowers the window for that one process.`
|
|
444
|
+
: `Compaction is at the runtime default, so this 1M session compacts near ${COMPACT_AT} and context-guard only warns; a 200k model picked with \`/model opus\` compacts near 167000. For a small task a 200k session is cheaper per turn — say so and let the user switch. \`claude-1m\` (\`~/.claude/bin/claude-1m\`) still pins the model at launch${ONE_M_FABLE}, and \`CLAUDE_1M_COMPACT_WINDOW\` sets an earlier window for that one process.`;
|
|
419
445
|
}
|
|
420
446
|
const READ_LINES = String(snippet.env.CLAUDE_READ_MAX_LINES);
|
|
421
447
|
const READ_BYTES = String(snippet.env.CLAUDE_READ_MAX_BYTES);
|
|
@@ -442,17 +468,17 @@ function claudeRender() {
|
|
|
442
468
|
ARCHITECT_MODEL_LINE = 'model: fable[1m]';
|
|
443
469
|
ARCHITECT_EFFORT = 'xhigh';
|
|
444
470
|
EXPERT_ROW = '`fable[1m]` / `xhigh`, pinned on `architect`';
|
|
445
|
-
PLAN_SPECIFIC = `- Session model is ${SESSION_HUMAN} by default, compaction near ${COMPACT_AT} tokens (
|
|
471
|
+
PLAN_SPECIFIC = `- Session model is ${SESSION_HUMAN} by default, compaction near ${COMPACT_AT} tokens (${COMPACT_NOTE}). A large context is not free: every turn re-reads it, so keep \`/clear\` between tasks. ${ONE_M_RULE} Fable 5.1 [1m] is pinned on \`architect\` (\`model: fable[1m]\`, \`xhigh\`) and is the session only when the user starts one with \`claude-1m fable\` — never pick it for a reader or a reviewer. \`max\` stays off.
|
|
446
472
|
- \`runtime-gate\` checks Fable at run time. \`fallbackModel\` covers an overload; after a rate-limit or model-not-found failure, and while the weekly limit is ${WEEKLY}% or more used, the gate sends every \`model: fable\` agent to Opus until the reset, and says so in the agent's context. If a Fable agent still returns such an error, re-run the same brief once with \`model: opus\` — an availability switch, not a downgrade. \`~/.claude/hooks/runtime-gate.py status\` shows the gate; \`clear\` re-enables Fable early.`;
|
|
447
473
|
} else {
|
|
448
474
|
ARCHITECT_MODEL_LINE = `# model: intentionally omitted — inherits the session model (${SESSION_HUMAN}, Fable disabled in this install)`;
|
|
449
475
|
ARCHITECT_EFFORT = 'high';
|
|
450
476
|
EXPERT_ROW = `the session model at \`${ARCHITECT_EFFORT}\` — \`architect\` inherits it`;
|
|
451
|
-
PLAN_SPECIFIC = `- Session model is ${SESSION_HUMAN} by default, compaction near ${COMPACT_AT} tokens (
|
|
477
|
+
PLAN_SPECIFIC = `- Session model is ${SESSION_HUMAN} by default, compaction near ${COMPACT_AT} tokens (${COMPACT_NOTE}). ${ONE_M_RULE} Fable is disabled in this install (\`--fable no\`). Do not request \`model: fable\` anywhere. \`xhigh\` and \`max\` stay off.`;
|
|
452
478
|
}
|
|
453
479
|
if (PLAN === 'balanced-max') {
|
|
454
480
|
ARCHITECT_MODEL_LINE = '# model: intentionally omitted — inherits the session model (Opus 5.5; no Fable on BalancedMax)';
|
|
455
|
-
PLAN_SPECIFIC = `- BalancedMax: only the session runs the strong model. Session model is Opus 5.5 with the 200k window and compaction near ${COMPACT_AT} tokens (
|
|
481
|
+
PLAN_SPECIFIC = `- BalancedMax: only the session runs the strong model. Session model is Opus 5.5 with the 200k window and compaction near ${COMPACT_AT} tokens (${COMPACT_NOTE}); it plans and implements. Agents keep their cheap tiers — readers (\`Explore\`, \`log-reader\`) on \`${tier('FAST', 'model')}\` / \`${tier('FAST', 'effort')}\`, judgement (\`ai-discovery\`) on \`${tier('BALANCED', 'model')}\`, \`ai-reviewer\` on \`${tier('STRONG', 'model')}\`, and \`architect\` inherits the session. \`opus[1m]\` is a per-task choice, never the default. ${ONE_M_RULE} Fable is off on this plan; never request \`model: fable\`. \`xhigh\` and \`max\` stay off.`;
|
|
456
482
|
}
|
|
457
483
|
EFFORT_RULE = 'Raise to `high` for architecture, root-cause analysis and adversarial verification, and say that you are raising it; readers stay at `low`.';
|
|
458
484
|
}
|
|
@@ -467,7 +493,10 @@ function claudeRender() {
|
|
|
467
493
|
const vars = {
|
|
468
494
|
PLAN: PLAN_LABEL, PLAN_LABEL, SESSION_MODEL: SESSION_HUMAN, FALLBACK_MODEL: FALLBACK_HUMAN,
|
|
469
495
|
DEFAULT_EFFORT: EFFORT, COMPACT_WINDOW: spaced(COMPACT_AT),
|
|
470
|
-
CONTEXT_WARN: `${Math.trunc(COMPACT_AT * 80 / 100000)}k`,
|
|
496
|
+
CONTEXT_WARN: `${Math.trunc(COMPACT_AT * 80 / 100000)}k`,
|
|
497
|
+
CONTEXT_BLOCK: COMPACT
|
|
498
|
+
? `and from ${Math.trunc(COMPACT_AT * 120 / 100000)}k the prompt is held back once — sending it again goes through`
|
|
499
|
+
: 'and the prompt is never held back (only `--context economy` sets a block threshold)',
|
|
471
500
|
READ_LINES, PLAN_SPECIFIC_ROUTING: PLAN_SPECIFIC, EFFORT_RULE, EXPERT_EFFORT, EXPERT_MODEL_LINE, EXPERT_ROW,
|
|
472
501
|
ARCHITECT_MODEL_LINE, ARCHITECT_EFFORT,
|
|
473
502
|
FAST_MODEL: tier('FAST', 'model'), FAST_EFFORT: tier('FAST', 'effort'),
|
|
@@ -524,8 +553,11 @@ function shSplit(s) { // shlex.split,
|
|
|
524
553
|
return words;
|
|
525
554
|
}
|
|
526
555
|
|
|
556
|
+
// The wrapper is `<PREFIX> [--context] [--then '<the user's command>']`; --context
|
|
557
|
+
// (--statusline-context yes) adds a line with the model and the context use.
|
|
527
558
|
function statuslineGate(file, gate, mode) {
|
|
528
559
|
const PREFIX = '"$HOME/.claude/hooks/runtime-gate.py" statusline';
|
|
560
|
+
const WANT = STATUSLINE_CONTEXT === 'yes' ? `${PREFIX} --context` : PREFIX;
|
|
529
561
|
const OLD_PREFIX = '"$HOME/.claude/hooks/fable-gate.py" statusline';
|
|
530
562
|
let settings = {};
|
|
531
563
|
if (exists(file)) {
|
|
@@ -538,23 +570,35 @@ function statuslineGate(file, gate, mode) {
|
|
|
538
570
|
line.command = PREFIX + cmd.slice(OLD_PREFIX.length);
|
|
539
571
|
ours = true; gate = 'moved';
|
|
540
572
|
}
|
|
573
|
+
// What follows the prefix and an optional --context: the user's --then, byte for byte.
|
|
574
|
+
const tail = () => line.command.slice(PREFIX.length).trim().replace(/^--context(\s+|$)/, '');
|
|
575
|
+
const shown = STATUSLINE_CONTEXT === 'yes' ? ' with the model and the context use' : '';
|
|
541
576
|
let action = null;
|
|
542
577
|
if (gate === 'moved') {
|
|
578
|
+
const rest = tail();
|
|
579
|
+
line.command = rest ? `${WANT} ${rest}` : WANT;
|
|
543
580
|
action = 'moved the fable-gate statusline wrapper to runtime-gate (your command is unchanged)';
|
|
544
581
|
} else if (gate === 'on') {
|
|
545
|
-
if (ours)
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
582
|
+
if (ours) {
|
|
583
|
+
const rest = tail();
|
|
584
|
+
const want = rest ? `${WANT} ${rest}` : WANT;
|
|
585
|
+
if (want === cmd) return ['statusline: already records the quota'];
|
|
586
|
+
line.command = want;
|
|
587
|
+
action = STATUSLINE_CONTEXT === 'yes'
|
|
588
|
+
? 'added the model and the context use to the statusline wrapper (your command is unchanged)'
|
|
589
|
+
: 'removed the model and context line from the statusline wrapper (your command is unchanged)';
|
|
590
|
+
} else if (line === undefined || line === null) {
|
|
591
|
+
settings.statusLine = { type: 'command', command: WANT };
|
|
592
|
+
action = `added a statusline that records the quota (and the Fable weekly limit)${shown}`;
|
|
549
593
|
} else if (isObj(line) && line.type === 'command' && typeof cmd === 'string' && cmd.trim()) {
|
|
550
|
-
line.command = `${
|
|
551
|
-
action =
|
|
594
|
+
line.command = `${WANT} --then ${shQuote(cmd)}`;
|
|
595
|
+
action = `wrapped your statusline command with the quota check${shown} (its output is unchanged)`;
|
|
552
596
|
} else {
|
|
553
597
|
return ['statusline: not a command statusline; the weekly-limit check is not wired'];
|
|
554
598
|
}
|
|
555
599
|
} else {
|
|
556
600
|
if (!ours) return [];
|
|
557
|
-
const rest =
|
|
601
|
+
const rest = tail();
|
|
558
602
|
if (rest.startsWith('--then')) {
|
|
559
603
|
const parts = shSplit(rest);
|
|
560
604
|
line.command = parts.length > 1 ? parts[1] : '';
|
|
@@ -579,12 +623,26 @@ const claudeHookFiles = () => [...glob(path.join(SRC, 'hooks'), '.sh'), ...glob(
|
|
|
579
623
|
.filter((f) => !path.basename(f).startsWith('codex-'));
|
|
580
624
|
const skillsLine = (plugin) => out(` skills: ${ls(path.join(SRC, 'skills')).filter((n) => isDir(path.join(SRC, 'skills', n))).join(',')}${plugin ? ' (without SKILL.md: the plugin loads them)' : ''}`);
|
|
581
625
|
|
|
626
|
+
// Without --context economy, an autoCompactWindow equal to a shipped plan's economy window
|
|
627
|
+
// (800000 Max, 300000 Pro — the one an earlier install wrote, whichever plan it was for)
|
|
628
|
+
// goes; any other value is the user's own and stays. Returns the value to remove, or 0.
|
|
629
|
+
function ownCompactWindow(doc) {
|
|
630
|
+
if (CONTEXT === 'economy' || !isObj(doc)) return 0;
|
|
631
|
+
const shipped = glob(path.join(SRC, 'profiles'), '.json').map((f) => {
|
|
632
|
+
try { return Number(((readJSON(f).riskrails || {}).economy || {}).autoCompactWindow) || 0; } catch (e) { return 0; }
|
|
633
|
+
});
|
|
634
|
+
return shipped.includes(doc.autoCompactWindow) ? doc.autoCompactWindow : 0;
|
|
635
|
+
}
|
|
636
|
+
|
|
582
637
|
function claudeDryRun() {
|
|
583
638
|
out(`== claude: plan=${PLAN} (${C.PLAN_LABEL}, ${C.TIER} profile) fable=${FABLE} fable-gate=${C.GATE} runtime-gate=on (dry run, nothing written)`);
|
|
584
639
|
out(`== claude: target directory ${CLAUDE_DIR}`);
|
|
585
640
|
for (const l of statuslineGate(path.join(CLAUDE_DIR, 'settings.json'), 'on', 'dry')) out('== ' + l);
|
|
586
641
|
out(`== settings snippet (merged into ${CLAUDE_DIR}/settings.json):`);
|
|
587
642
|
out(pretty(C.snippet));
|
|
643
|
+
let current = {};
|
|
644
|
+
try { current = readJSON(path.join(CLAUDE_DIR, 'settings.json')); } catch (e) { /* nothing installed yet */ }
|
|
645
|
+
if (ownCompactWindow(current)) out(`== would remove autoCompactWindow ${current.autoCompactWindow} (economy off)`);
|
|
588
646
|
out('== agents/architect.md (rendered head):');
|
|
589
647
|
out(read(path.join(TMP, 'agents/architect.md')).split('\n').slice(0, 10).join('\n'));
|
|
590
648
|
out('== CLAUDE.md managed block:');
|
|
@@ -1045,6 +1103,11 @@ function claudeApply() {
|
|
|
1045
1103
|
if (dropPluginDuplicates(doc, keys)) out('removed: the hook entries the riskrails plugin now runs');
|
|
1046
1104
|
if (isObj(doc.env) && doc.env.CLAUDE_CODE_SUBAGENT_MODEL === 'sonnet') delete doc.env.CLAUDE_CODE_SUBAGENT_MODEL;
|
|
1047
1105
|
doc = mergeHooks(deepMerge(doc, snippet), newHooks, true);
|
|
1106
|
+
const own = ownCompactWindow(doc);
|
|
1107
|
+
if (own) {
|
|
1108
|
+
delete doc.autoCompactWindow;
|
|
1109
|
+
out(`removed: autoCompactWindow ${own} (economy off)`);
|
|
1110
|
+
}
|
|
1048
1111
|
writeJSONFile(SETTINGS, doc);
|
|
1049
1112
|
}
|
|
1050
1113
|
out('merged: settings.json');
|
|
@@ -1119,11 +1182,16 @@ function claudeSummary() {
|
|
|
1119
1182
|
` session model ${C.SESSION_MODEL} (${C.SESSION_HUMAN}), effort ${C.EFFORT}`,
|
|
1120
1183
|
` fallback ${C.FALLBACK}`,
|
|
1121
1184
|
` EXPERT tier architect on ${expert} at effort ${C.ARCHITECT_EFFORT}`,
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1185
|
+
...(C.COMPACT ? [
|
|
1186
|
+
` compaction near ${C.COMPACT_AT} tokens on this ${Math.trunc(C.MODEL_WINDOW / 1000)}k session (autoCompactWindow ${C.COMPACT},`,
|
|
1187
|
+
" capped at the model's window, --context economy); context-guard warns from",
|
|
1188
|
+
` ${Math.trunc(C.COMPACT_AT * 80 / 100)} and holds a prompt back once from ${Math.trunc(C.COMPACT_AT * 120 / 100)}`,
|
|
1189
|
+
] : [
|
|
1190
|
+
` compaction the runtime default: near ${C.COMPACT_AT} tokens on this ${Math.trunc(C.MODEL_WINDOW / 1000)}k session;`,
|
|
1191
|
+
` context-guard warns from ${Math.trunc(C.COMPACT_AT * 80 / 100)} and never holds a prompt back (economy only)`,
|
|
1192
|
+
]),
|
|
1125
1193
|
C.TIER === 'max'
|
|
1126
|
-
? ` large context claude-1m${FABLE === 'yes' ? ' [opus|fable]' : ''} starts one session on opus[1m]${FABLE === 'yes' ? ' or fable[1m] (Fable 5.1)' : ''} that compacts near ${C.ONE_M_AT}\n (the same autoCompactWindow, uncapped on a 1M model)`
|
|
1194
|
+
? ` large context claude-1m${FABLE === 'yes' ? ' [opus|fable]' : ''} starts one session on opus[1m]${FABLE === 'yes' ? ' or fable[1m] (Fable 5.1)' : ''} that compacts near ${C.ONE_M_AT}\n (${C.COMPACT ? 'the same autoCompactWindow, uncapped on a 1M model' : 'the runtime default'})`
|
|
1127
1195
|
: '',
|
|
1128
1196
|
` read guard an unbounded Read is refused above ${C.READ_LINES} lines / ${C.READ_BYTES} bytes`,
|
|
1129
1197
|
` agents ${agentNames(' ')}`,
|
|
@@ -1137,9 +1205,9 @@ function claudeSummary() {
|
|
|
1137
1205
|
'Restart Claude Code, then:',
|
|
1138
1206
|
' /config model and effort match the profile',
|
|
1139
1207
|
` /hooks lists cap-large-read, ai-guard, project-scaffold and context-guard, plus runtime-gate on PreToolUse, PostToolUse, SubagentStart, SubagentStop${C.GATE === 'on' ? ' and StopFailure' : ''}`,
|
|
1140
|
-
' /skills lists
|
|
1141
|
-
' /
|
|
1142
|
-
' /
|
|
1208
|
+
' /skills lists rr-setup, rr-spec, rr-plan, rr-build, rr-build-all, rr-review, rr-ship, rr-status',
|
|
1209
|
+
' /rr-setup in a project, to survey it and build .ai/',
|
|
1210
|
+
' /rr-setup --update in a project that already has .ai/ or docs/sdlc/, to pull in these rules',
|
|
1143
1211
|
];
|
|
1144
1212
|
out(lines.join('\n'));
|
|
1145
1213
|
}
|
|
@@ -1210,14 +1278,15 @@ function codexRender() {
|
|
|
1210
1278
|
}
|
|
1211
1279
|
|
|
1212
1280
|
// The guard scripts Codex can use. cap-large-read.py is absent: Codex has no hookable Read
|
|
1213
|
-
// tool. context-guard.py serves both runtimes: it reads its own location to know which
|
|
1214
|
-
|
|
1281
|
+
// tool. context-guard.py serves both runtimes: it reads its own location to know which;
|
|
1282
|
+
// dialog-gate.py reads the payload's envelope (R36.19).
|
|
1283
|
+
const codexHookFiles = () => ['ai-guard.py', 'runtime-gate.py', 'context-guard.py', 'dialog-gate.py', 'update_notice.py'].map((n) => path.join(SRC, 'hooks', n));
|
|
1215
1284
|
|
|
1216
1285
|
function codexDryRun() {
|
|
1217
1286
|
out(`== codex: plan=${CODEX_PLAN} (${X.label}) session ${X.s(X.P.session.model)} at ${X.s(X.P.session.model_reasoning_effort)} (dry run, nothing written)`);
|
|
1218
1287
|
out(`== codex: target directory ${CODEX_DIR}`);
|
|
1219
1288
|
const merged = python([path.join(SRC, 'scripts/merge-codex-config.py'), path.join(CODEX_DIR, 'config.toml'),
|
|
1220
|
-
'--profile', X.profileFile, '--dry-run'], { allowFail: true });
|
|
1289
|
+
'--profile', X.profileFile, '--status-context', STATUSLINE_CONTEXT, '--dry-run'], { allowFail: true });
|
|
1221
1290
|
for (const l of merged.replace(/\n$/, '').split('\n')) if (merged) out('== ' + l);
|
|
1222
1291
|
out('== AGENTS.md managed block:');
|
|
1223
1292
|
fs.writeSync(1, read(path.join(TMP, 'AGENTS.block.md')));
|
|
@@ -1274,7 +1343,9 @@ function codexApply() {
|
|
|
1274
1343
|
// codex-model-gate entries from before WP5 and runtime-gate ones from an earlier run go
|
|
1275
1344
|
// first, so the merge adds exactly one gate per event. Codex runs only hooks the user has
|
|
1276
1345
|
// reviewed (config.toml keeps a trusted_hash per entry): a changed entry is skipped until
|
|
1277
|
-
// it is reviewed once in /hooks.
|
|
1346
|
+
// it is reviewed once in /hooks. A group whose commands are exactly a new group's is
|
|
1347
|
+
// replaced by it, as on Claude, so a reinstall carries a widened matcher (R36 7d2); an
|
|
1348
|
+
// unchanged group is written back as it was and keeps its trust.
|
|
1278
1349
|
const HOOKS = path.join(CODEX_DIR, 'hooks.json');
|
|
1279
1350
|
if (isFile(HOOKS)) backupFile(HOOKS, CODEX_DIR); else fs.writeFileSync(HOOKS, '{}\n');
|
|
1280
1351
|
let hadShim = false, hadGuardShims = false;
|
|
@@ -1286,7 +1357,11 @@ function codexApply() {
|
|
|
1286
1357
|
dropHooks(doc, SHIM_RE);
|
|
1287
1358
|
const keys = pluginHookKeys(plugin, 'codex-hooks.json');
|
|
1288
1359
|
if (dropPluginDuplicates(doc, keys)) out('removed: the hook entries the riskrails plugin now runs');
|
|
1289
|
-
|
|
1360
|
+
const fresh = withoutPluginHooks(readJSON(path.join(SRC, 'codex/hooks.json')).hooks, keys);
|
|
1361
|
+
const changed = Object.entries(fresh).some(([ev, entries]) => ((isObj(doc.hooks) && doc.hooks[ev]) || []).some(
|
|
1362
|
+
(old) => entries.some((e) => sameList(commands(e), commands(old)) && !sameList(e, old))));
|
|
1363
|
+
writeJSONFile(HOOKS, mergeHooks(doc, fresh, true));
|
|
1364
|
+
if (changed) out('changed: riskrails hook entries in hooks.json — review them once in /hooks, or Codex skips them');
|
|
1290
1365
|
}
|
|
1291
1366
|
out('merged: hooks.json');
|
|
1292
1367
|
if (hadShim) out('replaced: codex-model-gate.py hooks with runtime-gate.py — review the changed gate entries once in /hooks, or Codex skips them');
|
|
@@ -1300,7 +1375,8 @@ function codexApply() {
|
|
|
1300
1375
|
const config = path.join(CODEX_DIR, 'config.toml'), before = path.join(TMP, 'config.toml.before');
|
|
1301
1376
|
rmrf(before);
|
|
1302
1377
|
if (exists(config) && fs.statSync(config).size > 0) fs.copyFileSync(fs.realpathSync(config), before);
|
|
1303
|
-
python([path.join(SRC, 'scripts/merge-codex-config.py'), config, '--profile', X.profileFile,
|
|
1378
|
+
python([path.join(SRC, 'scripts/merge-codex-config.py'), config, '--profile', X.profileFile,
|
|
1379
|
+
'--status-context', STATUSLINE_CONTEXT, '--no-backup'], { capture: false });
|
|
1304
1380
|
if (exists(before) && fs.statSync(before).size > 0 && !sameFile(before, config)) backupFile(config, CODEX_DIR, before);
|
|
1305
1381
|
|
|
1306
1382
|
// -------------------------------------------------------------- 6. AGENTS.md
|
|
@@ -1335,8 +1411,8 @@ function codexSummary() {
|
|
|
1335
1411
|
'',
|
|
1336
1412
|
'Restart Codex, then:',
|
|
1337
1413
|
' /hooks review and trust the guard, gate and context hooks — until you do, Codex skips them',
|
|
1338
|
-
' /skills lists
|
|
1339
|
-
' /
|
|
1414
|
+
' /skills lists rr-setup, rr-spec, rr-plan, rr-build, rr-build-all, rr-review, rr-ship, rr-status',
|
|
1415
|
+
' /rr-setup in a project, to survey it and build .ai/',
|
|
1340
1416
|
'',
|
|
1341
1417
|
`Your previous default model was replaced. backups/${BACKUP_DATE}/config.toml holds the old one.`,
|
|
1342
1418
|
].join('\n'));
|
package/codex/hooks.json
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
"hooks": {
|
|
4
4
|
"PreToolUse": [
|
|
5
5
|
{
|
|
6
|
-
"matcher": "
|
|
6
|
+
"matcher": ".*",
|
|
7
7
|
"hooks": [
|
|
8
8
|
{
|
|
9
9
|
"type": "command",
|
|
10
10
|
"command": "python3 -I -S \"$HOME/.codex/hooks/ai-guard.py\"",
|
|
11
11
|
"timeout": 15,
|
|
12
|
-
"statusMessage": "Checking git, paths and
|
|
12
|
+
"statusMessage": "Checking git, paths, scope and open decisions"
|
|
13
13
|
}
|
|
14
14
|
]
|
|
15
15
|
},
|
|
@@ -36,6 +36,17 @@
|
|
|
36
36
|
"statusMessage": "Recording expert-model availability"
|
|
37
37
|
}
|
|
38
38
|
]
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"matcher": "^request_user_input$",
|
|
42
|
+
"hooks": [
|
|
43
|
+
{
|
|
44
|
+
"type": "command",
|
|
45
|
+
"command": "python3 -I -S \"$HOME/.codex/hooks/dialog-gate.py\"",
|
|
46
|
+
"timeout": 5,
|
|
47
|
+
"statusMessage": "Recording the dialog's answer"
|
|
48
|
+
}
|
|
49
|
+
]
|
|
39
50
|
}
|
|
40
51
|
],
|
|
41
52
|
"SubagentStart": [
|
|
@@ -98,6 +109,18 @@
|
|
|
98
109
|
}
|
|
99
110
|
]
|
|
100
111
|
}
|
|
112
|
+
],
|
|
113
|
+
"Stop": [
|
|
114
|
+
{
|
|
115
|
+
"hooks": [
|
|
116
|
+
{
|
|
117
|
+
"type": "command",
|
|
118
|
+
"command": "python3 -I -S \"$HOME/.codex/hooks/dialog-gate.py\"",
|
|
119
|
+
"timeout": 5,
|
|
120
|
+
"statusMessage": "Checking for an open decision"
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
}
|
|
101
124
|
]
|
|
102
125
|
}
|
|
103
126
|
}
|
package/hooks/AGENTS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
<!--
|
|
1
|
+
<!-- riskrails:rule:hooks:start -->
|
|
2
2
|
# Hooks rules
|
|
3
3
|
- Weigh a change by what it does to a user project's decisions, never by the hook being a process every call runs through (constitution C6). The mode is what W gives (`state.py weight`); the rows below set the behaviour axis.
|
|
4
4
|
- behaviour 0: no guard decision moves, golden byte-identical. behaviour 1: adds a deny or changes a deny's text, golden re-recorded. behaviour 2: allows what was denied or weakens a refusal — declare it, `--declare behaviour=2:"<the effect>"`. Start such a task with `start --mode full --by <name>`: its weight lands in guarded, and these effects keep a STRONG review and the gate.
|
|
5
5
|
- The guards defend against an agent's mistakes, not a constructed command: such a finding is accepted in `known-risks.md` under the threat model in `.ai/policies/safety.md`.
|
|
6
|
-
<!--
|
|
6
|
+
<!-- riskrails:rule:hooks:end -->
|
package/hooks/CLAUDE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
<!--
|
|
1
|
+
<!-- riskrails:rule:hooks:start -->
|
|
2
2
|
# Hooks rules
|
|
3
3
|
- Weigh a change by what it does to a user project's decisions, never by the hook being a process every call runs through (constitution C6). The mode is what W gives (`state.py weight`); the rows below set the behaviour axis.
|
|
4
4
|
- behaviour 0: no guard decision moves, golden byte-identical. behaviour 1: adds a deny or changes a deny's text, golden re-recorded. behaviour 2: allows what was denied or weakens a refusal — declare it, `--declare behaviour=2:"<the effect>"`. Start such a task with `start --mode full --by <name>`: its weight lands in guarded, and these effects keep a STRONG review and the gate.
|
|
5
5
|
- The guards defend against an agent's mistakes, not a constructed command: such a finding is accepted in `known-risks.md` under the threat model in `.ai/policies/safety.md`.
|
|
6
|
-
<!--
|
|
6
|
+
<!-- riskrails:rule:hooks:end -->
|
package/hooks/ai-guard.py
CHANGED
|
@@ -3,8 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
Reads the hook payload from stdin and either exits 0 silently (allow) or prints
|
|
5
5
|
the deny object and exits 0, the contract of every guard. The checks are the
|
|
6
|
-
three shell guards this replaces, in the order a
|
|
6
|
+
open-decision gate, then the three shell guards this replaces, in the order a
|
|
7
|
+
runtime ran them:
|
|
7
8
|
|
|
9
|
+
decision the gate while a dialog decision is open, on every tool
|
|
10
|
+
(guard/decision.py): deny all but the dialog, the picked command
|
|
11
|
+
and `state.py decision show|settle|fallback`
|
|
8
12
|
git git and deployment safety, on Bash (guard/git.py)
|
|
9
13
|
paths sensitive paths, control files, the runtime lock, dependency
|
|
10
14
|
instruction files (guard/paths.py)
|
|
@@ -12,20 +16,24 @@ three shell guards this replaces, in the order a runtime ran them:
|
|
|
12
16
|
|
|
13
17
|
`--only <check>` runs one of them; that is what each hooks/ai-*-guard.sh shim
|
|
14
18
|
passes, so a registration made for the shell guards keeps its meaning. With no
|
|
15
|
-
flag all
|
|
19
|
+
flag all of them run and the first refusal wins.
|
|
16
20
|
|
|
17
21
|
Fail-open: anything unexpected — a payload that is not JSON, a missing file,
|
|
18
22
|
an exception — allows, with no output. A guard is defence-in-depth against an
|
|
19
23
|
agent's mistakes, not a security boundary, and must never make a tool unusable.
|
|
24
|
+
Two exceptions, both with the dialog on (R36): the git check's consent rule
|
|
25
|
+
denies a commit or push when the consent ledger cannot be read (R36.9), and
|
|
26
|
+
state.py's own approve refuses without a pick or a terminal (R36.10).
|
|
20
27
|
Run as `python3 -I -S`: no site packages, no environment-driven imports.
|
|
21
28
|
"""
|
|
22
29
|
import os
|
|
23
30
|
import sys
|
|
24
31
|
|
|
25
|
-
CHECKS = ("git", "paths", "scope")
|
|
32
|
+
CHECKS = ("decision", "git", "paths", "scope")
|
|
26
33
|
# The tools each check reads; any other tool is allowed by its first line, so
|
|
27
|
-
# the module is not even imported for it.
|
|
34
|
+
# the module is not even imported for it. None: every tool.
|
|
28
35
|
TOOLS = {
|
|
36
|
+
"decision": None,
|
|
29
37
|
"git": ("Bash",),
|
|
30
38
|
"paths": ("Read", "Edit", "Write", "MultiEdit", "NotebookEdit", "Bash"),
|
|
31
39
|
"scope": ("Edit", "Write", "MultiEdit", "NotebookEdit"),
|
|
@@ -52,16 +60,28 @@ def main(argv):
|
|
|
52
60
|
return 0
|
|
53
61
|
ctx = Context(payload, loc, pwd, common.bytes_b(os.fsencode(hook_dir)))
|
|
54
62
|
for name in (only,) if only else CHECKS:
|
|
55
|
-
if payload.tool not in TOOLS[name]:
|
|
63
|
+
if TOOLS[name] is not None and payload.tool not in TOOLS[name]:
|
|
56
64
|
continue
|
|
57
|
-
module = __import__("guard." + name, fromlist=["check"])
|
|
58
65
|
# Each shell guard was its own process with its own SECONDS: the
|
|
59
|
-
# first
|
|
60
|
-
|
|
66
|
+
# first shell guard's clock started with the process, a later one's
|
|
67
|
+
# now. The decision check, new, does not move that.
|
|
68
|
+
ctx.clock = common.Clock(common.START if name == (only or "git") else None)
|
|
61
69
|
try:
|
|
70
|
+
module = __import__("guard." + name, fromlist=["check"])
|
|
62
71
|
module.check(ctx)
|
|
63
72
|
except common.Allow:
|
|
64
73
|
continue
|
|
74
|
+
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
75
|
+
# The decision gate fails open on its own (R36.12): a missing or
|
|
76
|
+
# broken module — a half-done install — must not end the run
|
|
77
|
+
# before git, paths and scope.
|
|
78
|
+
if name != "decision" or isinstance(exc, common.Deny):
|
|
79
|
+
# A git pick the gate spent on this call is gone even
|
|
80
|
+
# though a later rule refuses the command (review focus 1).
|
|
81
|
+
if isinstance(exc, common.Deny) and name != "decision" and ctx.consent:
|
|
82
|
+
from guard import dialog # pylint: disable=import-outside-toplevel
|
|
83
|
+
exc.reason += common.text_to_b(dialog.used_note(ctx.consent)) # pylint: disable=no-member # exc is a common.Deny by the isinstance above
|
|
84
|
+
raise
|
|
65
85
|
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
66
86
|
common = sys.modules.get("guard.common")
|
|
67
87
|
if common is not None and isinstance(exc, common.Deny):
|
|
@@ -86,6 +106,7 @@ class Context: # pylint: disable=too-few-public-met
|
|
|
86
106
|
self.pwd = pwd
|
|
87
107
|
self.hook_dir = hook_dir
|
|
88
108
|
self.clock = common.Clock()
|
|
109
|
+
self.consent = None # the git pick the decision gate spent on this call
|
|
89
110
|
|
|
90
111
|
|
|
91
112
|
if __name__ == "__main__":
|
|
@@ -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, 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. 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. 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.",
|
|
3
3
|
"deny_patterns": [
|
|
4
4
|
"(^|/)\\.env$",
|
|
5
5
|
"(^|/)\\.env\\.(local|prod|production|staging|live)$",
|
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
"(^|/)\\.ai/reports/[^/]+/events\\.jsonl$",
|
|
30
30
|
"(^|/)\\.ai/reports/[^/]+/(adopt|migration)\\.json$",
|
|
31
31
|
"(^|/)\\.ai/policies/[^/]*\\.json$",
|
|
32
|
+
"(^|/)\\.ai/memory/",
|
|
33
|
+
"(^|/)\\.ai/local/memory/",
|
|
32
34
|
"(^|/)\\.claude/hooks/ai-[^/]*$",
|
|
33
35
|
"/\\.claude/hooks/ai-[^/]*$",
|
|
34
36
|
"(^|/)\\.codex/hooks/ai-[^/]*$",
|
package/hooks/codex-hooks.json
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
"hooks": {
|
|
4
4
|
"PreToolUse": [
|
|
5
5
|
{
|
|
6
|
-
"matcher": "
|
|
6
|
+
"matcher": ".*",
|
|
7
7
|
"hooks": [
|
|
8
8
|
{
|
|
9
9
|
"type": "command",
|
|
10
10
|
"command": "python3 -I -S \"${CLAUDE_PLUGIN_ROOT}/hooks/ai-guard.py\"",
|
|
11
11
|
"timeout": 15,
|
|
12
|
-
"statusMessage": "Checking git, paths and
|
|
12
|
+
"statusMessage": "Checking git, paths, scope and open decisions"
|
|
13
13
|
}
|
|
14
14
|
]
|
|
15
15
|
},
|
|
@@ -36,6 +36,17 @@
|
|
|
36
36
|
"statusMessage": "Recording expert-model availability"
|
|
37
37
|
}
|
|
38
38
|
]
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"matcher": "^request_user_input$",
|
|
42
|
+
"hooks": [
|
|
43
|
+
{
|
|
44
|
+
"type": "command",
|
|
45
|
+
"command": "python3 -I -S \"${CLAUDE_PLUGIN_ROOT}/hooks/dialog-gate.py\"",
|
|
46
|
+
"timeout": 5,
|
|
47
|
+
"statusMessage": "Recording the dialog's answer"
|
|
48
|
+
}
|
|
49
|
+
]
|
|
39
50
|
}
|
|
40
51
|
],
|
|
41
52
|
"SubagentStart": [
|
|
@@ -98,6 +109,18 @@
|
|
|
98
109
|
}
|
|
99
110
|
]
|
|
100
111
|
}
|
|
112
|
+
],
|
|
113
|
+
"Stop": [
|
|
114
|
+
{
|
|
115
|
+
"hooks": [
|
|
116
|
+
{
|
|
117
|
+
"type": "command",
|
|
118
|
+
"command": "python3 -I -S \"${CLAUDE_PLUGIN_ROOT}/hooks/dialog-gate.py\"",
|
|
119
|
+
"timeout": 5,
|
|
120
|
+
"statusMessage": "Checking for an open decision"
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
}
|
|
101
124
|
]
|
|
102
125
|
}
|
|
103
126
|
}
|