claudenv 1.2.4 → 1.2.5

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # claudenv
2
2
 
3
- One command to set up [Claude Code](https://docs.anthropic.com/en/docs/claude-code) in any project. Claude AI analyzes your codebase and generates all the documentation it needs to work effectively.
3
+ Set up [Claude Code](https://docs.anthropic.com/en/docs/claude-code) in any project with one command. claudenv analyzes your codebase and generates everything Claude needs to work effectively — documentation, rules, hooks, MCP servers, and slash commands.
4
4
 
5
5
  ## Quick Start
6
6
 
@@ -8,244 +8,222 @@ One command to set up [Claude Code](https://docs.anthropic.com/en/docs/claude-co
8
8
  npm i -g claudenv && claudenv
9
9
  ```
10
10
 
11
- Done. Open Claude Code in any project and type `/claudenv`.
11
+ Open Claude Code in any project and type `/claudenv`. That's it.
12
12
 
13
- ## How It Works
13
+ ## What happens when you run `/claudenv`
14
14
 
15
- **One-time setup** install and activate:
15
+ Claude reads your code, asks a few questions, and generates:
16
16
 
17
- ```bash
18
- npm i -g claudenv && claudenv
19
- ```
20
-
21
- This installs the `/claudenv` command globally into `~/.claude/`, making it available in every project.
22
-
23
- **In any project** — open Claude Code and type `/claudenv`.
17
+ - **CLAUDE.md** — project overview, architecture, key commands
18
+ - **Rules** coding style, testing patterns, workflow guidelines (`.claude/rules/`)
19
+ - **MCP servers** — auto-detected from your stack, configured in `.mcp.json`
20
+ - **Slash commands** — `/init-docs`, `/update-docs`, `/validate-docs`, `/setup-mcp`, `/improve`
21
+ - **Hooks** validation on tool use, audit logging (`.claude/settings.json`)
24
22
 
25
- Claude AI will:
26
- 1. Read your manifest files, configs, and source code
27
- 2. Detect your tech stack, frameworks, and tooling
28
- 3. Ask you about the project (description, deployment, conventions)
29
- 4. Generate all documentation files
30
- 5. Search the [MCP Registry](https://registry.modelcontextprotocol.io) and configure MCP servers
31
- 6. Install slash commands for ongoing maintenance
23
+ Everything is committed to your repo. Team members get the same Claude experience.
32
24
 
33
- You now have five commands available in Claude Code:
25
+ ## Autonomous Loop
34
26
 
35
- | Command | What it does |
36
- |---------|-------------|
37
- | `/init-docs` | Regenerate documentation from scratch |
38
- | `/update-docs` | Scan for changes and propose updates |
39
- | `/validate-docs` | Check that documentation is complete and correct |
40
- | `/setup-mcp` | Recommend and configure MCP servers |
41
- | `/improve` | Analyze and make one improvement |
27
+ The killer feature. `claudenv loop` runs Claude in headless mode, iterating over your project — planning improvements, implementing them one by one, committing each step.
42
28
 
43
- ## What Gets Generated
44
-
45
- ```
46
- your-project/
47
- ├── CLAUDE.md # Project overview, commands, architecture
48
- ├── _state.md # Session memory (decisions, focus, issues)
49
- ├── .mcp.json # MCP server configuration
50
- └── .claude/
51
- ├── rules/
52
- │ ├── code-style.md # Coding conventions (scoped by file paths)
53
- │ ├── testing.md # Test patterns and commands
54
- │ └── workflow.md # Claude Code best practices
55
- ├── settings.json # Validation hooks
56
- ├── commands/
57
- │ ├── init-docs.md # /init-docs
58
- │ ├── update-docs.md # /update-docs
59
- │ ├── validate-docs.md # /validate-docs
60
- │ ├── setup-mcp.md # /setup-mcp
61
- │ └── improve.md # /improve
62
- ├── skills/
63
- │ └── doc-generator/ # Auto-triggers when docs need updating
64
- └── agents/
65
- └── doc-analyzer.md # Read-only analysis subagent
29
+ ```bash
30
+ claudenv loop --goal "add test coverage" --trust -n 5
66
31
  ```
67
32
 
68
- ### Key Files
33
+ **What it does:**
69
34
 
70
- | File | Purpose |
71
- |------|---------|
72
- | `CLAUDE.md` | Compact project overview with `@import` references to rule files |
73
- | `_state.md` | Persists context between sessions current focus, decisions, known issues |
74
- | `.claude/rules/workflow.md` | Best practices: plan mode, `/compact`, subagents, git discipline |
75
- | `.claude/rules/code-style.md` | Language and framework-specific coding conventions |
76
- | `.claude/rules/testing.md` | Test framework patterns and commands |
77
- | `.mcp.json` | MCP server configuration with `${ENV_VAR}` placeholders |
35
+ 1. Creates a git safety tag (rollback anytime with `--rollback`)
36
+ 2. Claude generates an improvement plan (`.claude/improvement-plan.md`)
37
+ 3. Each iteration picks the next item, implements it, runs tests, commits
38
+ 4. Stops when the plan is done, iterations run out, or it detects it's stuck
78
39
 
79
- ## MCP Server Recommendations
40
+ ### Common recipes
80
41
 
81
- `/claudenv` automatically recommends MCP servers based on your tech stack. You can also run `/setup-mcp` independently at any time.
42
+ ```bash
43
+ # Interactive mode — pauses between iterations so you can review
44
+ claudenv loop
82
45
 
83
- **How it works:**
46
+ # Fully autonomous — no pauses, no permission prompts
47
+ claudenv loop --trust
84
48
 
85
- 1. Claude analyzes your project's dependencies, databases, cloud services, and tools
86
- 2. Searches the [official MCP Registry](https://registry.modelcontextprotocol.io) for matching servers
87
- 3. Verifies trust via npm download counts (filters out servers with <100 monthly downloads)
88
- 4. Presents recommendations grouped as **Essential** / **Recommended** / **Optional**
89
- 5. Generates `.mcp.json` with selected servers
49
+ # Goal-driven with Opus for max capability
50
+ claudenv loop --goal "refactor auth to JWT" --trust --model opus -n 3
90
51
 
91
- **Example output** (`.mcp.json`):
52
+ # Budget-conscious CI run
53
+ claudenv loop --profile ci --goal "fix lint errors" -n 10
92
54
 
93
- ```json
94
- {
95
- "mcpServers": {
96
- "context7": {
97
- "command": "npx",
98
- "args": ["-y", "@upstash/context7-mcp@latest"],
99
- "env": {}
100
- },
101
- "postgres": {
102
- "command": "npx",
103
- "args": ["-y", "@modelcontextprotocol/server-postgres@latest", "${POSTGRES_CONNECTION_STRING}"],
104
- "env": {}
105
- }
106
- }
107
- }
55
+ # Undo everything from the last loop
56
+ claudenv loop --rollback
108
57
  ```
109
58
 
110
- Secrets use `${ENV_VAR}` placeholders — configure them with:
59
+ ### Rate limit recovery
60
+
61
+ If Claude hits API rate limits mid-loop, claudenv saves your progress automatically:
111
62
 
112
63
  ```bash
113
- claude config set env.POSTGRES_CONNECTION_STRING "postgresql://..."
114
- ```
64
+ # Rate limited? Just resume where you left off
65
+ claudenv loop --resume
115
66
 
116
- `.mcp.json` is safe to commit it never contains actual secrets.
67
+ # Override model on resume (e.g., switch to cheaper model)
68
+ claudenv loop --resume --model sonnet
69
+ ```
117
70
 
118
- Run `/setup-mcp --force` to auto-select Essential + Recommended servers without prompting.
71
+ ### Live progress tracking
119
72
 
120
- ## Iterative Improvement Loop
73
+ Monitor what Claude is doing in real time:
121
74
 
122
- `claudenv loop` spawns Claude Code in headless mode to analyze and improve your project iteratively. Each run creates a git safety tag so you can always rollback.
75
+ ```bash
76
+ # In another terminal — tail -f style
77
+ claudenv report --follow
123
78
 
124
- **How it works:**
79
+ # Summary of the last loop run
80
+ claudenv report
125
81
 
126
- 1. **Planning** (iteration 0) — Claude analyzes the project and generates `.claude/improvement-plan.md` with prioritized hypotheses
127
- 2. **Execution** (iterations 1-N) — each iteration picks the top unfinished item from the plan, implements it, runs tests, and commits
128
- 3. **Convergence** — the loop stops when the plan is complete, max iterations are reached, or the loop detects it's stuck
82
+ # Last 5 events only
83
+ claudenv report --last 5
84
+ ```
129
85
 
130
- **Usage:**
86
+ Events are stored in `.claude/work-report.jsonl` — machine-readable JSONL format.
131
87
 
132
- ```bash
133
- claudenv loop # Interactive, pauses between iterations
134
- claudenv loop --trust # Full trust, no pauses, no permission prompts
135
- claudenv loop --trust -n 5 # 5 iterations in full trust
136
- claudenv loop --goal "add test coverage" # Focused improvement
137
- claudenv loop --trust --model opus -n 3 # Use Opus, 3 iterations
138
- claudenv loop --budget 1.00 -n 10 # Budget cap per iteration
139
- claudenv loop --rollback # Undo all loop changes
140
- ```
88
+ ### All loop flags
141
89
 
142
90
  | Flag | Description |
143
91
  |------|-------------|
144
- | `-n, --iterations <n>` | Max iterations (default: unlimited) |
92
+ | `--goal <text>` | What to work on (any goal — Claude interprets it) |
145
93
  | `--trust` | Full trust mode — no pauses, skip permission prompts |
146
- | `--goal <text>` | Focus area for improvements |
147
- | `--profile <name>` | Autonomy profile: safe, moderate, full, ci |
148
- | `--no-pause` | Don't pause between iterations |
149
- | `--max-turns <n>` | Max agentic turns per iteration (default: 30) |
150
- | `--model <model>` | Model to use (default: sonnet) |
94
+ | `-n, --iterations <n>` | Max iterations (default: unlimited) |
95
+ | `--model <model>` | Model: `opus`, `sonnet`, `haiku` |
96
+ | `--profile <name>` | Autonomy profile (sets model, trust, budget) |
151
97
  | `--budget <usd>` | Budget cap per iteration in USD |
152
- | `-d, --dir <path>` | Target project directory |
153
- | `--allow-dirty` | Allow running with uncommitted changes |
98
+ | `--max-turns <n>` | Max agentic turns per iteration (default: 30) |
99
+ | `--resume` | Continue from last rate-limited loop |
154
100
  | `--rollback` | Undo all changes from the most recent loop |
155
- | `--unsafe` | Remove default tool restrictions |
156
-
157
- **Git safety:** Before the first iteration, a `claudenv-loop-<timestamp>` git tag is created. Each iteration commits separately. Use `claudenv loop --rollback` to reset everything, or cherry-pick individual commits.
158
-
159
- **Single iteration:** Use `/improve` inside Claude Code for a one-shot improvement without the full loop.
160
-
161
- ## Autonomous Agent Mode
101
+ | `--worktree` | Run each iteration in an isolated git worktree |
102
+ | `--allow-dirty` | Allow running with uncommitted changes |
103
+ | `--no-pause` | Don't pause between iterations |
104
+ | `--unsafe` | Remove default tool restrictions (allows rm -rf) |
105
+ | `-d, --dir <path>` | Target project directory |
162
106
 
163
- `claudenv autonomy` configures Claude Code for autonomous operation with predefined security profiles. Inspired by [trailofbits/claude-code-config](https://github.com/trailofbits/claude-code-config).
107
+ ## Autonomy Profiles
164
108
 
165
- **Usage:**
109
+ Control how much freedom Claude gets. Profiles configure permissions, hooks, model defaults, and safety guardrails.
166
110
 
167
111
  ```bash
168
- claudenv autonomy # Interactive profile selection
169
- claudenv autonomy --profile moderate # Non-interactive, writes files directly
170
- claudenv autonomy --profile moderate -y # Same -y only needed for profile selection
171
- claudenv autonomy --profile ci --dry-run # Preview generated files
172
- claudenv autonomy --profile full # Full autonomy — requires typing "full" to confirm
173
- claudenv autonomy --profile full --yes # Full autonomy, skip confirmation
112
+ claudenv autonomy # Interactive selection
113
+ claudenv autonomy --profile moderate # Apply directly
114
+ claudenv autonomy --profile ci --dry-run # Preview without writing
174
115
  ```
175
116
 
176
- ### Profiles
117
+ ### Profile comparison
177
118
 
178
- | Profile | Description | Permissions | Credentials |
179
- |---------|-------------|-------------|-------------|
180
- | `safe` | Read-only + limited bash | Allow-list only | Blocked |
181
- | `moderate` | Full dev with deny-list | Allow + deny lists | Blocked |
182
- | `full` | Unrestricted + audit logging | `--dangerously-skip-permissions` | Warn-only |
183
- | `ci` | Headless CI/CD (50 turns, $5 budget) | `--dangerously-skip-permissions` | Warn-only |
119
+ | Profile | Model | Permissions | Credentials | Use case |
120
+ |---------|-------|-------------|-------------|----------|
121
+ | **safe** | sonnet | Allow-list only (read + limited bash) | Blocked | Exploring unfamiliar codebases |
122
+ | **moderate** | sonnet | Allow + deny lists (full dev tools) | Blocked | Day-to-day development |
123
+ | **full** | opus | Unrestricted (`--dangerously-skip-permissions`) | Warn-only | Maximum capability runs |
124
+ | **ci** | haiku | Unrestricted + 50 turn / $5 budget limits | Warn-only | CI/CD pipelines |
184
125
 
185
- All profiles hard-block `rm -rf`, force push to main/master, and `sudo`.
126
+ All profiles hard-block `rm -rf`, force push to main/master, and `sudo` — regardless of permission settings.
186
127
 
187
- ### Generated files
128
+ ### What gets generated
188
129
 
189
130
  ```
190
131
  .claude/
191
- ├── settings.json # Permissions + hooks config
132
+ ├── settings.json # Permissions, hooks config
192
133
  ├── hooks/
193
- │ ├── pre-tool-use.sh # PreToolUse guard (blocks dangerous ops)
194
- │ └── audit-log.sh # PostToolUse audit audit-log.jsonl
195
- └── aliases.sh # Shell aliases: claude-safe, claude-yolo, claude-ci, claude-local
134
+ │ ├── pre-tool-use.sh # Blocks dangerous operations (reads stdin JSON from Claude Code)
135
+ │ └── audit-log.sh # Logs every tool call to audit-log.jsonl
136
+ └── aliases.sh # Shell aliases: claude-safe, claude-yolo, claude-ci, claude-local
196
137
  ```
197
138
 
198
139
  CI profile also generates `.github/workflows/claude-ci.yml`.
199
140
 
200
- ### Loop integration
141
+ ### Using profiles with the loop
201
142
 
202
- Use `--profile` with `claudenv loop` to apply profile settings:
143
+ Profiles set sensible defaults for model, trust, and budget:
203
144
 
204
145
  ```bash
205
- claudenv loop --profile moderate --goal "add types" # Uses profile's deny-list
206
- claudenv loop --profile ci -n 5 # CI defaults: 50 turns, $5 budget
146
+ claudenv loop --profile ci --goal "fix lint errors" # haiku, $5 budget, 50 turns
147
+ claudenv loop --profile full --goal "major refactor" # opus, unrestricted
148
+ claudenv loop --profile moderate --goal "add types" # sonnet, deny-list guarded
149
+
150
+ # CLI flags always override profile defaults
151
+ claudenv loop --profile ci --model sonnet # ci profile but with sonnet
207
152
  ```
208
153
 
209
- | Flag | Description |
210
- |------|-------------|
211
- | `-p, --profile <name>` | Profile: safe, moderate, full, ci |
212
- | `-d, --dir <path>` | Project directory (default: `.`) |
213
- | `--overwrite` | Overwrite existing files |
214
- | `-y, --yes` | Skip prompts |
215
- | `--dry-run` | Preview without writing |
154
+ ## MCP Server Setup
216
155
 
217
- ## Tech Stack Detection
156
+ `/claudenv` auto-detects your tech stack and recommends MCP servers from the [official registry](https://registry.modelcontextprotocol.io). You can also run `/setup-mcp` independently.
218
157
 
219
- Claude AI reads your actual code, but the following are auto-detected for context:
158
+ Servers are configured in `.mcp.json` with `${ENV_VAR}` placeholders safe to commit:
220
159
 
221
- - **Languages**: TypeScript, JavaScript, Python, Go, Rust, Ruby, PHP, Java, Kotlin, C#
222
- - **Frameworks**: Next.js, Vite, Nuxt, SvelteKit, Astro, Angular, Django, FastAPI, Flask, Rails, Laravel, Spring Boot, and more
223
- - **Package managers**: npm, yarn, pnpm, bun, poetry, pipenv, uv, cargo, go modules
224
- - **Test frameworks**: Vitest, Jest, Playwright, Cypress, pytest, RSpec, go test, cargo test
225
- - **CI/CD**: GitHub Actions, GitLab CI, Jenkins, CircleCI
226
- - **Linters/formatters**: ESLint, Biome, Prettier, Ruff, RuboCop, golangci-lint, Clippy
160
+ ```json
161
+ {
162
+ "mcpServers": {
163
+ "context7": {
164
+ "command": "npx",
165
+ "args": ["-y", "@upstash/context7-mcp@latest"]
166
+ },
167
+ "postgres": {
168
+ "command": "npx",
169
+ "args": ["-y", "@modelcontextprotocol/server-postgres@latest", "${POSTGRES_CONNECTION_STRING}"]
170
+ }
171
+ }
172
+ }
173
+ ```
174
+
175
+ Set secrets with `claude config set env.POSTGRES_CONNECTION_STRING "postgresql://..."`.
176
+
177
+ ## File Structure
178
+
179
+ After full setup (`/claudenv` + `claudenv autonomy`):
180
+
181
+ ```
182
+ your-project/
183
+ ├── CLAUDE.md # Project overview for Claude
184
+ ├── _state.md # Session memory (persists between conversations)
185
+ ├── .mcp.json # MCP server configuration
186
+ └── .claude/
187
+ ├── settings.json # Permissions + hooks
188
+ ├── rules/
189
+ │ ├── code-style.md # Coding conventions
190
+ │ ├── testing.md # Test patterns
191
+ │ └── workflow.md # Claude workflow best practices
192
+ ├── hooks/
193
+ │ ├── pre-tool-use.sh # Safety guardrails
194
+ │ └── audit-log.sh # Audit logging
195
+ ├── commands/ # Slash commands
196
+ │ ├── init-docs.md
197
+ │ ├── update-docs.md
198
+ │ ├── validate-docs.md
199
+ │ ├── setup-mcp.md
200
+ │ └── improve.md
201
+ ├── aliases.sh # Shell aliases
202
+ ├── work-report.jsonl # Loop progress events
203
+ ├── loop-log.json # Loop state (for resume/rollback)
204
+ ├── improvement-plan.md # Current loop plan
205
+ └── audit-log.jsonl # Tool call audit trail
206
+ ```
227
207
 
228
208
  ## CLI Reference
229
209
 
230
210
  ```
231
- claudenv Install /claudenv into ~/.claude/ (default)
232
- claudenv install Same as above (explicit)
233
- claudenv install -f Reinstall, overwriting existing files
234
- claudenv uninstall Remove /claudenv from ~/.claude/
235
- claudenv init [dir] Legacy: static analysis + terminal prompts (no AI)
236
- claudenv init -y Legacy: skip prompts, auto-detect everything
237
- claudenv generate Templates only, no scaffold
238
- claudenv validate Check documentation completeness
239
- claudenv loop Iterative improvement loop (spawns Claude)
240
- claudenv loop --trust Full trust mode, no pauses
241
- claudenv loop --profile moderate Use autonomy profile in loop
242
- claudenv loop --rollback Undo all loop changes
243
- claudenv autonomy Configure autonomous agent mode (interactive)
244
- claudenv autonomy -p moderate Apply moderate profile
245
- claudenv autonomy -p ci --dry-run Preview CI config without writing
211
+ claudenv Install /claudenv into ~/.claude/
212
+ claudenv install [-f] Same as above (-f to overwrite)
213
+ claudenv uninstall Remove from ~/.claude/
214
+
215
+ claudenv loop [options] Autonomous improvement loop
216
+ claudenv loop --resume Resume rate-limited loop
217
+ claudenv loop --rollback Undo all loop changes
218
+ claudenv report [--follow] [--last n] View loop progress
219
+
220
+ claudenv autonomy [-p <profile>] Configure autonomy profiles
221
+ claudenv init [dir] [-y] Legacy: static analysis (no AI)
222
+ claudenv generate [-d <dir>] Templates only, no scaffold
223
+ claudenv validate [-d <dir>] Check documentation completeness
246
224
  ```
247
225
 
248
- ## Alternative: Run Without Installing
226
+ ## Run Without Installing
249
227
 
250
228
  ```bash
251
229
  npx claudenv # npm
@@ -253,12 +231,9 @@ pnpm dlx claudenv # pnpm
253
231
  bunx claudenv # bun
254
232
  ```
255
233
 
256
- ## Uninstall
234
+ ## Tech Stack Detection
257
235
 
258
- ```bash
259
- claudenv uninstall # Remove from ~/.claude/
260
- npm uninstall -g claudenv
261
- ```
236
+ Auto-detected for context: TypeScript, JavaScript, Python, Go, Rust, Ruby, PHP, Java, Kotlin, C# / Next.js, Vite, Nuxt, SvelteKit, Astro, Django, FastAPI, Flask, Rails, Laravel, Spring Boot / npm, yarn, pnpm, bun, poetry, uv, cargo / Vitest, Jest, Playwright, pytest, RSpec / GitHub Actions, GitLab CI / ESLint, Biome, Prettier, Ruff, Clippy.
262
237
 
263
238
  ## Requirements
264
239
 
package/bin/cli.js CHANGED
@@ -10,9 +10,10 @@ import { generateDocs, writeDocs, installScaffold } from '../src/generator.js';
10
10
  import { validateClaudeMd, validateStructure, crossReferenceCheck } from '../src/validator.js';
11
11
  import { runExistingProjectFlow, runColdStartFlow, buildDefaultConfig } from '../src/prompts.js';
12
12
  import { installGlobal, uninstallGlobal } from '../src/installer.js';
13
- import { runLoop, rollback, checkClaudeCli } from '../src/loop.js';
13
+ import { runLoop, rollback, checkClaudeCli, readLoopLog } from '../src/loop.js';
14
14
  import { generateAutonomyConfig, printSecuritySummary, getFullModeWarning } from '../src/autonomy.js';
15
15
  import { getProfile, listProfiles } from '../src/profiles.js';
16
+ import { readReport, formatReport, formatEventLine, watchReport } from '../src/report.js';
16
17
 
17
18
  const __dirname = dirname(fileURLToPath(import.meta.url));
18
19
  const pkgJson = JSON.parse(await readFile(join(__dirname, '..', 'package.json'), 'utf-8'));
@@ -131,6 +132,7 @@ program
131
132
  .option('-d, --dir <path>', 'Target project directory')
132
133
  .option('--allow-dirty', 'Allow running with uncommitted git changes')
133
134
  .option('--rollback', 'Undo all changes from the most recent loop run')
135
+ .option('--resume', 'Resume from last rate-limited iteration')
134
136
  .option('--unsafe', 'Remove default tool restrictions (allows rm -rf)')
135
137
  .option('--worktree', 'Run each iteration in an isolated git worktree')
136
138
  .option('--profile <name>', 'Autonomy profile: safe, moderate, full, ci')
@@ -141,6 +143,36 @@ program
141
143
  return;
142
144
  }
143
145
 
146
+ // --- Resume mode ---
147
+ if (opts.resume) {
148
+ const resumeCwd = opts.dir ? resolve(opts.dir) : process.cwd();
149
+ const prevLog = await readLoopLog(resumeCwd);
150
+ if (!prevLog || !prevLog.pausedAt) {
151
+ console.error('\n No resumable loop found. Run a loop first or check .claude/loop-log.json.\n');
152
+ process.exit(1);
153
+ }
154
+ const saved = prevLog.options || {};
155
+ console.log(`\n Resuming loop from iteration ${prevLog.pausedAt.iteration}`);
156
+ console.log(` Goal: ${saved.goal || 'General improvement'}`);
157
+ console.log(` Session: ${prevLog.pausedAt.sessionId || 'new'}\n`);
158
+
159
+ await runLoop({
160
+ iterations: opts.iterations || Infinity,
161
+ trust: saved.trust || false,
162
+ goal: saved.goal,
163
+ pause: false,
164
+ maxTurns: saved.maxTurns || 30,
165
+ model: opts.model || saved.model,
166
+ budget: saved.budget,
167
+ cwd: resumeCwd,
168
+ allowDirty: true,
169
+ worktree: saved.worktree || false,
170
+ startIteration: prevLog.pausedAt.iteration,
171
+ initialSessionId: prevLog.pausedAt.sessionId,
172
+ });
173
+ return;
174
+ }
175
+
144
176
  // --- Pre-flight: check Claude CLI ---
145
177
  const cli = checkClaudeCli();
146
178
  if (!cli.installed) {
@@ -176,6 +208,7 @@ program
176
208
  disallowedTools: profile.disallowedTools,
177
209
  maxTurns: profile.maxTurns,
178
210
  budget: profile.maxBudget,
211
+ model: profile.model,
179
212
  };
180
213
  console.log(` Profile: ${profile.name} — ${profile.description}`);
181
214
  }
@@ -189,7 +222,8 @@ program
189
222
  if (opts.worktree) console.log(` Worktree: enabled (each iteration in isolated worktree)`);
190
223
  if (opts.iterations) console.log(` Max iterations: ${opts.iterations}`);
191
224
  if (opts.goal) console.log(` Goal: ${opts.goal}`);
192
- if (opts.model) console.log(` Model: ${opts.model}`);
225
+ const model = opts.model || profileDefaults.model || undefined;
226
+ if (model) console.log(` Model: ${model}`);
193
227
  if (opts.budget || profileDefaults.budget) console.log(` Budget: $${opts.budget || profileDefaults.budget}/iteration`);
194
228
  if (opts.maxTurns || profileDefaults.maxTurns) console.log(` Max turns: ${opts.maxTurns || profileDefaults.maxTurns}`);
195
229
 
@@ -199,7 +233,7 @@ program
199
233
  goal: opts.goal,
200
234
  pause,
201
235
  maxTurns: opts.maxTurns || profileDefaults.maxTurns || 30,
202
- model: opts.model,
236
+ model,
203
237
  budget: opts.budget || profileDefaults.budget,
204
238
  cwd,
205
239
  allowDirty: opts.allowDirty || false,
@@ -209,6 +243,40 @@ program
209
243
  });
210
244
  });
211
245
 
246
+ // --- report ---
247
+ program
248
+ .command('report')
249
+ .description('View work report from autonomous loop runs')
250
+ .option('-f, --follow', 'Live stream events (tail -f style)')
251
+ .option('--last <n>', 'Show last N events', parseInt)
252
+ .option('-d, --dir <path>', 'Project directory')
253
+ .action(async (opts) => {
254
+ const cwd = opts.dir ? resolve(opts.dir) : process.cwd();
255
+ const events = await readReport(cwd);
256
+
257
+ if (opts.follow) {
258
+ // Print existing events first
259
+ if (events.length > 0) {
260
+ const show = opts.last ? events.slice(-opts.last) : events;
261
+ process.stdout.write(formatReport(show));
262
+ }
263
+ console.log(' Watching for new events... (Ctrl+C to stop)\n');
264
+ await watchReport(cwd, (event) => {
265
+ process.stdout.write(formatEventLine(event));
266
+ });
267
+ return;
268
+ }
269
+
270
+ if (events.length === 0) {
271
+ console.log('\n No work report found. Run `claudenv loop` first.\n');
272
+ return;
273
+ }
274
+
275
+ const show = opts.last ? events.slice(-opts.last) : events;
276
+ console.log();
277
+ process.stdout.write(formatReport(show));
278
+ });
279
+
212
280
  // --- autonomy ---
213
281
  program
214
282
  .command('autonomy')
@@ -247,7 +315,8 @@ async function runInstall(opts) {
247
315
  console.log(`
248
316
  Done! Now open Claude Code in any project and type:
249
317
 
250
- /claudenv
318
+ /claudenv — Set up project documentation
319
+ /autonomy — Manage autonomy profiles
251
320
 
252
321
  Claude will analyze your project and generate documentation.
253
322
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudenv",
3
- "version": "1.2.4",
3
+ "version": "1.2.5",
4
4
  "description": "One command to integrate Claude Code into any project — installs /claudenv command for AI-powered documentation generation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,90 @@
1
+ ---
2
+ description: Manage autonomy profiles — switch between safe/moderate/full/ci or view current status
3
+ allowed-tools: Bash, Read, Glob, Grep
4
+ argument-hint: <status|safe|moderate|full|ci> [--dry-run]
5
+ ---
6
+
7
+ # Autonomy Profile Manager
8
+
9
+ Manage Claude Code autonomy profiles directly from within a session.
10
+
11
+ ## Instructions
12
+
13
+ Parse `$ARGUMENTS` and execute the matching action below.
14
+
15
+ ### 1. Determine the action
16
+
17
+ - If `$ARGUMENTS` is empty or `status` → go to **Status**
18
+ - If `$ARGUMENTS` starts with `safe`, `moderate`, `full`, or `ci` → go to **Switch Profile**
19
+ - Otherwise → go to **Usage Help**
20
+
21
+ ### 2. Status
22
+
23
+ Show the current autonomy configuration:
24
+
25
+ 1. Check if `.claude/hooks/pre-tool-use.sh` exists in the current project directory.
26
+ - If it exists, read the first 5 lines to find the `# Profile:` comment header and report the active profile name.
27
+ - If it doesn't exist, report "No autonomy profile configured for this project."
28
+ 2. Check if `.claude/settings.json` exists. If so, read it and summarize the permissions (allowedTools, disallowedTools counts).
29
+ 3. List available profiles: `safe`, `moderate`, `full`, `ci`.
30
+
31
+ ### 3. Switch Profile
32
+
33
+ The target profile is the first word of `$ARGUMENTS`. Check for `--dry-run` flag in the remaining arguments.
34
+
35
+ **If the profile is `full`:**
36
+ Before proceeding, display this warning:
37
+
38
+ ```
39
+ ⚠️ FULL AUTONOMY MODE — UNRESTRICTED ACCESS
40
+
41
+ This profile grants Claude Code complete access including:
42
+ • All file system operations (read, write, delete)
43
+ • Credential files (~/.ssh, ~/.aws, ~/.gnupg, ~/.kube, etc.)
44
+ • All git operations including force push
45
+ • No permission prompts (--dangerously-skip-permissions)
46
+
47
+ Safety net: audit logging + hard blocks on rm -rf / and force push to main
48
+ ```
49
+
50
+ Ask the user to confirm before proceeding. If they decline, abort.
51
+
52
+ **Apply the profile:**
53
+
54
+ Build the command:
55
+ ```
56
+ claudenv autonomy --profile <name> --yes --overwrite
57
+ ```
58
+
59
+ If `--dry-run` is present in `$ARGUMENTS`, append `--dry-run` to the command.
60
+
61
+ Run the command via Bash. If the command fails with "command not found", tell the user:
62
+
63
+ ```
64
+ claudenv is not on PATH. Install it with:
65
+ npm install -g claudenv
66
+ ```
67
+
68
+ After successful execution, show the output and confirm the profile was applied.
69
+
70
+ ### 4. Usage Help
71
+
72
+ Display:
73
+ ```
74
+ Usage: /autonomy <action> [options]
75
+
76
+ Actions:
77
+ status Show current autonomy profile and permissions
78
+ safe Switch to safe profile (read-only + limited bash)
79
+ moderate Switch to moderate profile (read/write + controlled bash)
80
+ full Switch to full profile (unrestricted — requires confirmation)
81
+ ci Switch to CI profile (optimized for CI/CD pipelines)
82
+
83
+ Options:
84
+ --dry-run Preview generated files without writing them
85
+
86
+ Examples:
87
+ /autonomy status
88
+ /autonomy moderate
89
+ /autonomy full --dry-run
90
+ ```
package/src/autonomy.js CHANGED
@@ -63,6 +63,7 @@ export function printSecuritySummary(profile) {
63
63
  console.log(`\n Security summary — ${profile.name} profile\n`);
64
64
  console.log(` ${'─'.repeat(50)}`);
65
65
 
66
+ console.log(` Default model: ${profile.model || 'sonnet'}`);
66
67
  console.log(` Skip permissions: ${profile.skipPermissions ? 'YES (--dangerously-skip-permissions)' : 'No'}`);
67
68
  console.log(` Credential policy: ${profile.credentialPolicy}`);
68
69
 
package/src/hooks-gen.js CHANGED
@@ -101,11 +101,21 @@ export function generatePreToolUseHook(profile, detected = {}) {
101
101
  return `#!/usr/bin/env bash
102
102
  # Pre-tool-use hook — generated by claudenv autonomy (profile: ${profile.name})
103
103
  # Exit 0 = allow, Exit 2 = block
104
+ # Claude Code sends hook data via stdin as JSON: {"tool_name":"...","tool_input":{...}}
104
105
 
105
106
  set -euo pipefail
106
107
 
107
- TOOL_NAME="\${CLAUDE_TOOL_NAME:-}"
108
- TOOL_INPUT="\${CLAUDE_TOOL_INPUT:-}"
108
+ # Read hook input from stdin (Claude Code sends JSON)
109
+ HOOK_INPUT=$(cat)
110
+ TOOL_NAME=$(echo "$HOOK_INPUT" | sed -n 's/.*"tool_name":"\\([^"]*\\)".*/\\1/p')
111
+
112
+ # Extract actionable string based on tool type
113
+ # Uses [^"]* to stop at the first unescaped quote — safely ignores other JSON fields
114
+ if [ "$TOOL_NAME" = "Bash" ]; then
115
+ TOOL_INPUT=$(echo "$HOOK_INPUT" | sed -n 's/.*"command":"\\([^"]*\\)".*/\\1/p')
116
+ else
117
+ TOOL_INPUT=$(echo "$HOOK_INPUT" | sed -n 's/.*"file_path":"\\([^"]*\\)".*/\\1/p')
118
+ fi
109
119
 
110
120
  # === Hard blocks (all profiles) ===
111
121
 
@@ -157,11 +167,14 @@ export function generateAuditLogHook() {
157
167
  return `#!/usr/bin/env bash
158
168
  # Post-tool-use audit hook — generated by claudenv autonomy
159
169
  # Logs every tool invocation to .claude/audit-log.jsonl
170
+ # Claude Code sends hook data via stdin as JSON: {"tool_name":"...","tool_input":{...}}
160
171
 
161
172
  set -uo pipefail
162
173
 
163
- TOOL_NAME="\${CLAUDE_TOOL_NAME:-unknown}"
164
- TOOL_INPUT="\${CLAUDE_TOOL_INPUT:-}"
174
+ # Read hook input from stdin (Claude Code sends JSON)
175
+ HOOK_INPUT=$(cat)
176
+ TOOL_NAME=$(echo "$HOOK_INPUT" | sed -n 's/.*"tool_name":"\\([^"]*\\)".*/\\1/p')
177
+ TOOL_INPUT=$(echo "$HOOK_INPUT" | sed -n 's/.*"tool_input":\\(.*\\)/\\1/p' | sed 's/}$//')
165
178
  SESSION_ID="\${CLAUDE_SESSION_ID:-}"
166
179
 
167
180
  # Truncate input for logging (max 500 chars)
package/src/installer.js CHANGED
@@ -92,6 +92,7 @@ export async function uninstallGlobal(options = {}) {
92
92
 
93
93
  const targets = [
94
94
  join(targetBase, 'commands', 'claudenv.md'),
95
+ join(targetBase, 'commands', 'autonomy.md'),
95
96
  join(targetBase, 'commands', 'setup-mcp.md'),
96
97
  join(targetBase, 'commands', 'improve.md'),
97
98
  join(targetBase, 'skills', 'claudenv'),
package/src/loop.js CHANGED
@@ -1,9 +1,22 @@
1
1
  import { execSync, spawn } from 'node:child_process';
2
- import { readFile, writeFile, mkdir } from 'node:fs/promises';
2
+ import { readFile, writeFile, mkdir, appendFile } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
4
  import { select } from '@inquirer/prompts';
5
5
  import { createWorktree, removeWorktree, mergeWorktree, getCurrentBranch } from './worktree.js';
6
6
 
7
+ // =============================================
8
+ // Work report helpers
9
+ // =============================================
10
+
11
+ /**
12
+ * Append a report event to .claude/work-report.jsonl
13
+ */
14
+ async function writeReportEvent(cwd, event) {
15
+ const line = JSON.stringify({ ts: new Date().toISOString(), ...event }) + '\n';
16
+ await mkdir(join(cwd, '.claude'), { recursive: true });
17
+ await appendFile(join(cwd, '.claude', 'work-report.jsonl'), line);
18
+ }
19
+
7
20
  // =============================================
8
21
  // Pre-flight: check Claude CLI
9
22
  // =============================================
@@ -68,16 +81,22 @@ export function spawnClaude(prompt, options = {}) {
68
81
 
69
82
  const child = spawn('claude', args, {
70
83
  cwd: options.cwd || process.cwd(),
71
- // stdin=inherit avoids Node.js spawn hang bug, stdout=pipe to capture JSON, stderr=inherit for real-time output
72
- stdio: ['inherit', 'pipe', 'inherit'],
84
+ // stdin=inherit avoids Node.js spawn hang bug, stdout=pipe to capture JSON, stderr=pipe for rate limit detection
85
+ stdio: ['inherit', 'pipe', 'pipe'],
73
86
  });
74
87
 
75
88
  let stdout = '';
89
+ let stderrBuf = '';
76
90
 
77
91
  child.stdout.on('data', (chunk) => {
78
92
  stdout += chunk.toString();
79
93
  });
80
94
 
95
+ child.stderr.on('data', (chunk) => {
96
+ process.stderr.write(chunk); // keep real-time output
97
+ stderrBuf += chunk.toString();
98
+ });
99
+
81
100
  child.on('error', (err) => {
82
101
  if (err.code === 'ENOENT') {
83
102
  reject(new Error('Claude CLI not found. Install it from https://docs.anthropic.com/en/docs/claude-code'));
@@ -88,7 +107,10 @@ export function spawnClaude(prompt, options = {}) {
88
107
 
89
108
  child.on('close', (code) => {
90
109
  if (code !== 0) {
91
- reject(new Error(`Claude exited with code ${code}`));
110
+ const isRateLimit = /rate.?limit|429|overloaded|too many requests/i.test(stderrBuf);
111
+ const err = new Error(`Claude exited with code ${code}`);
112
+ err.isRateLimit = isRateLimit;
113
+ reject(err);
92
114
  return;
93
115
  }
94
116
 
@@ -450,6 +472,8 @@ function printFinalSummary(log) {
450
472
  * @param {string} [options.cwd] - Working directory
451
473
  * @param {boolean} [options.allowDirty] - Allow dirty git state
452
474
  * @param {boolean} [options.worktree] - Run each iteration in an isolated git worktree
475
+ * @param {number} [options.startIteration] - Resume from this iteration (skip planning)
476
+ * @param {string} [options.initialSessionId] - Session ID to resume from
453
477
  */
454
478
  export async function runLoop(options = {}) {
455
479
  const cwd = options.cwd || process.cwd();
@@ -480,18 +504,19 @@ export async function runLoop(options = {}) {
480
504
  console.log(`\n Git safety tag: ${gitTag}`);
481
505
  console.log(` Pre-loop commit: ${preLoopCommit}`);
482
506
 
483
- // --- Initialize loop log ---
507
+ // --- Initialize loop log (carry over previous data on resume) ---
508
+ const prevLogData = startIteration > 0 ? await readLoopLog(cwd) : null;
484
509
  const log = {
485
- started: new Date().toISOString(),
510
+ started: prevLogData?.started || new Date().toISOString(),
486
511
  goal: options.goal || 'General improvement',
487
- model: options.model || 'sonnet',
488
- gitTag,
489
- preLoopCommit,
490
- iterations: [],
512
+ model: options.model || null,
513
+ gitTag: prevLogData?.gitTag || gitTag,
514
+ preLoopCommit: prevLogData?.preLoopCommit || preLoopCommit,
515
+ iterations: prevLogData?.iterations || [],
491
516
  completedAt: null,
492
517
  stopReason: null,
493
- totalIterations: 0,
494
- hypotheses: [],
518
+ totalIterations: prevLogData?.totalIterations || 0,
519
+ hypotheses: prevLogData?.hypotheses || [],
495
520
  };
496
521
 
497
522
  // --- Shared spawn options ---
@@ -505,8 +530,9 @@ export async function runLoop(options = {}) {
505
530
  appendSystemPrompt: options.goal ? buildAutonomySystemPrompt(options.goal) : undefined,
506
531
  };
507
532
 
508
- let sessionId = null;
533
+ let sessionId = options.initialSessionId || null;
509
534
  let shuttingDown = false;
535
+ const startIteration = options.startIteration || 0;
510
536
 
511
537
  // --- Ctrl+C handling ---
512
538
  const sigintHandler = () => {
@@ -521,37 +547,59 @@ export async function runLoop(options = {}) {
521
547
  process.on('SIGINT', sigintHandler);
522
548
 
523
549
  try {
524
- // --- Iteration 0: Planning ---
525
- console.log('\n Starting iteration 0 (planning)...\n');
526
-
527
- const planPrompt = buildPlanningPrompt(options.goal);
528
- const planResult = await spawnClaude(planPrompt, { ...spawnOpts, sessionId });
529
- sessionId = planResult.sessionId || sessionId;
530
-
531
- const planIteration = {
532
- number: 0,
533
- startedAt: log.started,
534
- completedAt: new Date().toISOString(),
535
- sessionId,
536
- summary: 'Generated improvement plan',
537
- commitHash: getCurrentCommit(cwd),
538
- usage: planResult.usage,
539
- };
540
- log.iterations.push(planIteration);
541
- printIterationSummary(planIteration);
542
- await writeLoopLog(cwd, log);
550
+ // --- Report: loop start ---
551
+ await writeReportEvent(cwd, {
552
+ event: 'loop_start',
553
+ goal: options.goal || 'General improvement',
554
+ model: options.model || null,
555
+ gitTag,
556
+ resume: startIteration > 0,
557
+ });
543
558
 
544
- if (shuttingDown) {
545
- log.stopReason = 'interrupted';
546
- log.completedAt = new Date().toISOString();
547
- log.totalIterations = 0;
559
+ // --- Iteration 0: Planning (skip if resuming) ---
560
+ if (startIteration === 0) {
561
+ console.log('\n Starting iteration 0 (planning)...\n');
562
+ await writeReportEvent(cwd, { event: 'iteration_start', iteration: 0, type: 'planning' });
563
+
564
+ const planPrompt = buildPlanningPrompt(options.goal);
565
+ const planResult = await spawnClaude(planPrompt, { ...spawnOpts, sessionId });
566
+ sessionId = planResult.sessionId || sessionId;
567
+
568
+ const planIteration = {
569
+ number: 0,
570
+ startedAt: log.started,
571
+ completedAt: new Date().toISOString(),
572
+ sessionId,
573
+ summary: 'Generated improvement plan',
574
+ commitHash: getCurrentCommit(cwd),
575
+ usage: planResult.usage,
576
+ };
577
+ log.iterations.push(planIteration);
578
+ printIterationSummary(planIteration);
548
579
  await writeLoopLog(cwd, log);
549
- printFinalSummary(log);
550
- return;
580
+ await writeReportEvent(cwd, {
581
+ event: 'iteration_end',
582
+ iteration: 0,
583
+ summary: 'Generated improvement plan',
584
+ commitHash: planIteration.commitHash,
585
+ tokens: planResult.usage ? { in: planResult.usage.input_tokens, out: planResult.usage.output_tokens } : null,
586
+ });
587
+
588
+ if (shuttingDown) {
589
+ log.stopReason = 'interrupted';
590
+ log.completedAt = new Date().toISOString();
591
+ log.totalIterations = 0;
592
+ await writeLoopLog(cwd, log);
593
+ printFinalSummary(log);
594
+ return;
595
+ }
596
+ } else {
597
+ console.log(`\n Resuming from iteration ${startIteration}...\n`);
551
598
  }
552
599
 
553
- // --- Execution iterations 1-N ---
554
- for (let i = 1; i <= maxIterations; i++) {
600
+ // --- Execution iterations 1-N (or startIteration-N if resuming) ---
601
+ const firstIter = startIteration > 0 ? startIteration : 1;
602
+ for (let i = firstIter; i <= maxIterations; i++) {
555
603
  if (shuttingDown) break;
556
604
 
557
605
  // --- Pause between iterations ---
@@ -607,11 +655,12 @@ export async function runLoop(options = {}) {
607
655
  }
608
656
  }
609
657
 
658
+ await writeReportEvent(cwd, { event: 'iteration_start', iteration: i, type: 'execution' });
659
+
610
660
  let iterResult;
611
661
  try {
612
662
  iterResult = await spawnClaude(execPrompt, { ...spawnOpts, cwd: iterCwd, sessionId });
613
663
  } catch (err) {
614
- console.error(`\n Iteration ${i} failed: ${err.message}`);
615
664
  // In worktree mode, clean up the worktree on failure
616
665
  if (worktreeInfo) {
617
666
  try {
@@ -624,6 +673,26 @@ export async function runLoop(options = {}) {
624
673
  iteration: i,
625
674
  });
626
675
  }
676
+
677
+ // Rate limit detection — save state for resume
678
+ if (err.isRateLimit) {
679
+ log.stopReason = 'rate_limit';
680
+ log.pausedAt = { iteration: i, sessionId };
681
+ log.options = {
682
+ goal: options.goal,
683
+ model: options.model,
684
+ trust: options.trust,
685
+ maxTurns: options.maxTurns,
686
+ budget: options.budget,
687
+ worktree: options.worktree,
688
+ };
689
+ await writeLoopLog(cwd, log);
690
+ await writeReportEvent(cwd, { event: 'rate_limit', iteration: i, message: err.message });
691
+ console.log(`\n Rate limited at iteration ${i}. Resume with: claudenv loop --resume`);
692
+ break;
693
+ }
694
+
695
+ console.error(`\n Iteration ${i} failed: ${err.message}`);
627
696
  log.stopReason = 'error';
628
697
  break;
629
698
  }
@@ -696,6 +765,13 @@ export async function runLoop(options = {}) {
696
765
  log.totalIterations = i;
697
766
  printIterationSummary(iteration);
698
767
  await writeLoopLog(cwd, log);
768
+ await writeReportEvent(cwd, {
769
+ event: 'iteration_end',
770
+ iteration: i,
771
+ summary: iteration.summary,
772
+ commitHash: iteration.commitHash,
773
+ tokens: iterResult.usage ? { in: iterResult.usage.input_tokens, out: iterResult.usage.output_tokens } : null,
774
+ });
699
775
 
700
776
  // --- Convergence check ---
701
777
  if (detectConvergence(iterResult.result)) {
@@ -740,6 +816,11 @@ export async function runLoop(options = {}) {
740
816
 
741
817
  log.completedAt = new Date().toISOString();
742
818
  await writeLoopLog(cwd, log);
819
+ await writeReportEvent(cwd, {
820
+ event: 'loop_end',
821
+ reason: log.stopReason,
822
+ totalIterations: log.totalIterations,
823
+ });
743
824
  printFinalSummary(log);
744
825
  }
745
826
 
package/src/profiles.js CHANGED
@@ -18,6 +18,7 @@ export const AUTONOMY_PROFILES = {
18
18
  safe: {
19
19
  name: 'safe',
20
20
  description: 'Read-only + limited bash — safe for exploration',
21
+ model: 'sonnet',
21
22
  allowedTools: [
22
23
  'Read',
23
24
  'Glob',
@@ -41,6 +42,7 @@ export const AUTONOMY_PROFILES = {
41
42
  moderate: {
42
43
  name: 'moderate',
43
44
  description: 'Full development with deny-list — safe for most development work',
45
+ model: 'sonnet',
44
46
  allowedTools: [
45
47
  'Read',
46
48
  'Edit',
@@ -72,6 +74,7 @@ export const AUTONOMY_PROFILES = {
72
74
  full: {
73
75
  name: 'full',
74
76
  description: 'Full autonomy — unrestricted access with audit logging',
77
+ model: 'opus',
75
78
  allowedTools: [],
76
79
  disallowedTools: [],
77
80
  skipPermissions: true,
@@ -81,6 +84,7 @@ export const AUTONOMY_PROFILES = {
81
84
  ci: {
82
85
  name: 'ci',
83
86
  description: 'Headless CI/CD mode — full autonomy with turn/budget limits',
87
+ model: 'haiku',
84
88
  allowedTools: [],
85
89
  disallowedTools: [],
86
90
  skipPermissions: true,
package/src/report.js ADDED
@@ -0,0 +1,160 @@
1
+ // =============================================
2
+ // Work report reader/formatter/watcher
3
+ // =============================================
4
+
5
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
6
+ import { createReadStream, watch, statSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+
9
+ const REPORT_FILE = '.claude/work-report.jsonl';
10
+
11
+ /**
12
+ * Read all report events from the JSONL file.
13
+ * @param {string} cwd - Working directory
14
+ * @returns {Promise<Array<object>>}
15
+ */
16
+ export async function readReport(cwd) {
17
+ try {
18
+ const content = await readFile(join(cwd, REPORT_FILE), 'utf-8');
19
+ return content
20
+ .split('\n')
21
+ .filter((line) => line.trim())
22
+ .map((line) => {
23
+ try {
24
+ return JSON.parse(line);
25
+ } catch {
26
+ return null;
27
+ }
28
+ })
29
+ .filter(Boolean);
30
+ } catch {
31
+ return [];
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Format report events as a human-readable timeline.
37
+ * @param {Array<object>} events
38
+ * @returns {string}
39
+ */
40
+ export function formatReport(events) {
41
+ if (events.length === 0) return ' No report events found.\n';
42
+
43
+ const lines = [];
44
+ lines.push(' Work Report');
45
+ lines.push(` ${'─'.repeat(50)}`);
46
+
47
+ for (const ev of events) {
48
+ const time = ev.ts ? new Date(ev.ts).toLocaleTimeString() : '??:??:??';
49
+ switch (ev.event) {
50
+ case 'loop_start':
51
+ lines.push(` ${time} LOOP START`);
52
+ lines.push(` Goal: ${ev.goal || 'General improvement'}`);
53
+ if (ev.model) lines.push(` Model: ${ev.model}`);
54
+ if (ev.gitTag) lines.push(` Tag: ${ev.gitTag}`);
55
+ break;
56
+
57
+ case 'iteration_start':
58
+ lines.push(` ${time} ITERATION ${ev.iteration} start${ev.type ? ` (${ev.type})` : ''}`);
59
+ break;
60
+
61
+ case 'iteration_end':
62
+ lines.push(` ${time} ITERATION ${ev.iteration} done`);
63
+ if (ev.summary) lines.push(` ${ev.summary.slice(0, 120)}`);
64
+ if (ev.commitHash) lines.push(` Commit: ${ev.commitHash}`);
65
+ if (ev.tokens) lines.push(` Tokens: ${ev.tokens.in || 0} in / ${ev.tokens.out || 0} out`);
66
+ break;
67
+
68
+ case 'rate_limit':
69
+ lines.push(` ${time} RATE LIMITED at iteration ${ev.iteration}`);
70
+ if (ev.message) lines.push(` ${ev.message}`);
71
+ break;
72
+
73
+ case 'loop_end':
74
+ lines.push(` ${time} LOOP END — ${ev.reason || 'unknown'} (${ev.totalIterations || '?'} iterations)`);
75
+ break;
76
+
77
+ default:
78
+ lines.push(` ${time} ${ev.event || 'unknown'}`);
79
+ }
80
+ lines.push('');
81
+ }
82
+
83
+ lines.push(` ${'─'.repeat(50)}`);
84
+ return lines.join('\n') + '\n';
85
+ }
86
+
87
+ /**
88
+ * Format a single event as a compact one-line string (for follow/watch mode).
89
+ * @param {object} ev
90
+ * @returns {string}
91
+ */
92
+ export function formatEventLine(ev) {
93
+ const time = ev.ts ? new Date(ev.ts).toLocaleTimeString() : '??:??:??';
94
+ switch (ev.event) {
95
+ case 'loop_start':
96
+ return ` ${time} LOOP START — ${ev.goal || 'General improvement'}${ev.model ? ` [${ev.model}]` : ''}\n`;
97
+ case 'iteration_start':
98
+ return ` ${time} ITERATION ${ev.iteration} start${ev.type ? ` (${ev.type})` : ''}\n`;
99
+ case 'iteration_end': {
100
+ let line = ` ${time} ITERATION ${ev.iteration} done`;
101
+ if (ev.commitHash) line += ` [${ev.commitHash}]`;
102
+ if (ev.tokens) line += ` (${ev.tokens.in || 0}/${ev.tokens.out || 0} tokens)`;
103
+ line += '\n';
104
+ if (ev.summary) line += ` ${ev.summary.slice(0, 120)}\n`;
105
+ return line;
106
+ }
107
+ case 'rate_limit':
108
+ return ` ${time} RATE LIMITED at iteration ${ev.iteration}${ev.message ? ` — ${ev.message}` : ''}\n`;
109
+ case 'loop_end':
110
+ return ` ${time} LOOP END — ${ev.reason || 'unknown'} (${ev.totalIterations || '?'} iterations)\n`;
111
+ default:
112
+ return ` ${time} ${ev.event || 'unknown'}\n`;
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Watch the report file for new events and call cb for each.
118
+ * @param {string} cwd - Working directory
119
+ * @param {function} cb - Callback receiving each new event object
120
+ * @returns {{ close: function }} Watcher handle
121
+ */
122
+ export async function watchReport(cwd, cb) {
123
+ const filePath = join(cwd, REPORT_FILE);
124
+ let position = 0;
125
+
126
+ // Ensure the file exists (watch throws on non-existent files)
127
+ try {
128
+ const { size } = statSync(filePath);
129
+ position = size;
130
+ } catch {
131
+ await mkdir(join(cwd, '.claude'), { recursive: true });
132
+ await writeFile(filePath, '');
133
+ }
134
+
135
+ const watcher = watch(filePath, () => {
136
+ // On change, read new lines from last position
137
+ const stream = createReadStream(filePath, { start: position, encoding: 'utf-8' });
138
+ let buffer = '';
139
+
140
+ stream.on('data', (chunk) => {
141
+ buffer += chunk;
142
+ position += Buffer.byteLength(chunk, 'utf-8');
143
+ });
144
+
145
+ stream.on('end', () => {
146
+ const lines = buffer.split('\n').filter((l) => l.trim());
147
+ for (const line of lines) {
148
+ try {
149
+ cb(JSON.parse(line));
150
+ } catch {
151
+ // skip malformed lines
152
+ }
153
+ }
154
+ });
155
+ });
156
+
157
+ return {
158
+ close: () => watcher.close(),
159
+ };
160
+ }