claudenv 1.2.3 → 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 +154 -179
- package/bin/cli.js +89 -5
- package/package.json +1 -1
- package/scaffold/global/.claude/commands/autonomy.md +90 -0
- package/src/autonomy.js +1 -0
- package/src/hooks-gen.js +17 -4
- package/src/installer.js +1 -0
- package/src/loop.js +122 -41
- package/src/profiles.js +4 -0
- package/src/report.js +160 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# claudenv
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
11
|
+
Open Claude Code in any project and type `/claudenv`. That's it.
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## What happens when you run `/claudenv`
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Claude reads your code, asks a few questions, and generates:
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
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
|
-
|
|
25
|
+
## Autonomous Loop
|
|
34
26
|
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
33
|
+
**What it does:**
|
|
69
34
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
40
|
+
### Common recipes
|
|
80
41
|
|
|
81
|
-
|
|
42
|
+
```bash
|
|
43
|
+
# Interactive mode — pauses between iterations so you can review
|
|
44
|
+
claudenv loop
|
|
82
45
|
|
|
83
|
-
|
|
46
|
+
# Fully autonomous — no pauses, no permission prompts
|
|
47
|
+
claudenv loop --trust
|
|
84
48
|
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
52
|
+
# Budget-conscious CI run
|
|
53
|
+
claudenv loop --profile ci --goal "fix lint errors" -n 10
|
|
92
54
|
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
114
|
-
|
|
64
|
+
# Rate limited? Just resume where you left off
|
|
65
|
+
claudenv loop --resume
|
|
115
66
|
|
|
116
|
-
|
|
67
|
+
# Override model on resume (e.g., switch to cheaper model)
|
|
68
|
+
claudenv loop --resume --model sonnet
|
|
69
|
+
```
|
|
117
70
|
|
|
118
|
-
|
|
71
|
+
### Live progress tracking
|
|
119
72
|
|
|
120
|
-
|
|
73
|
+
Monitor what Claude is doing in real time:
|
|
121
74
|
|
|
122
|
-
|
|
75
|
+
```bash
|
|
76
|
+
# In another terminal — tail -f style
|
|
77
|
+
claudenv report --follow
|
|
123
78
|
|
|
124
|
-
|
|
79
|
+
# Summary of the last loop run
|
|
80
|
+
claudenv report
|
|
125
81
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
82
|
+
# Last 5 events only
|
|
83
|
+
claudenv report --last 5
|
|
84
|
+
```
|
|
129
85
|
|
|
130
|
-
|
|
86
|
+
Events are stored in `.claude/work-report.jsonl` — machine-readable JSONL format.
|
|
131
87
|
|
|
132
|
-
|
|
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
|
-
|
|
|
92
|
+
| `--goal <text>` | What to work on (any goal — Claude interprets it) |
|
|
145
93
|
| `--trust` | Full trust mode — no pauses, skip permission prompts |
|
|
146
|
-
|
|
|
147
|
-
| `--
|
|
148
|
-
| `--
|
|
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
|
-
|
|
|
153
|
-
| `--
|
|
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
|
-
| `--
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
107
|
+
## Autonomy Profiles
|
|
164
108
|
|
|
165
|
-
|
|
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
|
|
169
|
-
claudenv autonomy --profile moderate #
|
|
170
|
-
claudenv autonomy --profile
|
|
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
|
-
###
|
|
117
|
+
### Profile comparison
|
|
177
118
|
|
|
178
|
-
| Profile |
|
|
179
|
-
|
|
180
|
-
|
|
|
181
|
-
|
|
|
182
|
-
|
|
|
183
|
-
|
|
|
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
|
-
###
|
|
128
|
+
### What gets generated
|
|
188
129
|
|
|
189
130
|
```
|
|
190
131
|
.claude/
|
|
191
|
-
├── settings.json
|
|
132
|
+
├── settings.json # Permissions, hooks config
|
|
192
133
|
├── hooks/
|
|
193
|
-
│ ├── pre-tool-use.sh
|
|
194
|
-
│ └── audit-log.sh
|
|
195
|
-
└── aliases.sh
|
|
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
|
-
###
|
|
141
|
+
### Using profiles with the loop
|
|
201
142
|
|
|
202
|
-
|
|
143
|
+
Profiles set sensible defaults for model, trust, and budget:
|
|
203
144
|
|
|
204
145
|
```bash
|
|
205
|
-
claudenv loop --profile
|
|
206
|
-
claudenv loop --profile
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
158
|
+
Servers are configured in `.mcp.json` with `${ENV_VAR}` placeholders — safe to commit:
|
|
220
159
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
-
|
|
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
|
|
232
|
-
claudenv install
|
|
233
|
-
claudenv
|
|
234
|
-
|
|
235
|
-
claudenv
|
|
236
|
-
claudenv
|
|
237
|
-
claudenv
|
|
238
|
-
claudenv
|
|
239
|
-
|
|
240
|
-
claudenv
|
|
241
|
-
claudenv
|
|
242
|
-
claudenv
|
|
243
|
-
claudenv
|
|
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
|
-
##
|
|
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
|
-
##
|
|
234
|
+
## Tech Stack Detection
|
|
257
235
|
|
|
258
|
-
|
|
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) {
|
|
@@ -151,6 +183,22 @@ program
|
|
|
151
183
|
console.log(`\n claudenv loop v${pkgJson.version}`);
|
|
152
184
|
console.log(` Claude CLI: ${cli.version}`);
|
|
153
185
|
|
|
186
|
+
const cwd = opts.dir ? resolve(opts.dir) : process.cwd();
|
|
187
|
+
|
|
188
|
+
// --- Auto-detect project autonomy config ---
|
|
189
|
+
if (!opts.profile && !opts.trust) {
|
|
190
|
+
try {
|
|
191
|
+
const settingsPath = join(cwd, '.claude', 'settings.json');
|
|
192
|
+
const settings = JSON.parse(await readFile(settingsPath, 'utf-8'));
|
|
193
|
+
if (!settings.permissions || (!settings.permissions.allow && !settings.permissions.deny)) {
|
|
194
|
+
opts.trust = true;
|
|
195
|
+
console.log(' Auto-detected: full autonomy config (.claude/settings.json)');
|
|
196
|
+
}
|
|
197
|
+
} catch {
|
|
198
|
+
// No settings.json or invalid — proceed normally
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
154
202
|
// --- Load profile if specified ---
|
|
155
203
|
let profileDefaults = {};
|
|
156
204
|
if (opts.profile) {
|
|
@@ -160,12 +208,12 @@ program
|
|
|
160
208
|
disallowedTools: profile.disallowedTools,
|
|
161
209
|
maxTurns: profile.maxTurns,
|
|
162
210
|
budget: profile.maxBudget,
|
|
211
|
+
model: profile.model,
|
|
163
212
|
};
|
|
164
213
|
console.log(` Profile: ${profile.name} — ${profile.description}`);
|
|
165
214
|
}
|
|
166
215
|
|
|
167
216
|
// --- Config summary ---
|
|
168
|
-
const cwd = opts.dir ? resolve(opts.dir) : process.cwd();
|
|
169
217
|
const trust = opts.trust || profileDefaults.trust || false;
|
|
170
218
|
const pause = opts.pause !== undefined ? opts.pause : !trust;
|
|
171
219
|
|
|
@@ -174,7 +222,8 @@ program
|
|
|
174
222
|
if (opts.worktree) console.log(` Worktree: enabled (each iteration in isolated worktree)`);
|
|
175
223
|
if (opts.iterations) console.log(` Max iterations: ${opts.iterations}`);
|
|
176
224
|
if (opts.goal) console.log(` Goal: ${opts.goal}`);
|
|
177
|
-
|
|
225
|
+
const model = opts.model || profileDefaults.model || undefined;
|
|
226
|
+
if (model) console.log(` Model: ${model}`);
|
|
178
227
|
if (opts.budget || profileDefaults.budget) console.log(` Budget: $${opts.budget || profileDefaults.budget}/iteration`);
|
|
179
228
|
if (opts.maxTurns || profileDefaults.maxTurns) console.log(` Max turns: ${opts.maxTurns || profileDefaults.maxTurns}`);
|
|
180
229
|
|
|
@@ -184,7 +233,7 @@ program
|
|
|
184
233
|
goal: opts.goal,
|
|
185
234
|
pause,
|
|
186
235
|
maxTurns: opts.maxTurns || profileDefaults.maxTurns || 30,
|
|
187
|
-
model
|
|
236
|
+
model,
|
|
188
237
|
budget: opts.budget || profileDefaults.budget,
|
|
189
238
|
cwd,
|
|
190
239
|
allowDirty: opts.allowDirty || false,
|
|
@@ -194,6 +243,40 @@ program
|
|
|
194
243
|
});
|
|
195
244
|
});
|
|
196
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
|
+
|
|
197
280
|
// --- autonomy ---
|
|
198
281
|
program
|
|
199
282
|
.command('autonomy')
|
|
@@ -232,7 +315,8 @@ async function runInstall(opts) {
|
|
|
232
315
|
console.log(`
|
|
233
316
|
Done! Now open Claude Code in any project and type:
|
|
234
317
|
|
|
235
|
-
/claudenv
|
|
318
|
+
/claudenv — Set up project documentation
|
|
319
|
+
/autonomy — Manage autonomy profiles
|
|
236
320
|
|
|
237
321
|
Claude will analyze your project and generate documentation.
|
|
238
322
|
`);
|
package/package.json
CHANGED
|
@@ -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
|
-
|
|
108
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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=
|
|
72
|
-
stdio: ['inherit', 'pipe', '
|
|
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
|
-
|
|
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 ||
|
|
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
|
-
// ---
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
|
545
|
-
|
|
546
|
-
log
|
|
547
|
-
|
|
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
|
-
|
|
550
|
-
|
|
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
|
-
|
|
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
|
+
}
|