claude-brink 0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Linus Pisano
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,265 @@
1
+ # Brink
2
+
3
+ > **Usage limits should be checkpoints, not walls.**
4
+
5
+ Brink keeps your AI coding agent's work alive across a usage lockout. When you're about to hit your limit, it gracefully pauses the agent and writes everything in flight to a `HANDOFF.md` — so a surprise mid-task lockout costs you nothing. Optional (experimental): auto-resume when the limit resets.
6
+
7
+ [![status](https://img.shields.io/badge/status-pre--launch-orange)](#status) [![license](https://img.shields.io/badge/license-MIT-blue)](#license) [![core](https://img.shields.io/badge/core-live--verified-brightgreen)](#proof-live-verified-2026-06-26)
8
+
9
+ ```bash
10
+ npm i -g claude-brink
11
+ brink init # wires the hooks + sensor into Claude Code
12
+ brink doctor # verifies the whole chain on YOUR machine
13
+ ```
14
+
15
+ **What works where, today:**
16
+
17
+ | | Windows | macOS / Linux |
18
+ |---|---|---|
19
+ | Auto-pause + `HANDOFF.md` (the core) | ✅ live-verified | ⚠️ should work — **not yet tested** |
20
+ | Usage statusline + warnings | ✅ | ⚠️ untested |
21
+ | Desktop toast | ✅ | ❌ not built yet (ntfy push works everywhere) |
22
+ | Auto-resume (**experimental**, off by default) | ✅ Task Scheduler | ❌ launchd/cron planned |
23
+
24
+ **Kill switch:** `brink off` disables everything instantly (`brink on` re-enables, `brink uninstall` removes cleanly and restores your statusline). Brink *blocks tool calls by design* — you always hold the off button.
25
+
26
+ <!-- DEMO GIF GOES HERE — replace with docs/demo.gif -->
27
+ <p align="center"><em>[ demo.gif — Brink pausing a live session and writing HANDOFF.md ]</em></p>
28
+
29
+ ---
30
+
31
+ ## The problem
32
+
33
+ You're deep in a task. The agent is three files into a refactor, mid-thought, holding context you'll never reconstruct by hand. Then it hits the wall — the 5-hour limit, the weekly cap — and stops cold. Whatever it understood about the task is gone. When the limit resets, you start over, re-explaining, re-orienting, paying for the same ramp-up twice.
34
+
35
+ There are 25+ tools that show you your usage. Gauges, countdowns, menu-bar percentages, push notifiers. They tell you the wall is coming. **None of them keep your work alive when you hit it.**
36
+
37
+ That gap is the whole point of Brink. Usage display is a solved, crowded problem. **Work-continuity is not.**
38
+
39
+ ## The idea: `HANDOFF.md` is the spine
40
+
41
+ Auto-pause is the mechanism. `HANDOFF.md` is the thing that matters.
42
+
43
+ It's a single Markdown file Brink writes for you — the one artifact that survives a pause, a crash, or a reset. It captures what the agent was doing and what comes next, in plain language, so any fresh session (yours or the agent's) can pick the thread back up.
44
+
45
+ Crucially, **Brink writes it deterministically from the session transcript.** It does not ask the model to write its own handoff and hope it complies. (We tried that — it's a good story, [below](#proof-live-verified-2026-06-26).)
46
+
47
+ > The examples below are illustrative. Because the handoff is generated mechanically (not by a model), the real file is plainer: your task statement verbatim, the recent tool actions, and the pause context — no synthesized "next steps" prose beyond what the transcript itself contains.
48
+
49
+ ```markdown
50
+ # Handoff — Refactor auth middleware
51
+
52
+ Paused at 99% of your 5h limit (resets 06:46 PM).
53
+
54
+ ## The task
55
+ Extract the token-refresh logic out of `authMiddleware` into a
56
+ standalone `refreshSession()` helper, and add a unit test for the
57
+ expiry edge case.
58
+
59
+ ## Recent actions before the pause
60
+ - Created `src/auth/refreshSession.ts` with the extracted logic
61
+ - Updated `authMiddleware.ts` to call the new helper
62
+ - Started writing `refreshSession.test.ts` (expiry case stubbed,
63
+ not yet asserting)
64
+
65
+ ## Next steps
66
+ - Finish the expiry-edge-case assertion in `refreshSession.test.ts`
67
+ - Run the auth test suite and confirm green
68
+ - Remove the now-dead inline refresh block from `authMiddleware.ts`
69
+ ```
70
+
71
+ When the limit resets, you (or an external scheduler) point a fresh agent at this file and it knows exactly where to stand.
72
+
73
+ ---
74
+
75
+ ## Proof (live-verified 2026-06-26)
76
+
77
+ This isn't a design doc. The core was verified in a real headless Claude Code session.
78
+
79
+ **1. The pause is real, not theoretical.** Brink's `PreToolUse` deny fired at the configured threshold, **blocked the next tool, and overrode `bypassPermissions` mode** — the blocked action did not run. A control run at 10% usage allowed the exact same tool normally. So the deny is genuinely gating execution, not just printing a warning.
80
+
81
+ **2. The design was hardened against a real failure mode.** An early version *instructed the model* to write the handoff. The model refused it as a security reflex — verbatim:
82
+
83
+ > "I'm treating that message as spurious/injected rather than a genuine directive."
84
+
85
+ That's the correct instinct from a well-aligned model — and it's exactly why "ask the model nicely" is a broken design. So Brink was redesigned to **write `HANDOFF.md` itself**, deterministically, and let the hook do the stopping. After the redesign, the same model stopped gracefully — verbatim:
86
+
87
+ > "I've paused before completing the file creation — Brink stopped the action because you're at 99% of your 5-hour usage limit (resets at 06:46 PM)... progress is saved to HANDOFF.md. Once the limit resets, it'll pick up from there."
88
+
89
+ The mechanism stops the agent at the wall. The `HANDOFF.md` is what survives it.
90
+
91
+ ---
92
+
93
+ ## What Brink does
94
+
95
+ Four things, in increasing order of how much they save you:
96
+
97
+ | # | Capability | What you get |
98
+ |---|------------|--------------|
99
+ | 1 | **Passive usage** | Live 5h / 7d usage right in the CLI statusline (reset countdown shown for the 5h window). |
100
+ | 2 | **Notify** | Phone push (ntfy; Pushover planned) **plus** desktop toast (Windows today; macOS/Linux planned) at configurable thresholds. Both the 5h and 7d windows, plus a "budget's back" ping when the weekly window resets. |
101
+ | 3 | **Auto-pause + handoff** *(the core)* | At your threshold, Brink blocks the next tool and writes `HANDOFF.md` itself. Your work is checkpointed before the wall. |
102
+ | 4 | **Resume** *(optional, **experimental**, off by default)* | An external OS scheduler (Windows Task Scheduler) relaunches a fresh process that reads `HANDOFF.md` after the limit resets. ⚠️ This relaunches an autonomous agent **unattended** — read the caveats before enabling. |
103
+
104
+ ---
105
+
106
+ ## How it works
107
+
108
+ Brink is a **shared Node core with thin per-CLI adapters**, so the hard logic lives in one place and each CLI just maps its own config and field names.
109
+
110
+ ```
111
+ ┌─────────────┐ writes ┌────────────┐ reads ┌──────────────┐
112
+ │ statusline │ ──────────▶ │ state file │ ─────────▶ │ PreToolUse │
113
+ │ sensor │ usage % │ (small) │ │ hook │
114
+ │ (per CLI) │ └────────────┘ └──────┬───────┘
115
+ └─────────────┘ │ at threshold
116
+
117
+ ┌──────────────────────────────┐
118
+ │ 1. block the next tool │
119
+ │ 2. emit a short deny message │
120
+ │ 3. write HANDOFF.md itself │
121
+ │ (from session transcript) │
122
+ └──────────────────────────────┘
123
+ ```
124
+
125
+ 1. **Sensor.** A statusline sensor reads your current usage and writes a small state file.
126
+ 2. **Hook.** A `PreToolUse` hook reads that state file before each tool call. Below threshold, it does nothing. At or above threshold, it blocks the next tool and emits one short, credible, non-contradictory deny:
127
+
128
+ > *Paused by Brink at 99% of your 5h limit, resets 06:46 PM. Saved to HANDOFF.md. Stop and reply in plain text.*
129
+
130
+ 3. **Handoff.** Brink writes `HANDOFF.md` itself, deterministically, from the session transcript. It does **not** depend on the model cooperating.
131
+
132
+ Design choices that matter:
133
+
134
+ - **Default threshold ≈ 93%** of the 5h window — enough headroom that the pause lands *before* the wall, not on it.
135
+ - **No auto-commit.** Brink will not write to your branch unattended. An agent committing to your repo while you're away is the genuinely scary failure mode, so it's off by design — Brink saves a file; what you do with it is your call.
136
+ - **No trusting the model to write the handoff.** That path was tested and rejected (see the proof above).
137
+ - **Kill-switch escape hatch.** A single file instantly disables the hook. A bad threshold can never brick your session.
138
+ - **BOM-robust reads.** State and transcript reads tolerate byte-order marks, so Windows/PowerShell-written config won't trip the parser.
139
+
140
+ ---
141
+
142
+ ## What a generated `HANDOFF.md` looks like
143
+
144
+ Built from the live transcript at pause time — not a template you fill in:
145
+
146
+ ```markdown
147
+ # Handoff — feat/payment-retries
148
+
149
+ Paused at 99% of your 5h limit (resets 06:46 PM).
150
+
151
+ ## The task
152
+ Add exponential-backoff retry to the Stripe webhook handler so transient
153
+ 5xx responses don't drop payment events. Cap at 5 attempts, jittered.
154
+
155
+ ## Recent actions before the pause
156
+ - Added `retryWithBackoff()` helper in src/lib/retry.ts
157
+ - Wired it into webhookHandler() for the `charge.failed` path
158
+ - Started updating tests in test/webhook.test.ts (2 of 5 cases done)
159
+
160
+ ## Next steps
161
+ - Finish the remaining 3 test cases (timeout, max-attempts, success-on-retry)
162
+ - Confirm jitter is applied per-attempt, not once
163
+ - Run the suite and check the handler still returns 200 on give-up
164
+ ```
165
+
166
+ ---
167
+
168
+ ## Supported CLIs
169
+
170
+ The promise is honest: **auto-pause where the CLI supports it, notify-before-limit everywhere else.**
171
+
172
+ | CLI | Auto-pause | Notify | Notes |
173
+ |-----|:----------:|:------:|-------|
174
+ | **Claude Code** | ✅ Full | ✅ | Built and **live-verified** (see proof above). |
175
+ | **Codex CLI** | ⚙️ Pending upstream | ✅ | Adapter built; field names verified against Codex source. Currently **inert** because `rate_limits` is `null` in Codex rollout files ([openai/codex#14880](https://github.com/openai/codex/issues/14880)) — that nulls out notify too, so today the adapter reads nothing. Both notify and auto-pause activate when the upstream data lands (auto-pause with a caveat: Codex's pre-tool gate reliably covers shell commands only). |
176
+ | **Gemini CLI** | 🔜 Planned | 🔜 Planned | Adapter seam ready. |
177
+ | **opencode** | 🔜 Planned | 🔜 Planned | Adapter seam ready. |
178
+ | **Crush** | ❌ | 🔜 Planned | No usage feed / no hooks to gate on — notify-only is the ceiling, and that adapter isn't written yet. |
179
+ | **aider** | ❌ | 🔜 Planned | No usage feed / no hooks to gate on — notify-only is the ceiling, and that adapter isn't written yet. |
180
+
181
+ > To be explicit: **Codex auto-pause does not work today.** It doesn't yet, because of a bug upstream of Brink. The adapter is real and waiting — when the data lands, it lights up with no change on your side.
182
+
183
+ ---
184
+
185
+ ## Honest caveats
186
+
187
+ These are kept on purpose. Brink is a continuity tool, not magic.
188
+
189
+ - **Resume is EXPERIMENTAL.** It's off by default, Windows-only (**Task Scheduler** v1; launchd/cron planned), and has the least real-world runtime of any Brink feature. It relaunches a **fresh** process that reads `HANDOFF.md` — a clean restart from a written checkpoint, **not** an in-memory restore. Enabling it means an autonomous agent restarts **unattended, while you're away**, spending your quota in your project — and `skip_permissions` maps to `--dangerously-skip-permissions` (no permission prompts at all). Leave both off unless you fully accept that.
190
+ - **A 5h reset does not clear the weekly cap.** Brink tracks both windows; if you're up against the 7d wall, a 5h reset won't save you — but Brink will tell you which window you're hitting.
191
+ - **No unattended commits.** By design (see above). `HANDOFF.md` is written; your branch is left untouched.
192
+ - **`HANDOFF.md` contains your session.** It's built from the transcript, so your task text and recent tool actions land in it verbatim — including anything sensitive you typed. It's written to the project directory and **gitignoring it is on you** (Brink never commits it, but *you* might). Treat it like a scratch file, not documentation.
193
+ - **The usage sensor depends on an undocumented surface.** Claude Code pipes `rate_limits` to statuslines today; that's not a versioned API. If your setup doesn't receive usage data (some API-key/enterprise configurations), Brink can't arm — `brink doctor` tells you if that's you.
194
+
195
+ ## Positioning
196
+
197
+ Anthropic explicitly declined the configurable-threshold-alert feature request ([claude-code#17431](https://github.com/anthropics/claude-code/issues/17431)). Brink is **the threshold-aware pause Anthropic said no to, as a drop-in** — plus the handoff and resume layer on top.
198
+
199
+ ---
200
+
201
+ ## Install
202
+
203
+ ### npm (recommended)
204
+
205
+ ```bash
206
+ npm i -g claude-brink
207
+ brink init # wires hooks + usage sensor into ~/.claude/settings.json
208
+ brink doctor # proves the whole chain works on YOUR machine — run it
209
+ ```
210
+
211
+ That's the entire install. `brink init` backs up your settings first (`settings.json.brink-bak`), is idempotent, and **tells you loudly** if it replaces an existing custom statusline (yours is restorable — `brink uninstall` puts it back). If your `settings.json` has a syntax error, `init` **aborts instead of touching it**.
212
+
213
+ Then run **`brink doctor`**. It exercises the real chain end-to-end — sensor → state file → pause hook (in a sandbox) → kill switch → an actual desktop notification — and prints a copy-pasteable report. This exists because silent environment-dependent failure is the #1 risk of a tool like this; if something on your machine doesn't work, doctor finds it *now*, not at 93%.
214
+
215
+ Everyday commands:
216
+
217
+ ```bash
218
+ brink off # kill switch — instantly disables everything
219
+ brink on # re-enable
220
+ brink uninstall # removes hooks, restores your original statusline
221
+ ```
222
+
223
+ `brink init` currently targets **Claude Code**. Multi-CLI detection (Codex, Gemini, opencode) is on the roadmap — see [Supported CLIs](#supported-clis).
224
+
225
+ ### Plugin marketplace (alternative)
226
+
227
+ ```
228
+ /plugin marketplace add LinusPisano/claude-brink
229
+ /plugin install brink@brink
230
+ ```
231
+
232
+ This wires the **hooks** only — Claude Code plugins can't register a `statusLine` (platform limit), and the statusline is Brink's usage sensor. Complete it with `npm i -g claude-brink && brink init` anyway, which makes the plugin path redundant. Honestly: **just use npm.**
233
+
234
+ > ⚠️ If you wire the statusline by hand, never point it into the plugin's install directory — plugin dirs are versioned per update, so the path silently dies on the next update while the hooks keep acting on frozen usage data. The npm global path is stable; use that.
235
+
236
+ ---
237
+
238
+ ## Status
239
+
240
+ - **The core (pause + handoff) is live-verified** on real sessions and dogfooded daily by the author. It survived a 41-finding adversarial review and a 5-advisor launch council — both in `docs/`.
241
+ - **Resume and notifications are younger** — working and end-to-end tested, but with days (not months) of real-world runtime. That's why resume ships off-by-default and `brink doctor` exists.
242
+ - **Verified on Windows 11; macOS/Linux pause-path untested** — the honesty matrix at the top is the truth. If you run it on a Mac, `brink doctor` output in an issue is gold.
243
+ - **On GitHub, currently private** — the repo goes public alongside the launch of [pisanolinus.com](https://pisanolinus.com) and my [LinkedIn](https://www.linkedin.com/in/linus-pisano); the npm package is live now.
244
+
245
+ Roadmap from here: the remaining adapters (Gemini, opencode), the `npm` package + `brink init`, the demo GIF + public flip, and the Codex pause flipping live the moment upstream lands.
246
+
247
+ If you're reading this at launch: the pause is real, the handoff is real, and the caveats above are the whole truth.
248
+
249
+ ---
250
+
251
+ ## License
252
+
253
+ [MIT](LICENSE).
254
+
255
+ ---
256
+
257
+ ## Author
258
+
259
+ Built by **Linus Pisano** — CAD + full-stack + agentic AI.
260
+
261
+ - 🌐 [pisanolinus.com](https://pisanolinus.com)
262
+ - 💼 [linkedin.com/in/linus-pisano](https://www.linkedin.com/in/linus-pisano)
263
+ - ✉️ [pisanolinus@gmail.com](mailto:pisanolinus@gmail.com)
264
+
265
+ *Follow the launch on X — link coming with the public release.*
@@ -0,0 +1,22 @@
1
+ {
2
+ "_comment": "Copy to config.json in ~/.claude/brink/ and edit. Never commit your ntfy topic (it is the only auth for your push channel). The kill switch is a fixed path: create ~/.claude/brink/DISABLED to instantly disable Brink.",
3
+ "notify": {
4
+ "ntfy_topic": "REPLACE-with-your-secret-topic",
5
+ "windows_toast": true
6
+ },
7
+ "thresholds": {
8
+ "five_hour": { "warn": [75, 85], "pause": 93 },
9
+ "seven_day": { "warn": [80, 90], "pause": 95 }
10
+ },
11
+ "_reset_ping_comment": "Ping when a window rolls over (budget is back). 'enabled' off for 5h (resets several times a day = noise) / on for weekly (days of budget back). 'floor' = only ping if you were at/above this % before it reset, so a reset from low usage stays silent.",
12
+ "reset_ping": {
13
+ "five_hour": { "enabled": false, "floor": 75 },
14
+ "seven_day": { "enabled": true, "floor": 80 }
15
+ },
16
+ "_resume_comment": "Opt-in. skip_permissions=true relaunches with --dangerously-skip-permissions: the resumed agent runs WITHOUT permission prompts, unattended, in your project. Leave false unless you fully accept that.",
17
+ "resume": {
18
+ "enabled": false,
19
+ "buffer_seconds": 90,
20
+ "skip_permissions": false
21
+ }
22
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "comment": "Brink plugin hooks (auto-discovered when installed via the Claude Code plugin marketplace). PreToolUse = graceful auto-pause before the limit; PostToolUse = threshold warning. ${CLAUDE_PLUGIN_ROOT} resolves to the installed plugin dir. NOTE: a plugin CANNOT register the statusLine (the usage sensor) — only 'agent'/'subagentStatusLine' are honored in a plugin's settings.json. The statusLine must be added by the user (one line) or by `node src/install.js --statusline`. Without it, state.json is never written and these hooks have no data to act on.",
3
+ "hooks": {
4
+ "PreToolUse": [
5
+ {
6
+ "hooks": [
7
+ { "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/src/brink.js\" claude pause" }
8
+ ]
9
+ }
10
+ ],
11
+ "PostToolUse": [
12
+ {
13
+ "hooks": [
14
+ { "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/src/brink.js\" claude warn" }
15
+ ]
16
+ }
17
+ ]
18
+ }
19
+ }
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "claude-brink",
3
+ "version": "0.1.0",
4
+ "description": "Claude Code stops at the brink — graceful auto-pause + handoff before you hit a usage limit, with optional auto-resume after reset.",
5
+ "license": "MIT",
6
+ "author": "Linus Pisano <pisanolinus@gmail.com>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/LinusPisano/claude-brink.git"
10
+ },
11
+ "homepage": "https://github.com/LinusPisano/claude-brink#readme",
12
+ "keywords": ["claude-code", "usage-limit", "rate-limit", "auto-pause", "handoff", "notifications", "ntfy"],
13
+ "bin": { "brink": "src/cli.js" },
14
+ "engines": { "node": ">=18" },
15
+ "scripts": {
16
+ "test": "node tests/run.js && node tests/core.test.js && node tests/handoff.test.js && node tests/install.test.js && node tests/reset.test.js && node tests/resume.test.js && node tests/cli.test.js",
17
+ "install-hooks": "node src/install.js",
18
+ "install-all": "node src/install.js --statusline"
19
+ },
20
+ "files": ["src", "hooks", "config.example.json", "README.md", "LICENSE"]
21
+ }
@@ -0,0 +1,43 @@
1
+ // Brink adapter — Claude Code. Implements the two-method adapter contract:
2
+ // readUsage() -> normalized usage | denyOutput(reason) -> how this CLI blocks
3
+ // Usage source: the state.json the Brink statusline writes (the cleanest source of all).
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const path = require('path');
7
+
8
+ const dir = () => process.env.BRINK_DIR || path.join(os.homedir(), '.claude', 'brink');
9
+
10
+ function readUsage() {
11
+ const sp = path.join(dir(), 'state.json');
12
+ if (!fs.existsSync(sp)) return null;
13
+ try {
14
+ const raw = fs.readFileSync(sp, 'utf8');
15
+ const s = JSON.parse(raw.charCodeAt(0) === 0xFEFF ? raw.slice(1) : raw);
16
+ const num = (v) => (typeof v === 'number' ? v : null);
17
+ return {
18
+ provider: 'claude',
19
+ five_pct: num(s.five_pct),
20
+ week_pct: num(s.week_pct),
21
+ five_reset: num(s.five_reset),
22
+ week_reset: num(s.week_reset),
23
+ session_id: s.session_id || '',
24
+ cwd: s.cwd || '',
25
+ };
26
+ } catch { return null; }
27
+ }
28
+
29
+ // Claude Code PreToolUse deny: JSON on stdout, exit 0.
30
+ function denyOutput(reason) {
31
+ return {
32
+ stdout: JSON.stringify({
33
+ hookSpecificOutput: {
34
+ hookEventName: 'PreToolUse',
35
+ permissionDecision: 'deny',
36
+ permissionDecisionReason: reason,
37
+ },
38
+ }),
39
+ exitCode: 0,
40
+ };
41
+ }
42
+
43
+ module.exports = { name: 'claude', readUsage, denyOutput };
@@ -0,0 +1,80 @@
1
+ // Brink adapter — OpenAI Codex CLI.
2
+ // readUsage(): parse the newest ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl token_count
3
+ // event, which carries the primary (~5h) + secondary (~weekly) rate-limit windows.
4
+ // denyOutput(): Codex PreToolUse blocks via exit code 2 + reason on stderr.
5
+ //
6
+ // Field names VERIFIED against Codex Rust source (protocol.rs, RateLimitWindow) + 53 real
7
+ // local rollout files: guard .type=="event_msg" && .payload.type=="token_count"; data at
8
+ // .payload.rate_limits.{primary,secondary}.{used_percent, window_minutes, resets_at} (epoch s).
9
+ // ⚠️ UPSTREAM BUG (openai/codex#14880): rate_limits is almost always `null` in rollout files,
10
+ // so this returns null TODAY and pause won't fire until OpenAI populates it (or we add an alt
11
+ // source: auth.json -> ChatGPT usage endpoint). We scan back for a populated line and never
12
+ // act on null (safe no-op). ⚠️ Codex PreToolUse also reliably gates Bash only (edits/MCP leaky).
13
+ const fs = require('fs');
14
+ const os = require('os');
15
+ const path = require('path');
16
+
17
+ const sessionsDir = () => process.env.CODEX_SESSIONS || path.join(os.homedir(), '.codex', 'sessions');
18
+
19
+ function newestRollout(root) {
20
+ if (!fs.existsSync(root)) return null;
21
+ let best = null;
22
+ const walk = (d) => {
23
+ for (const e of fs.readdirSync(d, { withFileTypes: true })) {
24
+ const p = path.join(d, e.name);
25
+ if (e.isDirectory()) walk(p);
26
+ else if (/^rollout-.*\.jsonl$/.test(e.name)) {
27
+ const m = fs.statSync(p).mtimeMs;
28
+ if (!best || m > best.m) best = { p, m };
29
+ }
30
+ }
31
+ };
32
+ try { walk(root); } catch { return null; }
33
+ return best && best.p;
34
+ }
35
+
36
+ const pick = (obj, keys) => {
37
+ for (const k of keys) if (obj && obj[k] != null) return obj[k];
38
+ return null;
39
+ };
40
+
41
+ function parseWindow(w, now) {
42
+ if (!w) return { pct: null, reset: null };
43
+ const pct = pick(w, ['used_percent', 'used_percentage', 'percent', 'percent_used']);
44
+ const at = pick(w, ['resets_at', 'reset_at']);
45
+ const inSecs = pick(w, ['resets_in_seconds', 'reset_in_seconds', 'resets_in']);
46
+ let reset = null;
47
+ if (typeof at === 'number') reset = at > 1e12 ? Math.floor(at / 1000) : at;
48
+ else if (typeof inSecs === 'number') reset = now + inSecs;
49
+ return { pct: typeof pct === 'number' ? pct : null, reset };
50
+ }
51
+
52
+ function readUsage() {
53
+ const f = newestRollout(sessionsDir());
54
+ if (!f) return null;
55
+ let lines;
56
+ try { let raw = fs.readFileSync(f, 'utf8'); if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1); lines = raw.split(/\r?\n/).filter(Boolean); } catch { return null; }
57
+ let rl = null;
58
+ for (let i = lines.length - 1; i >= 0; i--) {
59
+ let ev; try { ev = JSON.parse(lines[i]); } catch { continue; }
60
+ rl = ev.rate_limits || (ev.payload && ev.payload.rate_limits) || (ev.info && ev.info.rate_limits) || null;
61
+ if (rl) break;
62
+ }
63
+ if (!rl) return null;
64
+ const now = Math.floor(Date.now() / 1000);
65
+ const primary = parseWindow(rl.primary || rl.five_hour, now);
66
+ const secondary = parseWindow(rl.secondary || rl.seven_day || rl.weekly, now);
67
+ return {
68
+ provider: 'codex',
69
+ five_pct: primary.pct, five_reset: primary.reset,
70
+ week_pct: secondary.pct, week_reset: secondary.reset,
71
+ session_id: '', cwd: process.cwd(),
72
+ };
73
+ }
74
+
75
+ // Codex PreToolUse deny: exit code 2 + reason on stderr (the reliable path).
76
+ function denyOutput(reason) {
77
+ return { stderr: reason, exitCode: 2 };
78
+ }
79
+
80
+ module.exports = { name: 'codex', readUsage, denyOutput };
@@ -0,0 +1,27 @@
1
+ # Brink - arm resume (Phase 7, opt-in)
2
+ # Registers a one-shot Windows Task Scheduler job for the reset time that relaunches
3
+ # Claude from HANDOFF.md. Driven by the authoritative resets_at (epoch seconds).
4
+ # Buffer/Skip are passed through from config.json by brink.js (core/resume.js).
5
+ # Invoked SYNCHRONOUSLY by brink.js (detached PowerShell dies without a console -
6
+ # live-fire finding 2026-07-04).
7
+ param(
8
+ [Parameter(Mandatory)][string]$ResetsAt,
9
+ [string]$Sid,
10
+ [string]$Proj,
11
+ [int]$Buffer = 90,
12
+ [string]$Skip = '0'
13
+ )
14
+
15
+ $fireAt = [DateTimeOffset]::FromUnixTimeSeconds([int64]$ResetsAt).ToLocalTime().AddSeconds($Buffer).DateTime
16
+ # Sanitize the task name: session ids are normally uuid-safe, but Task Scheduler
17
+ # rejects several characters, and the name must round-trip to resume-once.ps1.
18
+ $name = 'BrinkResume_' + ($Sid -replace '[^\w\-]', '_')
19
+ Unregister-ScheduledTask -TaskName $name -Confirm:$false -ErrorAction SilentlyContinue
20
+
21
+ $resume = Join-Path $PSScriptRoot 'resume-once.ps1'
22
+ # -ExecutionPolicy Bypass: stock Windows is Restricted and would refuse the script.
23
+ $arg = "-NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$resume`" -Sid `"$Sid`" -Proj `"$Proj`" -Buffer $Buffer -Skip `"$Skip`""
24
+ Register-ScheduledTask -TaskName $name -RunLevel Limited `
25
+ -Action (New-ScheduledTaskAction -Execute 'powershell.exe' -Argument $arg) `
26
+ -Trigger (New-ScheduledTaskTrigger -Once -At $fireAt) `
27
+ -Settings(New-ScheduledTaskSettingsSet -StartWhenAvailable -WakeToRun -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries) -ErrorAction Stop | Out-Null