riskrails 3.1.0 → 3.2.2
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 +12 -11
- package/bin/riskrails.js +109 -18
- package/hooks/ai-path-guard-defaults.json +3 -2
- package/hooks/context-guard.py +14 -11
- package/hooks/dialog-gate.py +1 -1
- package/hooks/guard/dialog.py +107 -27
- package/hooks/guard/git.py +12 -3
- package/hooks/guard/paths.py +109 -7
- package/hooks/project-scaffold.py +2 -1
- package/hooks/runtime-gate.py +128 -27
- package/hooks/update_notice.py +3 -3
- package/package.json +1 -1
- package/skills/ai-init/templates/.ai/state/README.md +4 -4
- package/skills/ai-task/rr_state/__init__.py +1 -0
- package/skills/ai-task/rr_state/cli.py +210 -0
- package/skills/ai-task/rr_state/close.py +171 -0
- package/skills/ai-task/rr_state/dialog.py +541 -0
- package/skills/ai-task/rr_state/dialog_cmd.py +164 -0
- package/skills/ai-task/rr_state/fs.py +220 -0
- package/skills/ai-task/rr_state/gate.py +125 -0
- package/skills/ai-task/rr_state/gate_cmd.py +128 -0
- package/skills/ai-task/rr_state/handoff.py +226 -0
- package/skills/ai-task/rr_state/journal.py +126 -0
- package/skills/ai-task/rr_state/labels.py +99 -0
- package/skills/ai-task/rr_state/ledger.py +145 -0
- package/skills/ai-task/rr_state/locks.py +84 -0
- package/skills/ai-task/rr_state/memory_cmd.py +310 -0
- package/skills/ai-task/rr_state/policy.py +378 -0
- package/skills/ai-task/rr_state/profile.py +373 -0
- package/skills/ai-task/rr_state/questions.py +336 -0
- package/skills/ai-task/rr_state/questions_cmd.py +321 -0
- package/skills/ai-task/rr_state/report.py +271 -0
- package/skills/ai-task/rr_state/report_cmd.py +230 -0
- package/skills/ai-task/rr_state/review.py +165 -0
- package/skills/ai-task/rr_state/runtime.py +80 -0
- package/skills/ai-task/rr_state/start.py +257 -0
- package/skills/ai-task/rr_state/steps.py +131 -0
- package/skills/ai-task/rr_state/task.py +396 -0
- package/skills/ai-task/rr_state/verify.py +231 -0
- package/skills/ai-task/sensors.py +16 -3
- package/skills/ai-task/state.py +24 -5161
- package/skills/project-update/adopt.py +38 -24
- package/skills/project-update/history/blobs/a51517e13e57420d2d88a6bcfb4055d762d4a0d10a8f88450929c5c4572213a9 +67 -0
- package/skills/project-update/history/index.json +2 -1
- package/skills/project-update/migrations/0003_context_diet.py +1 -1
- package/skills/project-update/migrations/0007_weight_model.py +130 -23
- package/skills/project-update/migrations/README.md +16 -1
- package/skills/project-update/update.py +202 -26
- package/skills/rr-setup/SKILL.md +9 -2
- package/skills/rr-status/status.py +36 -9
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
},
|
|
6
6
|
"metadata": {
|
|
7
7
|
"description": "A weight-based engineering pipeline for Claude Code and Codex: each change is weighed on six axes, runs light, guarded or full, edits only the files its plan names, and ends at human approval — enforced by hooks, reviewed adversarially, routed to the cheap model tier by default.",
|
|
8
|
-
"version": "3.
|
|
8
|
+
"version": "3.2.2"
|
|
9
9
|
},
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"source": "url",
|
|
15
15
|
"url": "https://github.com/vanssata/claude-agentic-sdlc-module.git"
|
|
16
16
|
},
|
|
17
|
-
"version": "3.
|
|
17
|
+
"version": "3.2.2",
|
|
18
18
|
"strict": true
|
|
19
19
|
}
|
|
20
20
|
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "riskrails",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.2",
|
|
4
4
|
"description": "A weight-based engineering pipeline for Claude Code and Codex: each change is weighed on six axes, runs light, guarded or full, edits only the files its plan names, and ends at human approval — enforced by hooks, reviewed adversarially, routed to the cheap model tier by default.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Ivan Kakurov"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "riskrails",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.2",
|
|
4
4
|
"description": "A weight-based engineering pipeline for Claude Code and Codex: each change is weighed on six axes, runs light, guarded or full, edits only the files its plan names, and ends at human approval — enforced by hooks, reviewed adversarially, routed to the cheap model tier by default.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Ivan Kakurov"
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# RiskRails
|
|
2
2
|
|
|
3
|
-
[](https://github.com/vanssata/claude-agentic-sdlc-module/releases/tag/v3.2.2)
|
|
4
4
|
[](docs/routing.md)
|
|
5
5
|
[](docs/known-risks.md)
|
|
6
6
|
[](LICENSE)
|
|
@@ -18,7 +18,7 @@ approval. Risky actions are stopped by a hook, not by a polite request in a
|
|
|
18
18
|
prompt.
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
npx riskrails@3.
|
|
21
|
+
npx riskrails@3.2.2 # detects Claude Code, Codex or both (Node 18+, Python 3.11+)
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
Then, in any project: `/rr-setup` once, and `/rr-build <what you want>` for
|
|
@@ -50,12 +50,12 @@ Tuned for **one developer who knows the codebase**, on a Claude Pro, Max or
|
|
|
50
50
|
Team plan, or a ChatGPT Plus or Pro plan, following the
|
|
51
51
|
[AI-native SDLC playbook](https://claude.com/blog/the-ai-native-sdlc-playbook).
|
|
52
52
|
|
|
53
|
-
**Version 3.
|
|
53
|
+
**Version 3.2.2.** The guards have limits: read [Known risks](docs/known-risks.md)
|
|
54
54
|
before relying on them. What is decided but not built is in [ROADMAP.md](ROADMAP.md).
|
|
55
55
|
|
|
56
56
|
## Upgrade to 3.1
|
|
57
57
|
|
|
58
|
-
From 3.0.x: update the home (`npx riskrails@3.
|
|
58
|
+
From 3.0.x: update the home (`npx riskrails@3.2.2`, restart the runtime), then
|
|
59
59
|
in each project `/rr-setup --update` (dry run first, then `--apply`). Schema 10
|
|
60
60
|
becomes **11**: migration `0011` proposes deleting `.ai/policies/risk-tiers.json`
|
|
61
61
|
and `risk-tiers.md`, which `weight.json` and `weight.md` replaced; confirm with
|
|
@@ -82,7 +82,7 @@ version. Step by step, in this order:
|
|
|
82
82
|
3. **Update the home** on every machine, for every runtime:
|
|
83
83
|
|
|
84
84
|
```bash
|
|
85
|
-
npx riskrails@3.
|
|
85
|
+
npx riskrails@3.2.2 # or: npx -y riskrails@latest --yes
|
|
86
86
|
```
|
|
87
87
|
|
|
88
88
|
It moves `<home>/claude-agentic/` to `<home>/riskrails/` and leaves a
|
|
@@ -120,12 +120,13 @@ home-side details in [docs/installation.md](docs/installation.md).
|
|
|
120
120
|
## Install
|
|
121
121
|
|
|
122
122
|
```bash
|
|
123
|
-
npx riskrails@3.
|
|
124
|
-
npx riskrails@3.
|
|
125
|
-
npx riskrails@3.
|
|
126
|
-
npx riskrails@3.
|
|
127
|
-
npx riskrails@3.
|
|
128
|
-
npx riskrails@3.
|
|
123
|
+
npx riskrails@3.2.2 # detects Claude Code, Codex, or both; proposes the detected plan
|
|
124
|
+
npx riskrails@3.2.2 --target codex # auto | claude | codex | both
|
|
125
|
+
npx riskrails@3.2.2 --plan max --fable no # pro | team-pro | team-max | max | max20 | balanced-max
|
|
126
|
+
npx riskrails@3.2.2 --codex-plan plus # plus | pro | balanced-max
|
|
127
|
+
npx riskrails@3.2.2 --context economy # the plan's compaction window; the context guard also blocks
|
|
128
|
+
npx riskrails@3.2.2 --statusline own # keep your own statusline command (from 3.2.1; default: the plugin's two-line statusline)
|
|
129
|
+
npx riskrails@3.2.2 --dry-run # print what would be written, write nothing
|
|
129
130
|
npx -y riskrails@latest --yes # update in place, keeping the recorded plan
|
|
130
131
|
```
|
|
131
132
|
|
package/bin/riskrails.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
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
5
|
// [--context default|economy] [--statusline-context yes|no]
|
|
6
|
-
// [--home DIR] [--codex-home DIR] [--yes] [--version]
|
|
6
|
+
// [--statusline riskrails|own] [--home DIR] [--codex-home DIR] [--yes] [--version]
|
|
7
7
|
// --target defaults to auto: each runtime is installed only if its executable is on PATH.
|
|
8
8
|
// --plan Claude only: pro, team-pro (Team Standard seat), team-max (Team Premium seat), max
|
|
9
9
|
// or max20 (Max 20x: max's settings, larger budgets). Defaults to auto-detect from
|
|
@@ -22,9 +22,17 @@
|
|
|
22
22
|
// Max, 300 000 on Pro) and the guard also holds a prompt back past 120% of it.
|
|
23
23
|
// Without economy, an autoCompactWindow equal to the plan's is removed; any other
|
|
24
24
|
// value is the user's and stays. Recorded in riskrails/profile.json as `context`.
|
|
25
|
-
// --statusline-context yes (default) shows the model and
|
|
26
|
-
// statusline:
|
|
27
|
-
// and `context-used` in Codex's [tui] status_line. no
|
|
25
|
+
// --statusline-context yes (default) shows the model, effort and context use in the
|
|
26
|
+
// statusline: the first line in Claude Code (`--context`), `model-with-reasoning`
|
|
27
|
+
// and `context-used` in Codex's [tui] status_line. no leaves that line out (Claude:
|
|
28
|
+
// removes `--context`; Codex: adds nothing and removes nothing); not recorded.
|
|
29
|
+
// --statusline Claude only. riskrails (default) writes the plugin's statusline on every
|
|
30
|
+
// install and update: the model, effort and context line and the prompt line
|
|
31
|
+
// (user@host:cwd | branch | 5h/7d limits | cost), over a statusline command of your
|
|
32
|
+
// own (the old command is printed and kept in riskrails/statusline.json). own keeps
|
|
33
|
+
// your command behind the quota check, as before 3.2, and brings a replaced one
|
|
34
|
+
// back. The choice is recorded in riskrails/statusline.json; a later install
|
|
35
|
+
// without --statusline keeps it.
|
|
28
36
|
// --home / --codex-home the runtime homes (default ~/.claude and ~/.codex; the environment
|
|
29
37
|
// variables CLAUDE_DIR and CODEX_DIR mean the same). Naming exactly one of them under
|
|
30
38
|
// --target auto installs that runtime alone.
|
|
@@ -56,7 +64,7 @@ let CODEX_DIR = env.CODEX_DIR || path.join(os.homedir(), '.codex');
|
|
|
56
64
|
const HOME = env.HOME || os.homedir();
|
|
57
65
|
let PLAN = '', FABLE = 'auto', DRY = false, TARGET = 'auto', PLAN_GIVEN = false;
|
|
58
66
|
let CODEX_PLAN = '', CODEX_PLAN_GIVEN = false, YES = false;
|
|
59
|
-
let CONTEXT = 'default', STATUSLINE_CONTEXT = 'yes';
|
|
67
|
+
let CONTEXT = 'default', STATUSLINE_CONTEXT = 'yes', STATUSLINE = 'riskrails', STATUSLINE_GIVEN = false;
|
|
60
68
|
|
|
61
69
|
function version() {
|
|
62
70
|
try { return JSON.parse(fs.readFileSync(path.join(SRC, 'package.json'), 'utf8')).version || 'unknown'; }
|
|
@@ -88,6 +96,7 @@ function usage() {
|
|
|
88
96
|
case '--codex-plan': CODEX_PLAN = take(); CODEX_PLAN_GIVEN = true; break;
|
|
89
97
|
case '--context': CONTEXT = take(); break;
|
|
90
98
|
case '--statusline-context': STATUSLINE_CONTEXT = take(); break;
|
|
99
|
+
case '--statusline': STATUSLINE = take(); STATUSLINE_GIVEN = true; break;
|
|
91
100
|
case '--home': CLAUDE_DIR = path.resolve(take()); CLAUDE_DIR_GIVEN = true; break;
|
|
92
101
|
case '--codex-home': CODEX_DIR = path.resolve(take()); CODEX_DIR_GIVEN = true; break;
|
|
93
102
|
case '--dry-run': DRY = true; break;
|
|
@@ -105,6 +114,7 @@ if (!['default', 'economy'].includes(CONTEXT)) die(`--context must be default|ec
|
|
|
105
114
|
if (!['yes', 'no'].includes(STATUSLINE_CONTEXT)) {
|
|
106
115
|
die(`--statusline-context must be yes|no (got '${STATUSLINE_CONTEXT}')`, 2);
|
|
107
116
|
}
|
|
117
|
+
if (!['riskrails', 'own'].includes(STATUSLINE)) die(`--statusline must be riskrails|own (got '${STATUSLINE}')`, 2);
|
|
108
118
|
const major = Number(process.versions.node.split('.')[0]);
|
|
109
119
|
if (major < 18) die('riskrails needs Node 18 or newer (this is ' + process.versions.node + ')', 1);
|
|
110
120
|
|
|
@@ -117,6 +127,25 @@ function onPath(cmd) {
|
|
|
117
127
|
return false;
|
|
118
128
|
}
|
|
119
129
|
if (!onPath('python3')) die('python3 is required', 1);
|
|
130
|
+
{
|
|
131
|
+
const pv = spawnSync('python3', ['-c', "import sys; print('%d.%d' % sys.version_info[:2])"],
|
|
132
|
+
{ env, encoding: 'utf8', timeout: 10000, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
133
|
+
const lines = (pv.stdout || '').split('\n').map((l) => l.trim()).filter(Boolean);
|
|
134
|
+
const found = (lines.length ? lines[lines.length - 1] : '').match(/^(\d+)\.(\d+)$/) || [];
|
|
135
|
+
if (!found.length || Number(found[1]) < 3 || (Number(found[1]) === 3 && Number(found[2]) < 11)) {
|
|
136
|
+
let why = '';
|
|
137
|
+
if (!found.length) {
|
|
138
|
+
const errLine = ((pv.stderr || '').split('\n').map((l) => l.trim()).find(Boolean)) || '';
|
|
139
|
+
const parts = [];
|
|
140
|
+
if (pv.error) parts.push(`error ${pv.error.code || pv.error.message}`);
|
|
141
|
+
if (pv.status !== null && pv.status !== undefined && pv.status !== 0) parts.push(`exit status ${pv.status}`);
|
|
142
|
+
if (pv.signal) parts.push(`signal ${pv.signal}`);
|
|
143
|
+
if (errLine) parts.push(`stderr: ${errLine}`);
|
|
144
|
+
why = parts.length ? ` - ${parts.join(', ')}` : ' - no version on stdout';
|
|
145
|
+
}
|
|
146
|
+
die(`python3 3.11 or newer is required (found ${found.length ? found[0] : 'unknown'}${why})`, 1);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
120
149
|
|
|
121
150
|
// ------------------------------------------------------------------ helpers
|
|
122
151
|
const exists = (p) => fs.existsSync(p); // follows symlinks, as [ -e ]
|
|
@@ -553,11 +582,34 @@ function shSplit(s) { // shlex.split,
|
|
|
553
582
|
return words;
|
|
554
583
|
}
|
|
555
584
|
|
|
556
|
-
// The wrapper is `<PREFIX> [--context] [--then '<the user's command>']`; --context
|
|
557
|
-
// (--statusline-context yes) adds a line with the model and
|
|
585
|
+
// The wrapper is `<PREFIX> [--context] [--line] [--then '<the user's command>']`; --context
|
|
586
|
+
// (--statusline-context yes) adds a line with the model, effort and context use, --line
|
|
587
|
+
// (--statusline riskrails, the default) the plugin's prompt line. With riskrails the
|
|
588
|
+
// command is exactly `<PREFIX> [--context] --line`: a command of the user's is replaced
|
|
589
|
+
// (printed, and kept in <home>/riskrails/statusline.json: the dated settings.json backup is
|
|
590
|
+
// replaced by a second run the same day); with own it stays behind --then, and own brings
|
|
591
|
+
// a replaced command back. The mode is recorded there too: a run without --statusline
|
|
592
|
+
// keeps the recorded one, so the update notice's `--yes` does not undo own.
|
|
558
593
|
function statuslineGate(file, gate, mode) {
|
|
559
594
|
const PREFIX = '"$HOME/.claude/hooks/runtime-gate.py" statusline';
|
|
560
|
-
const
|
|
595
|
+
const RECORD = path.join(path.dirname(file), 'riskrails', 'statusline.json');
|
|
596
|
+
let record = {};
|
|
597
|
+
try { const r = JSON.parse(read(RECORD)); if (isObj(r)) record = r; } catch (e) { /* none yet */ }
|
|
598
|
+
const MODE = STATUSLINE_GIVEN ? STATUSLINE : (['riskrails', 'own'].includes(record.mode) ? record.mode : 'riskrails');
|
|
599
|
+
const OWN = MODE === 'own';
|
|
600
|
+
const kept = typeof record.replaced === 'string' && record.replaced.trim() ? record.replaced : '';
|
|
601
|
+
let saved = ''; // a command of the user's this run replaces
|
|
602
|
+
const persist = () => {
|
|
603
|
+
if (mode === 'dry') return;
|
|
604
|
+
const next = Object.assign({}, record, { mode: MODE },
|
|
605
|
+
saved ? { replaced: saved, replaced_at: BACKUP_DATE } : {});
|
|
606
|
+
if (JSON.stringify(next) === JSON.stringify(record)) return;
|
|
607
|
+
mkdirp(path.dirname(RECORD));
|
|
608
|
+
fs.writeFileSync(RECORD + '.tmp', asciiJSON(next) + '\n');
|
|
609
|
+
fs.renameSync(RECORD + '.tmp', RECORD);
|
|
610
|
+
};
|
|
611
|
+
const CTX = STATUSLINE_CONTEXT === 'yes' ? `${PREFIX} --context` : PREFIX;
|
|
612
|
+
const WANT = OWN ? CTX : `${CTX} --line`;
|
|
561
613
|
const OLD_PREFIX = '"$HOME/.claude/hooks/fable-gate.py" statusline';
|
|
562
614
|
let settings = {};
|
|
563
615
|
if (exists(file)) {
|
|
@@ -570,26 +622,64 @@ function statuslineGate(file, gate, mode) {
|
|
|
570
622
|
line.command = PREFIX + cmd.slice(OLD_PREFIX.length);
|
|
571
623
|
ours = true; gate = 'moved';
|
|
572
624
|
}
|
|
573
|
-
// What follows the prefix and
|
|
574
|
-
const tail = () => line.command.slice(PREFIX.length).trim().replace(
|
|
625
|
+
// What follows the prefix and its --context/--line flags: the user's --then, byte for byte.
|
|
626
|
+
const tail = () => line.command.slice(PREFIX.length).trim().replace(/^((--context|--line)(\s+|$))+/, '');
|
|
627
|
+
// The user's own command inside a `--then '<command>'` tail, for the replaced message.
|
|
628
|
+
const wrapped = (rest) => {
|
|
629
|
+
// an unparsable tail is not recorded: an earlier good `replaced` stays, the dated backup holds the line
|
|
630
|
+
try { const parts = shSplit(rest); return parts.length > 1 ? parts[1] : ''; } catch (e) { return ''; }
|
|
631
|
+
};
|
|
575
632
|
const shown = STATUSLINE_CONTEXT === 'yes' ? ' with the model and the context use' : '';
|
|
633
|
+
const plugin = STATUSLINE_CONTEXT === 'yes'
|
|
634
|
+
? 'the model, effort and context line and the prompt line' : 'the prompt line';
|
|
635
|
+
const backup = '--statusline own brings it back';
|
|
576
636
|
let action = null;
|
|
577
637
|
if (gate === 'moved') {
|
|
578
638
|
const rest = tail();
|
|
579
|
-
|
|
580
|
-
|
|
639
|
+
if (OWN) {
|
|
640
|
+
line.command = rest ? `${WANT} ${rest}` : WANT;
|
|
641
|
+
action = 'moved the fable-gate statusline wrapper to runtime-gate (your command is unchanged)';
|
|
642
|
+
} else {
|
|
643
|
+
line.command = WANT;
|
|
644
|
+
saved = rest.startsWith('--then') ? wrapped(rest) : '';
|
|
645
|
+
action = `moved the fable-gate statusline wrapper to runtime-gate, with the plugin's statusline (${plugin})` +
|
|
646
|
+
(saved ? `; replaced your statusline command: ${saved} (${backup})` : '');
|
|
647
|
+
}
|
|
581
648
|
} else if (gate === 'on') {
|
|
582
649
|
if (ours) {
|
|
583
650
|
const rest = tail();
|
|
584
|
-
const
|
|
585
|
-
|
|
651
|
+
const hadLine = /(^|\s)--line(\s|$)/.test(cmd.slice(PREFIX.length));
|
|
652
|
+
// one-shot: only the run that switches to own restores; once own is recorded, a bare line stays bare
|
|
653
|
+
const restore = OWN && !rest && kept && record.mode !== 'own';
|
|
654
|
+
saved = !OWN && rest.startsWith('--then') ? wrapped(rest) : '';
|
|
655
|
+
const want = restore ? `${WANT} --then ${shQuote(kept)}` : OWN && rest ? `${WANT} ${rest}` : WANT;
|
|
656
|
+
if (want === cmd) {
|
|
657
|
+
persist();
|
|
658
|
+
return [OWN ? 'statusline: already records the quota' : "statusline: already the plugin's statusline"];
|
|
659
|
+
}
|
|
586
660
|
line.command = want;
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
661
|
+
if (saved) {
|
|
662
|
+
action = `replaced your statusline command with the plugin's statusline (${plugin}): ${saved} (${backup})`;
|
|
663
|
+
} else if (restore) {
|
|
664
|
+
action = `restored your statusline command behind the quota check${shown}: ${kept}`;
|
|
665
|
+
} else if (!OWN) {
|
|
666
|
+
action = `set the plugin's statusline (${plugin})`;
|
|
667
|
+
} else if (hadLine) {
|
|
668
|
+
action = rest ? `removed the plugin's prompt line; your command runs behind the quota check${shown}`
|
|
669
|
+
: `removed the plugin's prompt line; no command of yours to restore, so the quota check runs${shown} alone`;
|
|
670
|
+
} else {
|
|
671
|
+
action = STATUSLINE_CONTEXT === 'yes'
|
|
672
|
+
? 'added the model and the context use to the statusline wrapper (your command is unchanged)'
|
|
673
|
+
: 'removed the model and context line from the statusline wrapper (your command is unchanged)';
|
|
674
|
+
}
|
|
590
675
|
} else if (line === undefined || line === null) {
|
|
591
676
|
settings.statusLine = { type: 'command', command: WANT };
|
|
592
|
-
action = `added a statusline that records the quota (and the Fable weekly limit)${shown}
|
|
677
|
+
action = OWN ? `added a statusline that records the quota (and the Fable weekly limit)${shown}`
|
|
678
|
+
: `added the plugin's statusline (${plugin}), which also records the quota`;
|
|
679
|
+
} else if (!OWN && isObj(line) && line.type === 'command' && typeof cmd === 'string' && cmd.trim()) {
|
|
680
|
+
line.command = WANT;
|
|
681
|
+
saved = cmd;
|
|
682
|
+
action = `replaced your statusline command with the plugin's statusline (${plugin}): ${cmd} (${backup})`;
|
|
593
683
|
} else if (isObj(line) && line.type === 'command' && typeof cmd === 'string' && cmd.trim()) {
|
|
594
684
|
line.command = `${WANT} --then ${shQuote(cmd)}`;
|
|
595
685
|
action = `wrapped your statusline command with the quota check${shown} (its output is unchanged)`;
|
|
@@ -609,6 +699,7 @@ function statuslineGate(file, gate, mode) {
|
|
|
609
699
|
}
|
|
610
700
|
}
|
|
611
701
|
if (mode === 'dry') return ['statusline: would have ' + action];
|
|
702
|
+
persist(); // the replaced command first: never lost
|
|
612
703
|
fs.writeFileSync(file + '.tmp', asciiJSON(settings) + '\n');
|
|
613
704
|
fs.renameSync(file + '.tmp', file);
|
|
614
705
|
return ['statusline: ' + action];
|
|
@@ -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. A project's dependency_instruction_patterns, from before 3.4, are read as deny_patterns. task_protected_patterns are on only while a task is in flight (.ai/state/state.json, or else current.json, exists and has not reached \"done\"): the runtime's settings, Claude's record of installed plugins and the pipeline's policies, so a run cannot change the rules it is being judged by. Reading anything but a secret is allowed.",
|
|
2
|
+
"_comment": "Shipped defaults for ai-path-guard. Do not edit this file; a project overrides it in .ai/policies/path-guard.json (patterns are unioned, allow wins over every other list). Patterns are POSIX extended regular expressions matched against the absolute path. deny_patterns are secrets: refused for read and write. protected_config_patterns are always on and refuse writes: the task's state and journal, task.md, the compact record (.ai/memory/, .ai/local/memory/), and the guards' own configuration, engine and state.py under .claude/ or .codex/, in a project or in the user's home, and the riskrails plugin's cache folder, which carries the same code on the plugin route. Also the installer's statusline record (.claude/riskrails/statusline.json), which keeps the statusline command it replaced. 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)$",
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
"(^|/)\\.(claude|codex)/hooks/runtime-gate\\.py$",
|
|
39
39
|
"(^|/)\\.(claude|codex)/hooks/(guard|lib)/",
|
|
40
40
|
"(^|/)\\.(claude|codex)/skills/ai-task/",
|
|
41
|
-
"(^|/)\\.(claude|codex)/plugins/cache/[^/]+/riskrails(/|$)"
|
|
41
|
+
"(^|/)\\.(claude|codex)/plugins/cache/[^/]+/riskrails(/|$)",
|
|
42
|
+
"(^|/)\\.claude/(riskrails|claude-agentic)/statusline\\.json$"
|
|
42
43
|
],
|
|
43
44
|
"task_protected_patterns": [
|
|
44
45
|
"(^|/)\\.claude/settings[^/]*\\.json$",
|
package/hooks/context-guard.py
CHANGED
|
@@ -126,7 +126,7 @@ def user_settings():
|
|
|
126
126
|
with open(os.path.join(config_dir(), "settings.json"), encoding="utf-8") as fh:
|
|
127
127
|
settings = json.load(fh)
|
|
128
128
|
return settings if isinstance(settings, dict) else {}
|
|
129
|
-
except (OSError, ValueError):
|
|
129
|
+
except (OSError, ValueError, RecursionError):
|
|
130
130
|
return {}
|
|
131
131
|
|
|
132
132
|
|
|
@@ -157,7 +157,7 @@ def profile_context():
|
|
|
157
157
|
with open(os.path.join(home, "riskrails", "profile.json"), encoding="utf-8") as fh:
|
|
158
158
|
context = json.load(fh).get("context")
|
|
159
159
|
return context if isinstance(context, dict) else {}
|
|
160
|
-
except (OSError, ValueError, AttributeError):
|
|
160
|
+
except (OSError, ValueError, RecursionError, AttributeError):
|
|
161
161
|
return {}
|
|
162
162
|
|
|
163
163
|
|
|
@@ -253,7 +253,7 @@ def current_context(path):
|
|
|
253
253
|
continue
|
|
254
254
|
try:
|
|
255
255
|
entry = json.loads(raw)
|
|
256
|
-
except ValueError:
|
|
256
|
+
except (ValueError, RecursionError):
|
|
257
257
|
continue
|
|
258
258
|
if entry.get("type") == "system" and entry.get("subtype") == "compact_boundary":
|
|
259
259
|
return None # the numbers before it describe the old context
|
|
@@ -301,7 +301,7 @@ def scan_session(path):
|
|
|
301
301
|
continue
|
|
302
302
|
try:
|
|
303
303
|
entry = json.loads(raw)
|
|
304
|
-
except ValueError:
|
|
304
|
+
except (ValueError, RecursionError):
|
|
305
305
|
continue
|
|
306
306
|
if entry.get("isSidechain"):
|
|
307
307
|
continue
|
|
@@ -395,7 +395,7 @@ def task_in_flight(root):
|
|
|
395
395
|
try:
|
|
396
396
|
with open(state, encoding="utf-8") as fh:
|
|
397
397
|
return (json.load(fh) or {}).get("current_stage") != "done"
|
|
398
|
-
except (OSError, ValueError):
|
|
398
|
+
except (OSError, ValueError, RecursionError):
|
|
399
399
|
return False
|
|
400
400
|
|
|
401
401
|
|
|
@@ -412,7 +412,7 @@ def write_session(root, updates):
|
|
|
412
412
|
with open(path, encoding="utf-8") as fh:
|
|
413
413
|
data = json.load(fh)
|
|
414
414
|
data = data if isinstance(data, dict) else {}
|
|
415
|
-
except (OSError, ValueError):
|
|
415
|
+
except (OSError, ValueError, RecursionError):
|
|
416
416
|
data = {}
|
|
417
417
|
data.update(updates)
|
|
418
418
|
try:
|
|
@@ -431,7 +431,7 @@ def prompt_count(root):
|
|
|
431
431
|
try:
|
|
432
432
|
with open(os.path.join(root, ".ai", "state", "session.json"), encoding="utf-8") as fh:
|
|
433
433
|
count = json.load(fh).get("prompt_count")
|
|
434
|
-
except (OSError, ValueError, AttributeError):
|
|
434
|
+
except (OSError, ValueError, RecursionError, AttributeError):
|
|
435
435
|
return 0
|
|
436
436
|
return count if isinstance(count, int) and not isinstance(count, bool) else 0
|
|
437
437
|
|
|
@@ -471,7 +471,7 @@ def recorded_mtime(root):
|
|
|
471
471
|
try:
|
|
472
472
|
with open(os.path.join(root, ".ai", "state", "session.json"), encoding="utf-8") as fh:
|
|
473
473
|
return json.load(fh).get("account_mtime")
|
|
474
|
-
except (OSError, ValueError, AttributeError):
|
|
474
|
+
except (OSError, ValueError, RecursionError, AttributeError):
|
|
475
475
|
return None
|
|
476
476
|
|
|
477
477
|
|
|
@@ -488,7 +488,7 @@ def detect_drift(root, cwd, session, open_it=True, mtime=None):
|
|
|
488
488
|
raw = ""
|
|
489
489
|
try:
|
|
490
490
|
found = json.loads(raw)
|
|
491
|
-
except ValueError:
|
|
491
|
+
except (ValueError, RecursionError):
|
|
492
492
|
return None
|
|
493
493
|
if root and mtime is not None and isinstance(found, dict):
|
|
494
494
|
write_session(root, {"account_mtime": mtime})
|
|
@@ -514,9 +514,12 @@ def drift_notice(root, found, session):
|
|
|
514
514
|
f"DIALOG_PENDING {decision} — call {tool} with exactly this JSON, then run the picked "
|
|
515
515
|
f"command verbatim or 'state.py decision settle --id {decision}': {payload}")
|
|
516
516
|
command = found.get("command") or ""
|
|
517
|
+
ack = found.get("ack")
|
|
518
|
+
keep = f" — or, to keep {installed} and not be told again for this account: {ack}" if ack else ""
|
|
517
519
|
return (f"RiskRails: this account is {account}; RiskRails is installed for {installed}.",
|
|
518
520
|
f"RiskRails: the account's plan is {account} but RiskRails is installed for {installed}, so its "
|
|
519
|
-
f"budgets do not match. Tell the user; in their own terminal, outside the agent, they run: {command}"
|
|
521
|
+
f"budgets do not match. Tell the user; in their own terminal, outside the agent, they run: {command}"
|
|
522
|
+
f"{keep}")
|
|
520
523
|
|
|
521
524
|
|
|
522
525
|
def handoff_block(root, reason):
|
|
@@ -656,7 +659,7 @@ def prompt_warning(payload, session, transcript):
|
|
|
656
659
|
try:
|
|
657
660
|
with open(marker, encoding="utf-8") as fh:
|
|
658
661
|
state = json.load(fh)
|
|
659
|
-
except (OSError, ValueError):
|
|
662
|
+
except (OSError, ValueError, RecursionError):
|
|
660
663
|
state = {}
|
|
661
664
|
|
|
662
665
|
def save():
|
package/hooks/dialog-gate.py
CHANGED
|
@@ -37,7 +37,7 @@ def prompt_count(root):
|
|
|
37
37
|
try:
|
|
38
38
|
with open(os.path.join(root, ".ai", "state", "session.json"), encoding="utf-8") as fh:
|
|
39
39
|
count = json.load(fh).get("prompt_count")
|
|
40
|
-
except (OSError, ValueError, AttributeError):
|
|
40
|
+
except (OSError, ValueError, AttributeError, RecursionError):
|
|
41
41
|
return None
|
|
42
42
|
return count if isinstance(count, int) and not isinstance(count, bool) else None
|
|
43
43
|
|