claude-marathon 0.2.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 marathon contributors
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,193 @@
1
+ # marathon
2
+
3
+ **Infinite context sessions for Claude Code.**
4
+
5
+ Two modes. Same result: Claude never loses context.
6
+
7
+ ## Auto mode (recommended)
8
+
9
+ Install once, then use `claude` normally. Marathon activates only when context gets large.
10
+
11
+ ```bash
12
+ marathon install # one-time setup
13
+ claude # use normally — marathon kicks in when needed
14
+ ```
15
+
16
+ Zero overhead for small tasks. When context triggers compaction, Claude is automatically told to write a handoff file. Next session picks up where it left off.
17
+
18
+ ```bash
19
+ marathon status # check if hooks are installed & any active handoffs
20
+ marathon uninstall # remove hooks
21
+ ```
22
+
23
+ ## Explicit mode
24
+
25
+ Wrap a task in marathon for guaranteed multi-session execution.
26
+
27
+ ```bash
28
+ marathon "Refactor the entire auth module to use JWT tokens"
29
+ ```
30
+
31
+ ```
32
+ marathon v0.2.0
33
+ Infinite context sessions for Claude Code
34
+
35
+ Task: Refactor the entire auth module to use JWT tokens
36
+ Max turns: 200 per leg
37
+ Max legs: 50
38
+
39
+ [marathon leg 1/50] Starting...
40
+ [marathon] Leg 1 complete (tokens: 45000in/12000out, stop: end_turn)
41
+ [marathon] Handoff found. Continuing to next leg...
42
+
43
+ [marathon leg 2/50] Starting...
44
+ [marathon] Leg 2 complete (tokens: 38000in/9000out, stop: end_turn)
45
+ [marathon] Task complete after 2 leg(s)!
46
+ ```
47
+
48
+ ## How it works
49
+
50
+ ### Auto mode (`marathon install`)
51
+
52
+ 1. Adds hooks to Claude Code's `~/.claude/settings.json`
53
+ 2. **PreCompact hook**: When context is about to be compacted (signal that it's getting large), Claude is told to write `.marathon/handoff.md` with current progress
54
+ 3. **SessionStart hook**: When a new session starts and a handoff file exists, Claude is given the context and picks up where it left off
55
+ 4. **Stop hook**: When a task is marked complete, the handoff is archived
56
+
57
+ No wrapper, no extra commands. Just `claude`.
58
+
59
+ ### Explicit mode (`marathon "task"`)
60
+
61
+ 1. Marathon runs Claude Code with `--print` and a system prompt teaching the handoff protocol
62
+ 2. Claude works normally, writing/updating `.marathon/handoff.md` as it goes
63
+ 3. When the session ends (max turns or completion), marathon checks the handoff
64
+ 4. If work remains, a new "leg" starts with the handoff context
65
+ 5. Repeats until `## Status: COMPLETE` or max legs reached
66
+
67
+ Each "leg" is a fresh session with full context budget.
68
+
69
+ ## Install
70
+
71
+ ```bash
72
+ # Install globally via npm
73
+ npm install -g claude-marathon
74
+
75
+ # Enable auto mode (hooks into Claude Code)
76
+ marathon install
77
+ ```
78
+
79
+ Or without npm:
80
+
81
+ ```bash
82
+ git clone https://github.com/josephtandle/claude-marathon.git
83
+ ln -s $(pwd)/claude-marathon/bin/marathon /usr/local/bin/marathon
84
+ marathon install
85
+ ```
86
+
87
+ **Requirements:** [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (`claude` CLI), `node`, and `jq`
88
+
89
+ ## Usage
90
+
91
+ ```bash
92
+ # Auto mode — install once, forget about it
93
+ marathon install
94
+
95
+ # Explicit mode — for specific tasks
96
+ marathon "Build a REST API for the user management system"
97
+ marathon -f tasks/migration-plan.md
98
+ marathon -m opus "Design and implement the caching layer"
99
+ marathon -t 100 -l 10 "Fix all TypeScript errors"
100
+ marathon -a "Bash,Read,Edit,Write,Glob,Grep" "Fix all lint errors"
101
+ marathon -p auto "Migrate database schema"
102
+
103
+ # Continue from an existing handoff
104
+ marathon continue
105
+
106
+ # Management
107
+ marathon status
108
+ marathon uninstall
109
+ ```
110
+
111
+ ## Options (explicit mode)
112
+
113
+ | Flag | Description | Default |
114
+ |------|-------------|---------|
115
+ | `-t, --max-turns <n>` | Max turns per leg | 200 |
116
+ | `-l, --max-legs <n>` | Max session legs | 50 |
117
+ | `-b, --max-budget <usd>` | Budget per leg (USD) | unlimited |
118
+ | `-m, --model <model>` | Model (opus, sonnet, etc.) | default |
119
+ | `-p, --permission-mode <mode>` | Permission mode | default |
120
+ | `-a, --allowed-tools <tools>` | Pre-approved tools | none |
121
+ | `-d, --work-dir <path>` | Working directory | current |
122
+ | `-f, --file <path>` | Read task from file | - |
123
+ | `-q, --quiet` | Minimal output | false |
124
+ | `--dry-run` | Preview without running | false |
125
+
126
+ ## Environment variables
127
+
128
+ ```bash
129
+ export MARATHON_MAX_TURNS=150
130
+ export MARATHON_MAX_LEGS=20
131
+ export MARATHON_MAX_BUDGET=5.00
132
+ export MARATHON_MODEL=opus
133
+ export MARATHON_PERMISSION_MODE=auto
134
+ export MARATHON_ALLOWED_TOOLS="Bash,Read,Edit,Write,Glob,Grep"
135
+ ```
136
+
137
+ ## The handoff file
138
+
139
+ Claude writes `.marathon/handoff.md` with this structure:
140
+
141
+ ```markdown
142
+ ## Task
143
+ What we're trying to achieve
144
+
145
+ ## Progress
146
+ What's been completed so far
147
+
148
+ ## Current State
149
+ Where things stand right now
150
+
151
+ ## Next Steps
152
+ 1. Specific next actions
153
+ 2. In order
154
+
155
+ ## Key Context
156
+ File paths, decisions made, gotchas found
157
+ ```
158
+
159
+ When the task is done, Claude adds `## Status: COMPLETE` at the top, and the handoff is archived automatically.
160
+
161
+ ## Output files
162
+
163
+ ```
164
+ .marathon/
165
+ handoff.md # Current handoff (auto mode + explicit mode)
166
+ run.json # Run metadata (explicit mode)
167
+ summary.json # Final summary (explicit mode)
168
+ leg-1-result.json # Claude output per leg (explicit mode)
169
+ ```
170
+
171
+ Add `.marathon/` to your `.gitignore`.
172
+
173
+ ## Recovery
174
+
175
+ If Claude hits max turns before writing a handoff, marathon creates a recovery handoff that tells the next session to check `git status`/`git diff` and continue.
176
+
177
+ ## FAQ
178
+
179
+ **Does auto mode slow down normal sessions?**
180
+ No. The hooks only fire on specific events (PreCompact, SessionStart, Stop). PreCompact only fires when context is large enough to need compaction — small sessions never trigger it.
181
+
182
+ **Can I use both modes?**
183
+ Yes. Auto mode handles interactive sessions. Explicit mode is better for headless/CI automation where you want the full loop.
184
+
185
+ **What if I want to stop auto-continuation?**
186
+ Delete `.marathon/handoff.md` in your project, or run `marathon uninstall` to remove hooks entirely.
187
+
188
+ **Does it work with subagents?**
189
+ The hooks only fire for the main session, not subagents. This is intentional — subagents have their own context management.
190
+
191
+ ## License
192
+
193
+ MIT
package/bin/marathon ADDED
@@ -0,0 +1,690 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # marathon - Infinite context sessions for Claude Code
5
+ # https://github.com/anthropics/marathon
6
+ #
7
+ # When context gets large, Claude writes a handoff file, the session ends,
8
+ # and a new session picks up right where it left off. Automatically.
9
+
10
+ VERSION="0.2.0"
11
+ # Resolve symlinks to find the actual install directory
12
+ _resolve_script() {
13
+ local src="${BASH_SOURCE[0]}"
14
+ while [[ -L "$src" ]]; do
15
+ local dir="$(cd "$(dirname "$src")" && pwd)"
16
+ src="$(readlink "$src")"
17
+ [[ "$src" != /* ]] && src="$dir/$src"
18
+ done
19
+ cd "$(dirname "$src")/.." && pwd
20
+ }
21
+ SCRIPT_DIR="$(_resolve_script)"
22
+ MARATHON_DIR=".marathon"
23
+ CLAUDE_SETTINGS="${HOME}/.claude/settings.json"
24
+ HOOKS_DIR="${HOME}/.claude/hooks"
25
+ MAX_TURNS="${MARATHON_MAX_TURNS:-200}"
26
+ MAX_LEGS="${MARATHON_MAX_LEGS:-50}"
27
+ MAX_BUDGET="${MARATHON_MAX_BUDGET:-}"
28
+ MODEL="${MARATHON_MODEL:-}"
29
+ PERMISSION_MODE="${MARATHON_PERMISSION_MODE:-}"
30
+ ALLOWED_TOOLS="${MARATHON_ALLOWED_TOOLS:-}"
31
+ WORK_DIR="${MARATHON_WORK_DIR:-.}"
32
+ QUIET=false
33
+ DRY_RUN=false
34
+
35
+ # Colors
36
+ RED='\033[0;31m'
37
+ GREEN='\033[0;32m'
38
+ YELLOW='\033[1;33m'
39
+ BLUE='\033[0;34m'
40
+ DIM='\033[2m'
41
+ BOLD='\033[1m'
42
+ NC='\033[0m'
43
+
44
+ usage() {
45
+ cat <<'EOF'
46
+ marathon - Infinite context sessions for Claude Code
47
+
48
+ USAGE:
49
+ marathon [options] "your task description"
50
+ marathon [options] -f task.md
51
+ marathon install Install hooks for automatic mode
52
+ marathon uninstall Remove marathon hooks
53
+ marathon status Show if hooks are installed & any active handoffs
54
+ marathon continue Continue from an existing handoff file
55
+
56
+ OPTIONS:
57
+ -t, --max-turns <n> Max turns per leg (default: 200)
58
+ -l, --max-legs <n> Max session legs before stopping (default: 50)
59
+ -b, --max-budget <usd> Max budget in USD per leg
60
+ -m, --model <model> Model to use (e.g., opus, sonnet)
61
+ -p, --permission-mode <mode> Permission mode (default, plan, auto, etc.)
62
+ -a, --allowed-tools <tools> Comma-separated tools to pre-approve
63
+ -d, --work-dir <path> Working directory (default: current)
64
+ -f, --file <path> Read task from file instead of argument
65
+ -q, --quiet Minimal output (no banner, less logging)
66
+ --dry-run Show what would run without executing
67
+ -v, --version Show version
68
+ -h, --help Show this help
69
+
70
+ ENVIRONMENT VARIABLES:
71
+ MARATHON_MAX_TURNS Default max turns per leg
72
+ MARATHON_MAX_LEGS Default max legs
73
+ MARATHON_MAX_BUDGET Default budget per leg
74
+ MARATHON_MODEL Default model
75
+ MARATHON_PERMISSION_MODE Default permission mode
76
+ MARATHON_ALLOWED_TOOLS Default allowed tools
77
+ MARATHON_WORK_DIR Default working directory
78
+
79
+ EXAMPLES:
80
+ # Explicit mode - wrap a task in marathon
81
+ marathon "Refactor the auth module to use JWT"
82
+ marathon -t 100 -l 10 -m opus "Build the entire test suite"
83
+ marathon -f tasks/migration.md --permission-mode auto
84
+
85
+ # Auto mode - install hooks, then use claude normally
86
+ marathon install
87
+ claude # marathon activates automatically when context gets large
88
+
89
+ HOW IT WORKS:
90
+
91
+ Explicit mode (marathon "task"):
92
+ Marathon runs Claude in a loop. Each "leg" is a fresh session.
93
+ Claude writes a handoff file, marathon reads it, starts a new session.
94
+
95
+ Auto mode (marathon install):
96
+ Hooks are added to Claude Code that fire automatically:
97
+ - PreCompact: When context is about to be compacted, Claude is told
98
+ to write a handoff file. Zero overhead until this point.
99
+ - SessionStart: If a handoff file exists from a previous session,
100
+ Claude is given the context and picks up where it left off.
101
+ No wrapper needed. Just use claude normally.
102
+
103
+ EOF
104
+ exit 0
105
+ }
106
+
107
+ # ─── Subcommands ──────────────────────────────────────────────────────────────
108
+
109
+ cmd_install() {
110
+ echo -e "${BOLD}marathon install${NC}"
111
+ echo ""
112
+
113
+ # Ensure hooks dir exists
114
+ mkdir -p "$HOOKS_DIR"
115
+
116
+ # Copy hooks script
117
+ local hook_src="$SCRIPT_DIR/hooks/marathon-hooks.js"
118
+ local hook_dst="$HOOKS_DIR/marathon-hooks.js"
119
+
120
+ if [[ ! -f "$hook_src" ]]; then
121
+ log_error "Hook script not found at $hook_src"
122
+ log_error "Make sure you're running from the marathon directory or it's properly installed."
123
+ exit 1
124
+ fi
125
+
126
+ cp "$hook_src" "$hook_dst"
127
+ echo -e " ${GREEN}+${NC} Copied hooks to $hook_dst"
128
+
129
+ # Update settings.json
130
+ if [[ ! -f "$CLAUDE_SETTINGS" ]]; then
131
+ echo '{}' > "$CLAUDE_SETTINGS"
132
+ fi
133
+
134
+ # Read current settings
135
+ local settings
136
+ settings=$(cat "$CLAUDE_SETTINGS")
137
+
138
+ # Check if marathon hooks already installed
139
+ if echo "$settings" | jq -e '.hooks.PreCompact[]?.hooks[]? | select(.command | contains("marathon-hooks.js"))' &>/dev/null; then
140
+ echo -e " ${DIM}Hooks already installed, updating...${NC}"
141
+ fi
142
+
143
+ # Remove any existing marathon hooks first (idempotent install)
144
+ # Use a node one-liner since jq gets tricky with nested hook structures
145
+ settings=$(node -e "
146
+ const s = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
147
+ if (s.hooks) {
148
+ for (const [event, matchers] of Object.entries(s.hooks)) {
149
+ if (!Array.isArray(matchers)) continue;
150
+ s.hooks[event] = matchers.filter(m => {
151
+ if (Array.isArray(m.hooks)) {
152
+ m.hooks = m.hooks.filter(h => !(h.command || '').includes('marathon-hooks.js'));
153
+ return m.hooks.length > 0;
154
+ }
155
+ return true;
156
+ });
157
+ if (s.hooks[event].length === 0) delete s.hooks[event];
158
+ }
159
+ if (Object.keys(s.hooks).length === 0) delete s.hooks;
160
+ }
161
+ process.stdout.write(JSON.stringify(s, null, 2));
162
+ " <<< "$settings")
163
+
164
+ # Add marathon hooks for PreCompact, SessionStart, and Stop
165
+ local hook_cmd="node \"$hook_dst\""
166
+
167
+ settings=$(echo "$settings" | jq --arg cmd "$hook_cmd" '
168
+ .hooks //= {} |
169
+
170
+ .hooks.PreCompact //= [] |
171
+ .hooks.PreCompact += [{
172
+ "hooks": [{
173
+ "type": "command",
174
+ "command": ($cmd + " PreCompact")
175
+ }]
176
+ }] |
177
+
178
+ .hooks.SessionStart //= [] |
179
+ .hooks.SessionStart += [{
180
+ "hooks": [{
181
+ "type": "command",
182
+ "command": ($cmd + " SessionStart")
183
+ }]
184
+ }] |
185
+
186
+ .hooks.Stop //= [] |
187
+ .hooks.Stop += [{
188
+ "hooks": [{
189
+ "type": "command",
190
+ "command": ($cmd + " Stop")
191
+ }]
192
+ }]
193
+ ')
194
+
195
+ echo "$settings" | jq '.' > "$CLAUDE_SETTINGS"
196
+ echo -e " ${GREEN}+${NC} Added hooks to $CLAUDE_SETTINGS"
197
+
198
+ echo ""
199
+ echo -e "${GREEN}${BOLD}Marathon auto mode installed.${NC}"
200
+ echo ""
201
+ echo "How it works:"
202
+ echo " 1. Use claude normally — zero overhead for small tasks"
203
+ echo " 2. When context gets large enough to trigger compaction,"
204
+ echo " Claude is told to write a handoff file (.marathon/handoff.md)"
205
+ echo " 3. Next time you start claude in the same directory,"
206
+ echo " it automatically picks up from the handoff"
207
+ echo ""
208
+ echo "That's it. No wrapper needed."
209
+ echo ""
210
+ echo "To uninstall: marathon uninstall"
211
+ exit 0
212
+ }
213
+
214
+ cmd_uninstall() {
215
+ echo -e "${BOLD}marathon uninstall${NC}"
216
+ echo ""
217
+
218
+ # Remove hooks from settings
219
+ if [[ -f "$CLAUDE_SETTINGS" ]]; then
220
+ local settings
221
+ settings=$(cat "$CLAUDE_SETTINGS")
222
+
223
+ settings=$(node -e "
224
+ const s = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
225
+ if (s.hooks) {
226
+ for (const [event, matchers] of Object.entries(s.hooks)) {
227
+ if (!Array.isArray(matchers)) continue;
228
+ s.hooks[event] = matchers.filter(m => {
229
+ if (Array.isArray(m.hooks)) {
230
+ m.hooks = m.hooks.filter(h => !(h.command || '').includes('marathon-hooks.js'));
231
+ return m.hooks.length > 0;
232
+ }
233
+ return true;
234
+ });
235
+ if (s.hooks[event].length === 0) delete s.hooks[event];
236
+ }
237
+ if (Object.keys(s.hooks).length === 0) delete s.hooks;
238
+ }
239
+ process.stdout.write(JSON.stringify(s, null, 2));
240
+ " <<< "$settings")
241
+
242
+ echo "$settings" | jq '.' > "$CLAUDE_SETTINGS"
243
+ echo -e " ${GREEN}-${NC} Removed hooks from $CLAUDE_SETTINGS"
244
+ fi
245
+
246
+ # Remove hook script
247
+ local hook_dst="$HOOKS_DIR/marathon-hooks.js"
248
+ if [[ -f "$hook_dst" ]]; then
249
+ rm "$hook_dst"
250
+ echo -e " ${GREEN}-${NC} Removed $hook_dst"
251
+ fi
252
+
253
+ echo ""
254
+ echo -e "${GREEN}Marathon auto mode uninstalled.${NC}"
255
+ echo "You can still use marathon explicitly: marathon \"your task\""
256
+ exit 0
257
+ }
258
+
259
+ cmd_status() {
260
+ echo -e "${BOLD}marathon status${NC}"
261
+ echo ""
262
+
263
+ # Check hooks
264
+ local hooks_installed=false
265
+ if [[ -f "$CLAUDE_SETTINGS" ]] && jq -e '.hooks.PreCompact[]?.hooks[]? | select(.command | contains("marathon-hooks.js"))' "$CLAUDE_SETTINGS" &>/dev/null; then
266
+ hooks_installed=true
267
+ fi
268
+
269
+ if [[ "$hooks_installed" == true ]]; then
270
+ echo -e " Auto mode: ${GREEN}installed${NC}"
271
+ else
272
+ echo -e " Auto mode: ${DIM}not installed${NC} (run 'marathon install')"
273
+ fi
274
+
275
+ # Check for active handoff
276
+ if [[ -f "$MARATHON_DIR/handoff.md" ]]; then
277
+ local first_line
278
+ first_line=$(head -5 "$MARATHON_DIR/handoff.md" | tr '[:upper:]' '[:lower:]')
279
+ if echo "$first_line" | grep -q "status:.*complete"; then
280
+ echo -e " Handoff: ${GREEN}complete${NC} (.marathon/handoff.md)"
281
+ else
282
+ echo -e " Handoff: ${YELLOW}active${NC} (.marathon/handoff.md)"
283
+ echo ""
284
+ echo -e " ${DIM}Next 'claude' session will auto-continue from this handoff.${NC}"
285
+ echo -e " ${DIM}Or run: marathon continue${NC}"
286
+ fi
287
+ else
288
+ echo -e " Handoff: ${DIM}none${NC}"
289
+ fi
290
+
291
+ # Check for run history
292
+ if [[ -f "$MARATHON_DIR/summary.json" ]]; then
293
+ local legs total_in total_out
294
+ legs=$(jq -r '.legs // "?"' "$MARATHON_DIR/summary.json" 2>/dev/null)
295
+ total_in=$(jq -r '.total_input_tokens // 0' "$MARATHON_DIR/summary.json" 2>/dev/null)
296
+ total_out=$(jq -r '.total_output_tokens // 0' "$MARATHON_DIR/summary.json" 2>/dev/null)
297
+ echo -e " Last run: ${legs} legs, ${total_in} in / ${total_out} out tokens"
298
+ fi
299
+
300
+ echo ""
301
+ exit 0
302
+ }
303
+
304
+ cmd_continue() {
305
+ if [[ ! -f "$MARATHON_DIR/handoff.md" ]]; then
306
+ log_error "No handoff file found at $MARATHON_DIR/handoff.md"
307
+ log_error "Nothing to continue."
308
+ exit 1
309
+ fi
310
+
311
+ # Read handoff as the task
312
+ local handoff_content
313
+ handoff_content=$(cat "$MARATHON_DIR/handoff.md")
314
+
315
+ TASK=$(cat <<EOF
316
+ You are continuing a task from a previous session. Here is the handoff:
317
+
318
+ ---
319
+ $handoff_content
320
+ ---
321
+
322
+ Pick up where the previous session left off. Follow the "Next Steps" above.
323
+ Update .marathon/handoff.md as you make progress.
324
+ When the task is fully complete, add "## Status: COMPLETE" at the top.
325
+ EOF
326
+ )
327
+
328
+ # Fall through to the main marathon loop below
329
+ }
330
+
331
+ log() {
332
+ [[ "$QUIET" == true ]] && return
333
+ echo -e "${DIM}[marathon]${NC} $*"
334
+ }
335
+
336
+ log_leg() {
337
+ echo -e "${BLUE}${BOLD}[marathon leg $1/$MAX_LEGS]${NC} $2"
338
+ }
339
+
340
+ log_done() {
341
+ echo -e "${GREEN}${BOLD}[marathon]${NC} $1"
342
+ }
343
+
344
+ log_warn() {
345
+ echo -e "${YELLOW}[marathon]${NC} $1"
346
+ }
347
+
348
+ log_error() {
349
+ echo -e "${RED}[marathon]${NC} $1" >&2
350
+ }
351
+
352
+ # The system prompt injected into each session
353
+ marathon_system_prompt() {
354
+ cat <<'PROMPT'
355
+ ## Marathon Auto-Continuation Protocol
356
+
357
+ You are running inside **marathon**, an auto-continuation wrapper. This means when your context gets large, you can hand off to a fresh session that will continue your work.
358
+
359
+ ### How it works:
360
+ 1. You do your work normally
361
+ 2. When you sense context is getting large (many tool calls, large file reads, deep into a task), proactively write a handoff file
362
+ 3. Your session will end (via max-turns or natural completion), and marathon will start a new session with your handoff
363
+
364
+ ### Writing a handoff:
365
+ When you need to hand off, write the file `.marathon/handoff.md` with this structure:
366
+
367
+ ```markdown
368
+ ## Task
369
+ [Original task / goal - what are we trying to achieve]
370
+
371
+ ## Progress
372
+ [What has been completed so far - be specific about files changed, decisions made]
373
+
374
+ ## Current State
375
+ [Where things stand right now - what was the last thing done]
376
+
377
+ ## Next Steps
378
+ [Exactly what needs to happen next - ordered list, be specific]
379
+
380
+ ## Key Context
381
+ [Important details the next session needs to know - file paths, patterns found, gotchas discovered, architectural decisions]
382
+ ```
383
+
384
+ ### Rules:
385
+ - Write the handoff **proactively** - don't wait until you're forced to stop
386
+ - Be specific in the handoff - the next session has NO memory of this one
387
+ - Include file paths, function names, error messages - anything concrete
388
+ - If the task is **fully complete**, write the handoff with `## Status: COMPLETE` at the top and summarize what was done
389
+ - Update the handoff file as you make progress, so it's always current even if the session ends unexpectedly
390
+ - The handoff file is your ONLY way to communicate with the next session
391
+
392
+ ### Important:
393
+ - You are leg MARATHON_LEG_NUMBER of up to MARATHON_MAX_LEGS legs
394
+ - Each leg has up to MARATHON_MAX_TURNS turns
395
+ - Don't rush - do quality work. Marathon gives you unlimited context.
396
+ - After ~70% of your turns, start writing/updating the handoff file
397
+ PROMPT
398
+ }
399
+
400
+ # Handle subcommands first
401
+ case "${1:-}" in
402
+ install) cmd_install ;;
403
+ uninstall) cmd_uninstall ;;
404
+ status) cmd_status ;;
405
+ continue) cmd_continue ;; # sets TASK, falls through to main loop
406
+ esac
407
+
408
+ # Parse arguments
409
+ TASK="${TASK:-}"
410
+ TASK_FILE=""
411
+
412
+ while [[ $# -gt 0 ]]; do
413
+ case $1 in
414
+ -t|--max-turns) MAX_TURNS="$2"; shift 2 ;;
415
+ -l|--max-legs) MAX_LEGS="$2"; shift 2 ;;
416
+ -b|--max-budget) MAX_BUDGET="$2"; shift 2 ;;
417
+ -m|--model) MODEL="$2"; shift 2 ;;
418
+ -p|--permission-mode) PERMISSION_MODE="$2"; shift 2 ;;
419
+ -a|--allowed-tools) ALLOWED_TOOLS="$2"; shift 2 ;;
420
+ -d|--work-dir) WORK_DIR="$2"; shift 2 ;;
421
+ -f|--file) TASK_FILE="$2"; shift 2 ;;
422
+ -q|--quiet) QUIET=true; shift ;;
423
+ --dry-run) DRY_RUN=true; shift ;;
424
+ -v|--version) echo "marathon $VERSION"; exit 0 ;;
425
+ -h|--help) usage ;;
426
+ -*) log_error "Unknown option: $1"; exit 1 ;;
427
+ *) TASK="$1"; shift ;;
428
+ esac
429
+ done
430
+
431
+ # Read task from file if specified
432
+ if [[ -n "$TASK_FILE" ]]; then
433
+ if [[ ! -f "$TASK_FILE" ]]; then
434
+ log_error "Task file not found: $TASK_FILE"
435
+ exit 1
436
+ fi
437
+ TASK=$(cat "$TASK_FILE")
438
+ fi
439
+
440
+ if [[ -z "$TASK" ]]; then
441
+ log_error "No task provided. Usage: marathon \"your task\""
442
+ echo "Run 'marathon --help' for options."
443
+ exit 1
444
+ fi
445
+
446
+ # Check dependencies
447
+ if ! command -v claude &>/dev/null; then
448
+ log_error "claude CLI not found. Install Claude Code first:"
449
+ log_error " https://docs.anthropic.com/en/docs/claude-code"
450
+ exit 1
451
+ fi
452
+
453
+ if ! command -v jq &>/dev/null; then
454
+ log_error "jq not found. Install it:"
455
+ log_error " brew install jq (macOS)"
456
+ log_error " apt install jq (Linux)"
457
+ exit 1
458
+ fi
459
+
460
+ # Setup
461
+ cd "$WORK_DIR"
462
+ mkdir -p "$MARATHON_DIR"
463
+
464
+ # Clean previous handoff
465
+ rm -f "$MARATHON_DIR/handoff.md"
466
+
467
+ # Write run metadata
468
+ cat > "$MARATHON_DIR/run.json" <<EOF
469
+ {
470
+ "task": $(echo "$TASK" | jq -Rs .),
471
+ "started_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
472
+ "max_turns": $MAX_TURNS,
473
+ "max_legs": $MAX_LEGS,
474
+ "version": "$VERSION"
475
+ }
476
+ EOF
477
+
478
+ # Banner
479
+ if [[ "$QUIET" != true ]]; then
480
+ echo ""
481
+ echo -e "${BOLD} marathon v${VERSION}${NC}"
482
+ echo -e " ${DIM}Infinite context sessions for Claude Code${NC}"
483
+ echo ""
484
+ echo -e " Task: ${TASK:0:80}$([ ${#TASK} -gt 80 ] && echo '...')"
485
+ echo -e " Max turns: $MAX_TURNS per leg"
486
+ echo -e " Max legs: $MAX_LEGS"
487
+ [[ -n "$MODEL" ]] && echo -e " Model: $MODEL"
488
+ echo ""
489
+ fi
490
+
491
+ # Build base claude command
492
+ build_claude_cmd() {
493
+ local leg_num=$1
494
+ local prompt="$2"
495
+ local cmd="claude -p"
496
+
497
+ # System prompt with leg number substituted
498
+ local sys_prompt
499
+ sys_prompt=$(marathon_system_prompt)
500
+ sys_prompt="${sys_prompt//MARATHON_LEG_NUMBER/$leg_num}"
501
+ sys_prompt="${sys_prompt//MARATHON_MAX_LEGS/$MAX_LEGS}"
502
+ sys_prompt="${sys_prompt//MARATHON_MAX_TURNS/$MAX_TURNS}"
503
+
504
+ cmd+=" --append-system-prompt $(printf '%q' "$sys_prompt")"
505
+ cmd+=" --max-turns $MAX_TURNS"
506
+ cmd+=" --output-format json"
507
+
508
+ [[ -n "$MODEL" ]] && cmd+=" --model $MODEL"
509
+ [[ -n "$MAX_BUDGET" ]] && cmd+=" --max-budget-usd $MAX_BUDGET"
510
+ [[ -n "$PERMISSION_MODE" ]] && cmd+=" --permission-mode $PERMISSION_MODE"
511
+ [[ -n "$ALLOWED_TOOLS" ]] && cmd+=" --allowedTools $ALLOWED_TOOLS"
512
+
513
+ cmd+=" $(printf '%q' "$prompt")"
514
+ echo "$cmd"
515
+ }
516
+
517
+ # Build the prompt for leg 1 (initial task)
518
+ build_initial_prompt() {
519
+ echo "$TASK"
520
+ }
521
+
522
+ # Build the prompt for continuation legs
523
+ build_continuation_prompt() {
524
+ local handoff_content
525
+ handoff_content=$(cat "$MARATHON_DIR/handoff.md")
526
+ cat <<EOF
527
+ You are continuing a task from a previous marathon session. Here is the handoff from the previous leg:
528
+
529
+ ---
530
+ $handoff_content
531
+ ---
532
+
533
+ Continue the work described above. Pick up exactly where the previous session left off.
534
+ Remember to update .marathon/handoff.md as you make progress.
535
+ EOF
536
+ }
537
+
538
+ # Check if the task is complete
539
+ is_task_complete() {
540
+ if [[ ! -f "$MARATHON_DIR/handoff.md" ]]; then
541
+ return 1
542
+ fi
543
+ # Check for COMPLETE status in handoff
544
+ if head -5 "$MARATHON_DIR/handoff.md" | grep -qi "status:.*complete"; then
545
+ return 0
546
+ fi
547
+ return 1
548
+ }
549
+
550
+ # Main loop
551
+ total_input_tokens=0
552
+ total_output_tokens=0
553
+ leg=1
554
+
555
+ while [[ $leg -le $MAX_LEGS ]]; do
556
+ log_leg "$leg" "Starting..."
557
+
558
+ # Build prompt
559
+ if [[ $leg -eq 1 ]]; then
560
+ prompt=$(build_initial_prompt)
561
+ else
562
+ if [[ ! -f "$MARATHON_DIR/handoff.md" ]]; then
563
+ log_warn "No handoff file found. Assuming task is complete."
564
+ break
565
+ fi
566
+ prompt=$(build_continuation_prompt)
567
+ fi
568
+
569
+ # Build command
570
+ cmd=$(build_claude_cmd "$leg" "$prompt")
571
+
572
+ if [[ "$DRY_RUN" == true ]]; then
573
+ echo -e "${DIM}Would run:${NC}"
574
+ echo "$cmd"
575
+ exit 0
576
+ fi
577
+
578
+ # Run claude and capture output
579
+ # We use a temp file because the output can be large
580
+ output_file=$(mktemp)
581
+ trap "rm -f $output_file" EXIT
582
+
583
+ log "Running claude..."
584
+ set +e
585
+ eval "$cmd" > "$output_file" 2>"$MARATHON_DIR/leg-${leg}-stderr.log"
586
+ exit_code=$?
587
+ set -e
588
+
589
+ # Parse output
590
+ if [[ -s "$output_file" ]]; then
591
+ # Extract the last valid JSON object (claude may output multiple)
592
+ result=$(tail -1 "$output_file")
593
+
594
+ # Try to parse usage
595
+ input_tokens=$(echo "$result" | jq -r '.usage.input_tokens // 0' 2>/dev/null || echo 0)
596
+ output_tokens=$(echo "$result" | jq -r '.usage.output_tokens // 0' 2>/dev/null || echo 0)
597
+ session_id=$(echo "$result" | jq -r '.session_id // "unknown"' 2>/dev/null || echo "unknown")
598
+ stop_reason=$(echo "$result" | jq -r '.stop_reason // "unknown"' 2>/dev/null || echo "unknown")
599
+ response_text=$(echo "$result" | jq -r '.result // ""' 2>/dev/null || echo "")
600
+
601
+ total_input_tokens=$((total_input_tokens + input_tokens))
602
+ total_output_tokens=$((total_output_tokens + output_tokens))
603
+
604
+ # Save leg result
605
+ echo "$result" > "$MARATHON_DIR/leg-${leg}-result.json"
606
+
607
+ log "Leg $leg complete (tokens: ${input_tokens}in/${output_tokens}out, stop: ${stop_reason})"
608
+ else
609
+ log_warn "No output from claude (exit code: $exit_code)"
610
+ if [[ -s "$MARATHON_DIR/leg-${leg}-stderr.log" ]]; then
611
+ log_error "stderr: $(cat "$MARATHON_DIR/leg-${leg}-stderr.log")"
612
+ fi
613
+ fi
614
+
615
+ rm -f "$output_file"
616
+
617
+ # Check if task is complete
618
+ if is_task_complete; then
619
+ log_done "Task complete after $leg leg(s)!"
620
+ log "Total tokens: ${total_input_tokens} in / ${total_output_tokens} out"
621
+
622
+ # Show completion summary from handoff
623
+ if [[ -f "$MARATHON_DIR/handoff.md" ]]; then
624
+ echo ""
625
+ echo -e "${GREEN}${BOLD}--- Completion Summary ---${NC}"
626
+ cat "$MARATHON_DIR/handoff.md"
627
+ echo ""
628
+ fi
629
+ break
630
+ fi
631
+
632
+ # Check if we've hit max legs
633
+ if [[ $leg -ge $MAX_LEGS ]]; then
634
+ log_warn "Reached maximum legs ($MAX_LEGS). Task may be incomplete."
635
+ log_warn "Check $MARATHON_DIR/handoff.md for current state."
636
+ break
637
+ fi
638
+
639
+ # Check if handoff exists for continuation
640
+ if [[ ! -f "$MARATHON_DIR/handoff.md" ]]; then
641
+ # No handoff and not complete - Claude didn't write one
642
+ # This could mean the task finished without explicit COMPLETE status
643
+ # or Claude ran out of turns before writing handoff
644
+ if [[ "$stop_reason" == "max_turns" ]]; then
645
+ log_warn "Hit max turns without handoff. Starting recovery leg..."
646
+ # Create a minimal handoff for recovery
647
+ cat > "$MARATHON_DIR/handoff.md" <<RECOVERY
648
+ ## Task
649
+ $TASK
650
+
651
+ ## Progress
652
+ Previous session (leg $leg) ran out of turns before writing a handoff.
653
+
654
+ ## Current State
655
+ Unknown - the previous session ended abruptly.
656
+
657
+ ## Next Steps
658
+ 1. Assess what has been done by checking recent file changes (git status/diff)
659
+ 2. Continue the original task
660
+ 3. Write a proper handoff file early this time
661
+
662
+ ## Key Context
663
+ This is a recovery leg. Check the codebase state to understand progress.
664
+ RECOVERY
665
+ else
666
+ log_done "Session ended naturally without handoff. Assuming complete."
667
+ log "Total tokens: ${total_input_tokens} in / ${total_output_tokens} out"
668
+ break
669
+ fi
670
+ fi
671
+
672
+ log "Handoff found. Continuing to next leg..."
673
+ echo ""
674
+ leg=$((leg + 1))
675
+ done
676
+
677
+ # Write final run summary
678
+ cat > "$MARATHON_DIR/summary.json" <<EOF
679
+ {
680
+ "task": $(echo "$TASK" | jq -Rs .),
681
+ "started_at": $(jq -r '.started_at' "$MARATHON_DIR/run.json" | jq -Rs .),
682
+ "finished_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
683
+ "legs": $leg,
684
+ "total_input_tokens": $total_input_tokens,
685
+ "total_output_tokens": $total_output_tokens,
686
+ "version": "$VERSION"
687
+ }
688
+ EOF
689
+
690
+ log "Run summary saved to $MARATHON_DIR/summary.json"
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env node
2
+ // marathon-hooks.js — Auto-continuation hooks for Claude Code
3
+ //
4
+ // Handles multiple hook events:
5
+ // PreCompact → Inject marathon protocol, tell Claude to write handoff
6
+ // SessionStart → Detect existing handoff, inject continuation context
7
+ // Stop → After Claude responds, check if handoff says COMPLETE
8
+ //
9
+ // Install via: marathon install
10
+ // Zero overhead for normal sessions — only activates when needed.
11
+
12
+ const fs = require('fs');
13
+ const os = require('os');
14
+ const path = require('path');
15
+
16
+ const MARATHON_DIR = '.marathon';
17
+ const HANDOFF_FILE = path.join(MARATHON_DIR, 'handoff.md');
18
+
19
+ let input = '';
20
+ const stdinTimeout = setTimeout(() => process.exit(0), 3000);
21
+ process.stdin.setEncoding('utf8');
22
+ process.stdin.on('data', chunk => input += chunk);
23
+ process.stdin.on('end', () => {
24
+ clearTimeout(stdinTimeout);
25
+ try {
26
+ const data = JSON.parse(input);
27
+ const cwd = data.cwd || process.cwd();
28
+ const hookEvent = data.hook_event_name || process.argv[2] || '';
29
+
30
+ switch (hookEvent) {
31
+ case 'PreCompact':
32
+ handlePreCompact(data, cwd);
33
+ break;
34
+ case 'SessionStart':
35
+ handleSessionStart(data, cwd);
36
+ break;
37
+ case 'Stop':
38
+ handleStop(data, cwd);
39
+ break;
40
+ default:
41
+ process.exit(0);
42
+ }
43
+ } catch (e) {
44
+ process.exit(0);
45
+ }
46
+ });
47
+
48
+ // ─── PreCompact ──────────────────────────────────────────────────────────────
49
+ // Context is about to be compacted. This is our signal that the session is big.
50
+ // Inject the marathon protocol and tell Claude to write a handoff.
51
+ function handlePreCompact(data, cwd) {
52
+ // Create .marathon dir if it doesn't exist
53
+ const marathonPath = path.join(cwd, MARATHON_DIR);
54
+ try { fs.mkdirSync(marathonPath, { recursive: true }); } catch (e) {}
55
+
56
+ // Check if handoff already exists (Claude already knows about marathon)
57
+ const handoffPath = path.join(cwd, HANDOFF_FILE);
58
+ const handoffExists = fs.existsSync(handoffPath);
59
+
60
+ let message;
61
+ if (handoffExists) {
62
+ message = [
63
+ 'MARATHON: Context is being compacted again. UPDATE your handoff file at .marathon/handoff.md',
64
+ 'with current progress, state, and next steps. This ensures nothing is lost during compaction.',
65
+ 'Keep working after updating — this is not the end of the session.'
66
+ ].join(' ');
67
+ } else {
68
+ message = [
69
+ 'MARATHON AUTO-CONTINUATION ACTIVATED: Context is large enough to trigger compaction.',
70
+ 'Write a handoff file NOW at .marathon/handoff.md so your progress survives if the session ends.',
71
+ '',
72
+ 'Format:',
73
+ '```',
74
+ '## Task',
75
+ '[What we\'re trying to achieve]',
76
+ '',
77
+ '## Progress',
78
+ '[What\'s been completed - files changed, decisions made]',
79
+ '',
80
+ '## Current State',
81
+ '[Where things stand right now]',
82
+ '',
83
+ '## Next Steps',
84
+ '[Ordered list of what needs to happen next]',
85
+ '',
86
+ '## Key Context',
87
+ '[File paths, patterns, gotchas, architectural decisions]',
88
+ '```',
89
+ '',
90
+ 'Rules:',
91
+ '- Update this file as you make progress',
92
+ '- Be specific — the next session has NO memory of this one',
93
+ '- When task is fully complete, add "## Status: COMPLETE" at the top',
94
+ '- Keep working after writing the handoff — this is a checkpoint, not a stop signal',
95
+ '',
96
+ 'If this session ends, marathon will start a fresh session with your handoff.'
97
+ ].join('\n');
98
+ }
99
+
100
+ const output = {
101
+ hookSpecificOutput: {
102
+ hookEventName: 'PreCompact',
103
+ additionalContext: message
104
+ }
105
+ };
106
+ process.stdout.write(JSON.stringify(output));
107
+ }
108
+
109
+ // ─── SessionStart ────────────────────────────────────────────────────────────
110
+ // Check if there's a handoff file from a previous session.
111
+ // If so, inject continuation context so Claude picks up where it left off.
112
+ function handleSessionStart(data, cwd) {
113
+ const handoffPath = path.join(cwd, HANDOFF_FILE);
114
+
115
+ if (!fs.existsSync(handoffPath)) {
116
+ process.exit(0);
117
+ return;
118
+ }
119
+
120
+ let handoff;
121
+ try {
122
+ handoff = fs.readFileSync(handoffPath, 'utf8').trim();
123
+ } catch (e) {
124
+ process.exit(0);
125
+ return;
126
+ }
127
+
128
+ if (!handoff) {
129
+ process.exit(0);
130
+ return;
131
+ }
132
+
133
+ // Check if it's a completed task (don't auto-continue completed work)
134
+ const firstLines = handoff.split('\n').slice(0, 5).join('\n').toLowerCase();
135
+ if (firstLines.includes('status: complete')) {
136
+ // Rename to archive instead of continuing
137
+ const archiveName = `handoff-complete-${Date.now()}.md`;
138
+ try {
139
+ fs.renameSync(handoffPath, path.join(cwd, MARATHON_DIR, archiveName));
140
+ } catch (e) {}
141
+ process.exit(0);
142
+ return;
143
+ }
144
+
145
+ // Active handoff found — inject continuation context
146
+ const message = [
147
+ 'MARATHON CONTINUATION: A previous session left a handoff file. Here is the context:',
148
+ '',
149
+ '---',
150
+ handoff,
151
+ '---',
152
+ '',
153
+ 'Pick up where the previous session left off. Follow the "Next Steps" above.',
154
+ 'Update .marathon/handoff.md as you make progress.',
155
+ 'When the task is fully complete, add "## Status: COMPLETE" at the top of the handoff.'
156
+ ].join('\n');
157
+
158
+ const output = {
159
+ hookSpecificOutput: {
160
+ hookEventName: 'SessionStart',
161
+ additionalContext: message
162
+ }
163
+ };
164
+ process.stdout.write(JSON.stringify(output));
165
+ }
166
+
167
+ // ─── Stop ────────────────────────────────────────────────────────────────────
168
+ // After Claude finishes responding, check if the handoff says COMPLETE.
169
+ // If so, archive it so the next session starts fresh.
170
+ function handleStop(data, cwd) {
171
+ const handoffPath = path.join(cwd, HANDOFF_FILE);
172
+
173
+ if (!fs.existsSync(handoffPath)) {
174
+ process.exit(0);
175
+ return;
176
+ }
177
+
178
+ let handoff;
179
+ try {
180
+ handoff = fs.readFileSync(handoffPath, 'utf8').trim();
181
+ } catch (e) {
182
+ process.exit(0);
183
+ return;
184
+ }
185
+
186
+ const firstLines = handoff.split('\n').slice(0, 5).join('\n').toLowerCase();
187
+ if (firstLines.includes('status: complete')) {
188
+ // Archive the completed handoff
189
+ const archiveName = `handoff-complete-${Date.now()}.md`;
190
+ const marathonPath = path.join(cwd, MARATHON_DIR);
191
+ try {
192
+ fs.renameSync(handoffPath, path.join(marathonPath, archiveName));
193
+ } catch (e) {}
194
+ }
195
+
196
+ process.exit(0);
197
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "claude-marathon",
3
+ "version": "0.2.0",
4
+ "description": "Infinite context sessions for Claude Code. Auto-continues when context gets large.",
5
+ "bin": {
6
+ "marathon": "./bin/marathon"
7
+ },
8
+ "files": [
9
+ "bin/",
10
+ "hooks/",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "scripts": {
15
+ "test": "echo \"Tests coming soon\" && exit 0"
16
+ },
17
+ "keywords": [
18
+ "claude",
19
+ "claude-code",
20
+ "context",
21
+ "continuation",
22
+ "handoff",
23
+ "ai",
24
+ "cli",
25
+ "anthropic",
26
+ "infinite-context",
27
+ "session-management",
28
+ "developer-tools"
29
+ ],
30
+ "author": "",
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/josephtandle/claude-marathon.git"
35
+ },
36
+ "homepage": "https://github.com/josephtandle/claude-marathon#readme",
37
+ "bugs": {
38
+ "url": "https://github.com/josephtandle/claude-marathon/issues"
39
+ },
40
+ "engines": {
41
+ "node": ">=16.0.0"
42
+ }
43
+ }