@evertonferreira96/agents 0.1.1 → 0.1.3

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,15 +1,504 @@
1
+ <div align="center">
2
+
1
3
  # agents
2
4
 
3
- Personal agent and subagent configuration for Claude Code, portable to Cursor and Codex through `AGENTS.md`.
5
+ **Agents, rules, hooks and skills for Claude Code, installed into any project with one command.**
6
+
7
+ [![npm](https://img.shields.io/npm/v/@evertonferreira96/agents?label=npm&color=cb3837)](https://www.npmjs.com/package/@evertonferreira96/agents)
8
+ [![node](https://img.shields.io/node/v/@evertonferreira96/agents?color=339933)](package.json)
9
+ [![Claude Code](https://img.shields.io/badge/Claude%20Code-agents%20%C2%B7%20rules%20%C2%B7%20hooks%20%C2%B7%20skills-d97757)](docs/README.md)
10
+ [![Cursor and Codex](https://img.shields.io/badge/Cursor%20%26%20Codex-AGENTS.md-555555)](AGENTS.md)
11
+
12
+ </div>
13
+
14
+ ---
15
+
16
+ ## Contents
17
+
18
+ - [What it is](#what-it-is)
19
+ - [Quick start](#quick-start)
20
+ - [How it works](#how-it-works)
21
+ - [The big picture](#the-big-picture)
22
+ - [What `setup:pull` does, step by step](#what-setuppull-does-step-by-step)
23
+ - [How each file is decided](#how-each-file-is-decided)
24
+ - [Projects that already have an AGENTS.md](#projects-that-already-have-an-agentsmd)
25
+ - [What lands in your project](#what-lands-in-your-project)
26
+ - [Working in a configured project](#working-in-a-configured-project)
27
+ - [Command reference](#command-reference)
28
+ - [Release pipeline](#release-pipeline)
29
+ - [Maintaining this repo](#maintaining-this-repo)
30
+ - [Troubleshooting](#troubleshooting)
31
+ - [Further reading](#further-reading)
32
+
33
+ ---
34
+
35
+ ## What it is
36
+
37
+ A complete working setup for Claude Code, kept in one repo and shipped as the npm package `@evertonferreira96/agents`. Install it once per machine, run one command in a project, and the project gets the same team of agents, the same guardrails and the same architecture playbooks as every other project.
38
+
39
+ | Piece | What you get |
40
+ |---|---|
41
+ | **Agents** (10) | An orchestrator flow plus specialists: `quick`, `product`, `architect`, `api`, `web`, `app`, `infra`, `ui`, `verifier`, `security` |
42
+ | **Skills** (37) | Workflow (orchestrate, triage, brief, review, grilling, handoff, retro), architecture (`arch-api`, `arch-web`, `arch-app`, `arch-db`), quality (tdd, diagnosing-bugs, ponytail), design and motion |
43
+ | **Rules** (9) | Loaded always or by path glob: git, tickets, api, web, mobile, db, ui, graphify, delivery diagram |
44
+ | **Hooks** (8) | Deterministic guards that block what rules can only ask for: `git push`, `git add -A`, staging `.env`, the orchestrator editing code, lint and tests on Stop |
45
+ | **AGENTS.md** | The shared base that Cursor and Codex read too |
46
+ | **Updates** | A manifest of every installed file, so updates refresh what you did not touch and never overwrite what you changed |
47
+
48
+ ---
49
+
50
+ ## Quick start
51
+
52
+ **Requirements:** Node.js 18 or newer, npm, and Claude Code.
53
+
54
+ ```bash
55
+ # 1. once per machine
56
+ npm install -g @evertonferreira96/agents
57
+
58
+ # 2. in the root of any project, new or already configured
59
+ cd path/to/your-project
60
+ chew agents setup:pull
61
+ ```
62
+
63
+ 3. Fill in the project's purpose and commands in the generated `CLAUDE.md`.
64
+ 4. Open Claude Code in the project and describe the task.
65
+
66
+ To update later, run `chew agents setup:pull` again. Every run downloads the latest published version, so the global install does not need to be current.
67
+
68
+ <details>
69
+ <summary>Without a global install</summary>
70
+
71
+ ```bash
72
+ npx @evertonferreira96/agents init # first time
73
+ npx @evertonferreira96/agents@latest update # later
74
+ npx @evertonferreira96/agents check # drift report
75
+ ```
76
+
77
+ </details>
78
+
79
+ ---
80
+
81
+ ## How it works
82
+
83
+ ### The big picture
84
+
85
+ The config lives in this repo. Every push to `main` that changes a shipped file is checked, versioned and published. On your machine, `chew` pulls the newest published version into the project you are in.
86
+
87
+ ```mermaid
88
+ flowchart LR
89
+ subgraph REPO["This repo on GitHub"]
90
+ SRC[".claude/ agents, rules, skills, hooks<br/>AGENTS.md, templates, scripts"]
91
+ end
92
+
93
+ subgraph CI["GitHub Actions"]
94
+ CHECK["check.yml<br/>parse, catalog, tests"]
95
+ REL["release.yml<br/>version, publish, tag, release"]
96
+ CHECK --> REL
97
+ end
98
+
99
+ subgraph REG["Registries"]
100
+ NPM[("npmjs.com<br/>public")]
101
+ GHP[("GitHub Packages<br/>private")]
102
+ end
103
+
104
+ subgraph DEV["Your machine"]
105
+ CHEW["chew<br/>global CLI"]
106
+ PROJ["your-project/<br/>.claude/ AGENTS.md CLAUDE.md"]
107
+ end
108
+
109
+ SRC -- "git push main" --> CHECK
110
+ REL -- "npm publish" --> NPM
111
+ REL -- "npm publish" --> GHP
112
+ NPM -- "latest tarball" --> CHEW
113
+ CHEW -- "chew agents setup:pull" --> PROJ
114
+ ```
115
+
116
+ ### What `setup:pull` does, step by step
117
+
118
+ ```mermaid
119
+ sequenceDiagram
120
+ autonumber
121
+ actor You
122
+ participant Chew as chew (global)
123
+ participant Npm as npmjs.com
124
+ participant Tmp as Temp folder
125
+ participant Engine as Install engine (downloaded)
126
+ participant Proj as Your project
127
+
128
+ You->>Chew: chew agents setup:pull
129
+ alt registry reachable
130
+ Chew->>Npm: npm pack @evertonferreira96/agents@latest
131
+ Npm-->>Chew: agents-X.Y.Z.tgz
132
+ Chew->>Tmp: extract the package
133
+ Chew->>Engine: load scripts/bootstrap.mjs from version X.Y.Z
134
+ else offline or blocked
135
+ Chew-->>You: warning, using the version installed with chew
136
+ end
137
+ Engine->>Proj: read .claude/config-manifest.json (if any)
138
+ Engine->>Proj: add, update or remove files, one decision per file
139
+ Engine->>Proj: write manifest, config-source.json, CLAUDE.md (once), .gitignore lines
140
+ Chew->>Tmp: delete the temp folder
141
+ Chew-->>You: report: added, updated, removed, kept
142
+ ```
143
+
144
+ 1. **Download.** `chew` asks npm for the latest version, always against the public registry (also for the `@evertonferreira96` scope), so a user `.npmrc` that points to another feed does not interfere.
145
+ 2. **Extract.** The tarball is unpacked into a temporary folder.
146
+ 3. **Use the new engine.** The install runs with the `bootstrap.mjs` of the downloaded version, not the one installed with `chew`. An old global `chew` still applies the newest config and only prints a hint to update itself.
147
+ 4. **Decide file by file.** Each shipped file is compared with the project copy and with the manifest of the last install (next section).
148
+ 5. **Record.** `.claude/config-manifest.json` stores the hash of every installed file; `.claude/config-source.json` stores the package and version.
149
+ 6. **First run only.** `CLAUDE.md` is created from `templates/CLAUDE.project.md`, and the `.gitignore` entries are appended.
150
+ 7. **Clean up and report.** The temp folder is deleted and the result is printed.
151
+
152
+ ### How each file is decided
153
+
154
+ The manifest is what makes updates safe. It records the hash each file had when the config installed it, so the engine can tell a file that is only **outdated** (safe to refresh) from one you **edited** (kept).
155
+
156
+ ```mermaid
157
+ flowchart TD
158
+ A["File shipped by the latest version"] --> B{"Exists in<br/>the project?"}
159
+ B -- no --> ADD["added"]
160
+ B -- yes --> C{"Same content?"}
161
+ C -- yes --> SAME["unchanged"]
162
+ C -- no --> D{"Project copy matches<br/>the manifest hash?"}
163
+ D -- "yes: untouched,<br/>only outdated" --> UPD["updated"]
164
+ D -- "no: edited here" --> F{"run with<br/>force flag?"}
165
+ F -- yes --> UPD
166
+ F -- no --> KEPT["kept<br/>listed as edited"]
167
+
168
+ O["File in the manifest that the<br/>latest version no longer ships"] --> G{"Untouched?"}
169
+ G -- yes --> DEL["removed"]
170
+ G -- no --> OBS["kept<br/>listed as obsolete"]
171
+
172
+ classDef add fill:#dcfce7,stroke:#16a34a,color:#14532d
173
+ classDef upd fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
174
+ classDef keep fill:#fef3c7,stroke:#d97706,color:#78350f
175
+ classDef del fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
176
+ classDef same fill:#f3f4f6,stroke:#9ca3af,color:#374151
177
+ class ADD add
178
+ class UPD upd
179
+ class KEPT,OBS keep
180
+ class DEL del
181
+ class SAME same
182
+ ```
183
+
184
+ **Never touched, not even with `--force`:** `CLAUDE.md`, `.claude/settings.local.json` and `.claude/agent-memory/**`.
185
+
186
+ | Situation | Result |
187
+ |---|---|
188
+ | New project, nothing installed | Everything added, `CLAUDE.md` created, `.gitignore` extended |
189
+ | Configured project, nothing edited | New files added, changed files updated, removed files deleted |
190
+ | You edited a shipped file | Kept and listed; `--force` replaces it with the config version |
191
+ | Project configured before the manifest existed | Differing files count as edited; review them, then run `--force` once |
192
+ | The project has its own `AGENTS.md` | You choose: keep, overwrite, append or print ([next section](#projects-that-already-have-an-agentsmd)) |
193
+
194
+ ### Projects that already have an AGENTS.md
195
+
196
+ Many projects already carry an `AGENTS.md` with their own rules. When the project copy differs from the config, `setup:pull` asks what to do. Press Enter to keep yours; outside a terminal (CI, scripts) it keeps yours and lists the options.
197
+
198
+ ```text
199
+ AGENTS.md already exists in this project and differs from the config. What should happen to it?
200
+ [k] keep yours as it is (default)
201
+ [o] overwrite it with the config version
202
+ [a] append the config version at the end, in a managed block that later pulls keep current
203
+ [p] print the config version here, to concatenate it yourself
204
+ Choice [k/o/a/p]:
205
+ ```
206
+
207
+ ```mermaid
208
+ flowchart TD
209
+ A["Project AGENTS.md differs<br/>from the config"] --> B{"Has a managed block?"}
210
+ B -- yes --> RB["Refresh only the block<br/>no question asked"]
211
+ B -- no --> Q{"Your choice<br/>prompt or --agents-md"}
212
+ Q -- keep --> K["Yours stays as it is"]
213
+ Q -- overwrite --> O["Replaced by the config version<br/>later pulls keep it current"]
214
+ Q -- append --> AP["Config version added at the end<br/>inside the managed block"]
215
+ Q -- print --> P["Config version printed<br/>you paste it where you want"]
216
+ AP -. "next pull" .-> RB
217
+ P -. "pasted with the markers" .-> RB
218
+
219
+ classDef keep fill:#f3f4f6,stroke:#9ca3af,color:#374151
220
+ classDef upd fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
221
+ classDef add fill:#dcfce7,stroke:#16a34a,color:#14532d
222
+ classDef out fill:#fef3c7,stroke:#d97706,color:#78350f
223
+ class K keep
224
+ class O,RB upd
225
+ class AP add
226
+ class P out
227
+ ```
228
+
229
+ The managed block is plain Markdown between two comments. Everything outside the markers is yours; later pulls replace only what is inside them:
230
+
231
+ ```markdown
232
+ # My project
233
+ Rules that belong to this project.
234
+
235
+ <!-- agents-config:start -->
236
+ <!-- Managed by @evertonferreira96/agents: `chew agents setup:pull` refreshes this block. Edit outside the markers. -->
237
+ ...the config AGENTS.md...
238
+ <!-- agents-config:end -->
239
+ ```
240
+
241
+ | Want | Command |
242
+ |---|---|
243
+ | Decide without the question | `chew agents setup:pull --agents-md=keep` (or `overwrite`, `append`, `print`) |
244
+ | Just see the config version | `chew agents setup:print` (in the managed block) or `chew agents setup:print --raw` (file alone) |
245
+ | Concatenate from the shell | `chew agents setup:print >> AGENTS.md` (bash, zsh, cmd; in Windows PowerShell 5, `>>` writes UTF-16, so use `--agents-md=append`) |
246
+
247
+ ### What lands in your project
248
+
249
+ ```text
250
+ your-project/
251
+ ├── AGENTS.md shared base (Claude Code, Cursor, Codex)
252
+ ├── CLAUDE.md created once from the template, then yours
253
+ ├── .worktreeinclude files copied into each task worktree
254
+ ├── .gitignore + worktrees, handoffs, local settings, graphify-out
255
+ ├── skills-lock.json origin and hash of every vendored skill
256
+ ├── docs/
257
+ │ ├── plans/README.md plan template, one plan per task
258
+ │ ├── adr/README.md ADR template
259
+ │ └── brief-template.md
260
+ ├── scripts/ bootstrap, check-catalog, skills-update
261
+ └── .claude/
262
+ ├── agents/ 10 agents
263
+ ├── skills/ 37 skills
264
+ ├── rules/ 9 rules
265
+ ├── hooks/ 8 hooks
266
+ ├── settings.json hook wiring, worktree base, context limits
267
+ ├── config-manifest.json hash of every installed file (commit it)
268
+ └── config-source.json installed package and version
269
+ ```
270
+
271
+ ---
272
+
273
+ ## Working in a configured project
274
+
275
+ Open Claude Code and describe the task, paste the ticket or paste the error. The main session acts as the orchestrator: it sizes the work, plans it with you and delegates it. It never edits code itself (a hook enforces this).
276
+
277
+ ```mermaid
278
+ flowchart TD
279
+ REQ["Task, ticket or error"] --> RAW{"Raw report?"}
280
+ RAW -- yes --> TRI["triage<br/>classify, reproduce, brief"]
281
+ RAW -- no --> CLS{"Size"}
282
+ TRI --> CLS
283
+
284
+ CLS -- "S: one file,<br/>one-sentence diff" --> QK["quick agent"]
285
+ QK --> STOP["Stop hook<br/>lint and tests"]
286
+
287
+ CLS -- "M or L" --> WT["Task worktree<br/>.claude/worktrees/slug"]
288
+ WT --> GRL["Grill with you<br/>L always, M on open decisions"]
289
+ GRL --> PRD["product agent<br/>scope, AC, test seams, tickets"]
290
+ PRD -- "L or architecture trigger" --> ARC["architect agent"]
291
+ PRD -- M --> GATE
292
+ ARC --> GATE{"You approve<br/>the tickets"}
293
+ GATE --> IMP["One implementer per ticket<br/>api, web, app, infra, ui<br/>test-first with tdd"]
294
+ IMP --> INT["Integrate"]
295
+ INT --> VER["verifier agent<br/>Spec axis, then Standards axis"]
296
+ VER -- "auth, secrets, PII, payments" --> SEC["security agent"]
297
+ VER --> CMT["Commit and report"]
298
+ SEC --> CMT
299
+ CMT -- L --> RET["retro<br/>lessons become hooks or rules"]
300
+
301
+ classDef you fill:#fef3c7,stroke:#d97706,color:#78350f
302
+ classDef agent fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
303
+ classDef guard fill:#dcfce7,stroke:#16a34a,color:#14532d
304
+ class GRL,GATE you
305
+ class QK,PRD,ARC,IMP,VER,SEC agent
306
+ class STOP guard
307
+ ```
308
+
309
+ <sub>Amber: steps with you. Blue: agents. Green: hooks.</sub>
310
+
311
+ | Agent | Does | Mode |
312
+ |---|---|---|
313
+ | `quick` | S work: one file, one-sentence diff | writes |
314
+ | `product` | Plan: scope, stories, acceptance criteria, test seams, tickets | read-only |
315
+ | `architect` | Boundaries, contracts, failure modes; verdict and ADR proposal | read-only |
316
+ | `api` | Server side: APIs, domain, persistence, workers (`arch-api`, `arch-db`, `tdd`) | writes |
317
+ | `web` | Next.js app (`arch-web`, `tdd`) | writes |
318
+ | `app` | React Native / Expo app (`arch-app`, `tdd`) | writes |
319
+ | `infra` | Terraform, CI, deploy, observability; never applies | writes |
320
+ | `ui` | Design system, tokens, components, motion, accessibility | writes |
321
+ | `verifier` | Fresh-context check of the diff against the plan; runs lint, build, tests | no edits |
322
+ | `security` | Auth, secrets, PII, payments, public routes; severity report | read-only |
323
+
324
+ **Guardrails enforced by hooks**
325
+
326
+ | Hook | Effect |
327
+ |---|---|
328
+ | `guard-bash` | Blocks `git push`, `git add -A` / `.`, staging `.env*`, `--no-verify`, `reset --hard`, `clean -f`, PR and issue writes |
329
+ | `orchestrator-guard` | The main session can edit plans, ADRs and docs, never code |
330
+ | `readonly-guard` | Read-only agents can only write their own memory |
331
+ | `stop-verify` | Lint and tests on Stop when the tree changed |
332
+ | `context-budget` | Warns at 60k, 90k and 110k tokens; compaction at 120k |
333
+ | `post-compact` | Re-injects the active plan after compaction |
334
+ | `graphify-update` | Refreshes the code graph after edits, when one exists |
335
+ | `config-update-check` | One line at session start when a local-clone install has a newer version |
336
+
337
+ The full flow, with every section of the playbook, is in [`.claude/skills/orchestrate/SKILL.md`](.claude/skills/orchestrate/SKILL.md). A five-minute walkthrough is in [docs/quickstart.md](docs/quickstart.md).
338
+
339
+ ---
340
+
341
+ ## Command reference
342
+
343
+ ### `chew` (global)
344
+
345
+ | Command | What it does |
346
+ |---|---|
347
+ | `chew agents setup:pull [dir]` | Download the latest version and install or refresh it in `dir` (default: current folder) |
348
+ | `chew agents setup:check [dir]` | Compare the project with the latest version; exit code 1 on drift |
349
+ | `chew agents setup:print` | Print the latest config `AGENTS.md` in its managed block, to concatenate into yours (`--raw`: the file alone) |
350
+ | `chew --version` | Version of the installed `chew` |
351
+
352
+ | Flag | Effect |
353
+ |---|---|
354
+ | `--force` | Also overwrite files edited in the project (protected files excepted) |
355
+ | `--dry-run` | Show what would change, write nothing |
356
+ | `--local` | Use the version installed with `chew` instead of downloading |
357
+ | `--agents-md=<choice>` | When the project has its own `AGENTS.md`: `keep` (default), `overwrite`, `append` or `print`; skips the question |
358
+
359
+ ### `agents` (same package, via `npx` or global)
360
+
361
+ | Command | What it does |
362
+ |---|---|
363
+ | `agents init [dir]` | Copy what is missing; never overwrites |
364
+ | `agents update [dir]` | Same rules as `setup:pull`, using this installed version |
365
+ | `agents check [dir]` | Report missing, outdated, edited and obsolete files; exit 1 on drift |
366
+ | `agents print-agents-md` | Same as `chew agents setup:print`, for this installed version |
367
+
368
+ `init` and `update` accept `--force`, `--dry-run` and `--agents-md=<choice>`, like `chew`.
369
+
370
+ ### Legacy, from a local clone
371
+
372
+ ```bash
373
+ node <agents-repo>/scripts/bootstrap.mjs <project> # init
374
+ node <agents-repo>/scripts/bootstrap.mjs <project> --check # drift report
375
+ node scripts/bootstrap.mjs --sync # inside the project: pull the clone, then update
376
+ ```
377
+
378
+ ---
379
+
380
+ ## Release pipeline
381
+
382
+ Every push to `main` that changes a shipped file (`.claude/**`, `AGENTS.md`, `bin/`, `scripts/`, `templates/`, `package.json`, `VERSION`, the workflow itself) publishes a new version. Pushes that change only files the package does not ship (this README, the CHANGELOG, most of `docs/`) do not.
383
+
384
+ ```mermaid
385
+ flowchart LR
386
+ PUSH["push to main"] --> CHK["check.yml<br/>hooks and scripts parse<br/>JSON, catalog, em-dash scan<br/>npm test, npm pack"]
387
+ CHK --> VER["release-version.mjs<br/>picks X.Y.Z"]
388
+ VER --> PUB["npm publish<br/>npmjs.com"]
389
+ PUB --> TAG["git tag vX.Y.Z"]
390
+ TAG --> GHR["GitHub release<br/>notes from commits"]
391
+ GHR --> GPK["GitHub Packages<br/>same version"]
392
+ ```
393
+
394
+ **Versioning**
395
+
396
+ | You want | Do |
397
+ |---|---|
398
+ | A patch | Nothing. The pipeline publishes the next free patch after the highest published version |
399
+ | A minor or major | Set the new base in `package.json` and `VERSION` (they must match) and push |
400
+
401
+ The bumped version exists only in the published package and the tag; the repo keeps the base.
402
+
403
+ **Credentials**
404
+
405
+ | Target | Auth |
406
+ |---|---|
407
+ | npmjs.com | npm trusted publishing (OIDC, no secret), or the `NPM_TOKEN` repository secret until it is configured |
408
+ | GitHub Packages | The workflow's own `GITHUB_TOKEN`; nothing to set up |
409
+
410
+ `.npmrc` pins the public registry and reads the token only from the `NPM_TOKEN` environment variable. Never write a token into it: the file is committed.
411
+
412
+ ---
413
+
414
+ ## Maintaining this repo
415
+
416
+ ```bash
417
+ npm test # CLI round trip: init, check, update, force, obsolete files, chew from a tarball
418
+ npm run check # skills on disk vs catalog vs lock vs agents vs hooks
419
+ node scripts/skills-update.mjs # vendored skills that changed upstream
420
+ ```
421
+
422
+ - Every change that ships gets an entry in [CHANGELOG.md](CHANGELOG.md) in the same commit.
423
+ - Decisions about the config itself go to [docs/adr/](docs/adr/).
424
+ - Adding a skill: [docs/skills.md](docs/skills.md), section "Adding a skill"; `npm run check` lists what is missing.
425
+ - CI rejects em dashes in our own prose (`AGENTS.md`, docs, agents, rules, own skills).
426
+
427
+ ```text
428
+ agents/
429
+ ├── .claude/ the config that ships (agents, hooks, rules, skills, settings.json)
430
+ ├── .github/workflows/ check.yml (CI) and release.yml (publish)
431
+ ├── bin/ chew.mjs (global CLI), agents.mjs (package CLI)
432
+ ├── scripts/ bootstrap.mjs (install engine), release-version.mjs, tests, catalog and skills checks
433
+ ├── templates/ CLAUDE.project.md for new projects
434
+ ├── docs/ quickstart, catalog, ADRs, plan and brief templates
435
+ ├── AGENTS.md shared base
436
+ └── CHANGELOG.md
437
+ ```
438
+
439
+ ---
440
+
441
+ ## Troubleshooting
442
+
443
+ <details>
444
+ <summary><b><code>chew: command not found</code> after the global install</b></summary>
445
+
446
+ The npm global bin folder is not on your `PATH`. `npm prefix -g` shows the prefix; on Windows the binaries live in `%AppData%\npm`, on macOS and Linux in `<prefix>/bin`. Add it to `PATH` and open a new terminal.
447
+
448
+ </details>
449
+
450
+ <details>
451
+ <summary><b>The global install fails with E401 or asks for another registry</b></summary>
452
+
453
+ Your user `.npmrc` points to a private feed (for example Azure Artifacts). Install from the public registry:
454
+
455
+ ```bash
456
+ npm install -g @evertonferreira96/agents --registry https://registry.npmjs.org/
457
+ # or, once:
458
+ npm config set @evertonferreira96:registry https://registry.npmjs.org/
459
+ ```
460
+
461
+ `chew` itself already pins the public registry when it downloads updates.
462
+
463
+ </details>
464
+
465
+ <details>
466
+ <summary><b>A file I changed was not updated</b></summary>
467
+
468
+ By design: files edited in the project are kept and listed as `edited`. `chew agents setup:check` shows them. To adopt the config version, run `chew agents setup:pull --force` (try `--dry-run` first).
469
+
470
+ </details>
471
+
472
+ <details>
473
+ <summary><b>My project's AGENTS.md was not touched</b></summary>
474
+
475
+ Keeping it is the default when no choice is given, including every run outside a terminal. Run `chew agents setup:pull --agents-md=append` to add the config in a managed block below your content, or `--agents-md=overwrite` to replace it. See [Projects that already have an AGENTS.md](#projects-that-already-have-an-agentsmd).
476
+
477
+ </details>
478
+
479
+ <details>
480
+ <summary><b>"no manifest from a previous install"</b></summary>
481
+
482
+ The project was configured by copy before the manifest existed, so every differing file looks edited. Review the list, then run `chew agents setup:pull --force` once. From then on, updates are automatic.
483
+
484
+ </details>
485
+
486
+ <details>
487
+ <summary><b>No internet, or npm is blocked</b></summary>
488
+
489
+ `setup:pull` warns and installs the version that came with `chew`. Use `--local` to do that on purpose.
490
+
491
+ </details>
4
492
 
5
- - Orchestrator playbook: `.claude/skills/orchestrate/SKILL.md`
6
- - Roster: `.claude/agents/` (quick, product, architect, api, web, app, infra, ui, verifier, security)
7
- - Deterministic guards: `.claude/hooks/` wired in `.claude/settings.json`
8
- - `skills-lock.json` records origin, hash and adaptation of every vendored skill; `node scripts/skills-update.mjs` checks them against upstream; catalog in [docs/skills.md](docs/skills.md)
9
- - Decisions about this config: [docs/adr/](docs/adr/); history: [CHANGELOG.md](CHANGELOG.md)
10
- - Put it in a project: `npx @evertonferreira96/agents init` in the project root (or `npx github:EvertonFerreira96/agents init` straight from the repo); keep it current with `npx @evertonferreira96/agents@latest update` and look for drift with `npx @evertonferreira96/agents check`. A local clone still works: `node scripts/bootstrap.mjs <project-root>`. First task: [docs/quickstart.md](docs/quickstart.md)
11
- - Release: every push to `main` that changes what the package ships runs `.github/workflows/release.yml`: the full check, then `npm publish`, the `vX.Y.Z` tag and the GitHub release. Patches are automatic; for a minor or major, bump `version` in `package.json` and `VERSION` together. Add the CHANGELOG entry in the same commit
493
+ ---
12
494
 
13
- ## Docs
495
+ ## Further reading
14
496
 
15
- How to bootstrap a project with this config, and what each piece does: [docs/](docs/README.md)
497
+ | Document | Covers |
498
+ |---|---|
499
+ | [docs/quickstart.md](docs/quickstart.md) | First task in five minutes |
500
+ | [docs/README.md](docs/README.md) | Every file in the config, bootstrap details, common questions |
501
+ | [docs/skills.md](docs/skills.md) | Skill catalog: invocation and who uses each one |
502
+ | [docs/adr/](docs/adr/) | Why the config is the way it is |
503
+ | [AGENTS.md](AGENTS.md) | The shared base every agent reads |
504
+ | [CHANGELOG.md](CHANGELOG.md) | What changed, by version |
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.1
1
+ 0.1.3
package/bin/agents.mjs CHANGED
@@ -3,43 +3,67 @@
3
3
  // agents init [dir] copy the config into the project (default: current directory); never overwrites
4
4
  // agents update [dir] refresh files the project did not edit, add new ones, delete untouched ones that left the config
5
5
  // agents check [dir] report missing, outdated, edited and obsolete files; exit 1 on drift
6
+ // agents print-agents-md print this version's AGENTS.md in its managed block (--raw: the file alone)
6
7
  // Flags: --force (update also overwrites files edited in the project, except CLAUDE.md, settings.local.json and agent memory),
7
- // --dry-run (init/update: show what would change, write nothing).
8
- import { resolve } from "node:path";
9
- import { DEFAULT_SRC, drift, install, readPackage, report } from "../scripts/bootstrap.mjs";
8
+ // --dry-run (init/update: show what would change, write nothing),
9
+ // --agents-md=keep|overwrite|append|print (a project AGENTS.md that differs; asked in a terminal, default keep).
10
+ import { readFileSync } from "node:fs";
11
+ import { join, resolve } from "node:path";
12
+ import { AGENTS_MD_CHOICES, DEFAULT_SRC, agentsBlock, drift, install, readPackage, report, settleAgentsMd } from "../scripts/bootstrap.mjs";
10
13
 
11
- const HELP = `Usage: agents <command> [dir] [--force] [--dry-run]
14
+ const HELP = `Usage: agents <command> [dir] [flags]
12
15
 
13
16
  Commands:
14
- init copy the config into the project (never overwrites existing files)
15
- update refresh untouched files, add new ones, delete untouched files that left the config
16
- check report missing, outdated, edited and obsolete files (exit 1 on drift)
17
+ init copy the config into the project (never overwrites existing files)
18
+ update refresh untouched files, add new ones, delete untouched files that left the config
19
+ check report missing, outdated, edited and obsolete files (exit 1 on drift)
20
+ print-agents-md print the config AGENTS.md inside its managed block (--raw: the file alone)
17
21
 
18
22
  Flags:
19
- --force update also overwrites files edited in the project
20
- (CLAUDE.md, .claude/settings.local.json and .claude/agent-memory/** are never touched)
21
- --dry-run show what init/update would change without writing
23
+ --force update also overwrites files edited in the project
24
+ (CLAUDE.md, .claude/settings.local.json and .claude/agent-memory/** are never touched)
25
+ --dry-run show what init/update would change without writing
26
+ --agents-md=<choice>
27
+ when the project already has its own AGENTS.md (asked in a terminal; default keep):
28
+ keep, overwrite, append (managed block refreshed by later updates) or print
22
29
 
23
30
  dir defaults to the current directory.`;
24
31
 
25
32
  const args = process.argv.slice(2);
26
- const flags = new Set(args.filter((a) => a.startsWith("-")));
27
- const [command, dir] = args.filter((a) => !a.startsWith("-"));
33
+ const flags = new Set(), opts = {}, pos = [];
34
+ for (let i = 0; i < args.length; i++) {
35
+ const a = args[i];
36
+ if (a === "--agents-md") opts["agents-md"] = args[++i] ?? "";
37
+ else if (a.startsWith("--") && a.includes("=")) opts[a.slice(2, a.indexOf("="))] = a.slice(a.indexOf("=") + 1);
38
+ else if (a.startsWith("-")) flags.add(a);
39
+ else pos.push(a);
40
+ }
41
+ const [command, dir] = pos;
28
42
  const pkg = readPackage(DEFAULT_SRC);
29
43
 
30
- if (flags.has("--version") || flags.has("-v")) { console.log(pkg.version); process.exit(0); }
31
- if (!command || command === "help" || flags.has("--help") || flags.has("-h")) { console.log(`${pkg.name} ${pkg.version}\n\n${HELP}`); process.exit(command ? 0 : 2); }
32
- if (!["init", "update", "check"].includes(command)) { console.error(`unknown command: ${command}\n\n${HELP}`); process.exit(2); }
44
+ async function main() {
45
+ if (flags.has("--version") || flags.has("-v")) { console.log(pkg.version); return 0; }
46
+ if (!command || command === "help" || flags.has("--help") || flags.has("-h")) { console.log(`${pkg.name} ${pkg.version}\n\n${HELP}`); return command ? 0 : 2; }
47
+ if (!["init", "update", "check", "print-agents-md"].includes(command)) { console.error(`unknown command: ${command}\n\n${HELP}`); return 2; }
48
+ const agentsMd = opts["agents-md"];
49
+ if (agentsMd !== undefined && !AGENTS_MD_CHOICES.includes(agentsMd)) { console.error(`--agents-md must be one of: ${AGENTS_MD_CHOICES.join(", ")}`); return 2; }
50
+
51
+ if (command === "print-agents-md") {
52
+ process.stdout.write(flags.has("--raw") ? readFileSync(join(DEFAULT_SRC, "AGENTS.md"), "utf8") : agentsBlock(DEFAULT_SRC));
53
+ return 0;
54
+ }
33
55
 
34
- try {
35
56
  const target = resolve(dir ?? process.cwd());
36
- const r = install({ target, mode: command, force: flags.has("--force"), dryRun: flags.has("--dry-run") });
57
+ const dryRun = flags.has("--dry-run");
58
+ const r = install({ target, mode: command, force: flags.has("--force"), dryRun });
37
59
  report(r);
38
- if (command === "init" && !flags.has("--dry-run")) {
60
+ if (command !== "check") {
61
+ await settleAgentsMd({ target, r, choice: agentsMd, dryRun, interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY) });
62
+ }
63
+ if (command === "init" && !dryRun) {
39
64
  console.log("\nnext: fill in purpose and commands in CLAUDE.md; optional: `npx impeccable install` (for /ui) and `/graphify .`");
40
65
  }
41
- process.exit(command === "check" && drift(r) ? 1 : 0);
42
- } catch (e) {
43
- console.error(e.message);
44
- process.exit(2);
66
+ return command === "check" && drift(r) ? 1 : 0;
45
67
  }
68
+
69
+ main().then((code) => { process.exitCode = code; }, (e) => { console.error(e.message); process.exitCode = 2; });
package/bin/chew.mjs ADDED
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env node
2
+ // chew: the global CLI. Install once with `npm install -g @evertonferreira96/agents`, then, in any project:
3
+ // chew agents setup:pull [dir] download the latest published config and install it (fresh project: everything;
4
+ // configured project: refresh what was not edited there, add new files, drop obsolete ones)
5
+ // chew agents setup:check [dir] compare the project with the latest published config; exit 1 on drift
6
+ // chew agents setup:print print the latest config AGENTS.md in its managed block (--raw: the file alone)
7
+ // Flags: --force (also overwrite files edited in the project), --dry-run (show, write nothing),
8
+ // --local (use the version installed with chew instead of downloading the latest),
9
+ // --agents-md=keep|overwrite|append|print (a project AGENTS.md that differs; asked in a terminal, default keep).
10
+ // The install runs with the engine of the downloaded version, so an old global chew still applies the newest setup.
11
+ // Test hook: CHEW_TARBALL=<path to a .tgz from `npm pack`> replaces the registry download.
12
+ import { spawnSync } from "node:child_process";
13
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
14
+ import { tmpdir } from "node:os";
15
+ import { dirname, join, resolve, sep } from "node:path";
16
+ import { fileURLToPath, pathToFileURL } from "node:url";
17
+ import { gunzipSync } from "node:zlib";
18
+
19
+ const ROOT = fileURLToPath(new URL("..", import.meta.url));
20
+ const SELF = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"));
21
+ const REGISTRY = SELF.publishConfig?.registry || "https://registry.npmjs.org/";
22
+ const SCOPE = SELF.name.startsWith("@") ? SELF.name.split("/")[0] : null;
23
+
24
+ const HELP = `chew ${SELF.version}
25
+
26
+ Usage: chew agents <command> [dir] [flags]
27
+
28
+ Commands:
29
+ setup:pull download the latest ${SELF.name} and install it into the project
30
+ (fresh project: everything; configured project: refresh what was not edited there)
31
+ setup:check compare the project with the latest published config (exit 1 on drift)
32
+ setup:print print the latest config AGENTS.md inside its managed block, to concatenate into yours
33
+ (--raw: the file alone, without the markers)
34
+
35
+ Flags:
36
+ --force also overwrite files edited in the project
37
+ (CLAUDE.md, .claude/settings.local.json and .claude/agent-memory/** are never touched)
38
+ --dry-run show what would change without writing
39
+ --local use the version installed with chew (${SELF.version}) instead of downloading
40
+ --agents-md=<choice>
41
+ when the project already has its own AGENTS.md (asked in a terminal; default keep):
42
+ keep leave yours as it is
43
+ overwrite replace it with the config version
44
+ append add the config version at the end, in a managed block that later pulls refresh
45
+ print print the config version to concatenate yourself
46
+
47
+ dir defaults to the current directory.`;
48
+
49
+ // npm pinned to the public registry, also for the package scope, so a user npmrc pointing elsewhere (Azure feed) does not matter.
50
+ function npm(args, cwd) {
51
+ const full = [...args, "--registry", REGISTRY, ...(SCOPE ? [`--${SCOPE}:registry=${REGISTRY}`] : [])];
52
+ const win = process.platform === "win32";
53
+ const r = spawnSync(win ? "npm.cmd" : "npm", full, { cwd, encoding: "utf8", shell: win });
54
+ if (r.status !== 0) throw new Error((r.stderr || r.stdout || r.error?.message || "npm failed").trim().split("\n").slice(-3).join(" | "));
55
+ return r.stdout;
56
+ }
57
+
58
+ // Minimal reader for an npm tarball (ustar, GNU long names, pax paths); strips the leading "package/" and keeps file modes.
59
+ function untar(gz, dest) {
60
+ const buf = gunzipSync(gz);
61
+ const root = resolve(dest);
62
+ let off = 0, longName = null;
63
+ while (off + 512 <= buf.length) {
64
+ const h = buf.subarray(off, off + 512);
65
+ if (h.every((b) => b === 0)) break;
66
+ const field = (start, len) => h.subarray(start, start + len).toString("utf8").replace(/\0[\s\S]*$/, "");
67
+ let name = field(0, 100);
68
+ const prefix = field(345, 155);
69
+ if (prefix) name = `${prefix}/${name}`;
70
+ const size = parseInt(field(124, 12).trim() || "0", 8);
71
+ const mode = parseInt(field(100, 8).trim() || "644", 8) & 0o777;
72
+ const type = String.fromCharCode(h[156] || 48);
73
+ off += 512;
74
+ const data = buf.subarray(off, off + size);
75
+ off += Math.ceil(size / 512) * 512;
76
+ if (type === "L") { longName = data.toString("utf8").replace(/\0[\s\S]*$/, ""); continue; }
77
+ if (type === "x") { const m = /\d+ path=([^\n]+)\n/.exec(data.toString("utf8")); if (m) longName = m[1]; continue; }
78
+ if (longName) { name = longName; longName = null; }
79
+ if (type !== "0" && type !== "7") continue; // directories are created on demand; npm tarballs carry no links
80
+ const out = resolve(root, name.replace(/^package\//, ""));
81
+ if (!out.startsWith(root + sep)) throw new Error(`unsafe path in tarball: ${name}`);
82
+ mkdirSync(dirname(out), { recursive: true });
83
+ writeFileSync(out, data, { mode: mode || 0o644 });
84
+ }
85
+ }
86
+
87
+ // Directory with the config to install: the latest published version, or the one installed with chew.
88
+ function fetchConfig(local) {
89
+ const installed = { dir: ROOT, version: SELF.version, cleanup() {} };
90
+ if (local) return installed;
91
+ const tmp = mkdtempSync(join(tmpdir(), "chew-"));
92
+ const cleanup = () => rmSync(tmp, { recursive: true, force: true });
93
+ try {
94
+ let tarball = process.env.CHEW_TARBALL;
95
+ if (!tarball) {
96
+ const packed = JSON.parse(npm(["pack", `${SELF.name}@latest`, "--pack-destination", ".", "--json"], tmp));
97
+ tarball = join(tmp, packed[0].filename);
98
+ }
99
+ const dir = join(tmp, "package");
100
+ untar(readFileSync(tarball), dir);
101
+ const version = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")).version;
102
+ return { dir, version, cleanup };
103
+ } catch (e) {
104
+ cleanup();
105
+ console.warn(`could not download the latest ${SELF.name} (${e.message}); using the installed ${SELF.version}`);
106
+ return installed;
107
+ }
108
+ }
109
+
110
+ const newer = (a, b) => { const x = a.split(".").map(Number), y = b.split(".").map(Number); for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] > y[i]; return false; };
111
+
112
+ const COMMANDS = ["setup:pull", "setup:check", "setup:print"];
113
+ const AGENTS_MD = ["keep", "overwrite", "append", "print"];
114
+
115
+ // Flags, `--name=value` / `--name value` options and positionals.
116
+ function parseArgs(argv) {
117
+ const flags = new Set(), opts = {}, pos = [];
118
+ for (let i = 0; i < argv.length; i++) {
119
+ const a = argv[i];
120
+ if (a === "--agents-md") opts["agents-md"] = argv[++i] ?? "";
121
+ else if (a.startsWith("--") && a.includes("=")) { const k = a.slice(2, a.indexOf("=")); opts[k] = a.slice(a.indexOf("=") + 1); }
122
+ else if (a.startsWith("-")) flags.add(a);
123
+ else pos.push(a);
124
+ }
125
+ return { flags, opts, pos };
126
+ }
127
+
128
+ async function main(argv) {
129
+ const { flags, opts, pos } = parseArgs(argv);
130
+ const [ns, command, dir] = pos;
131
+ if (flags.has("--version") || flags.has("-v")) { console.log(SELF.version); return 0; }
132
+ if (!ns || ns === "help" || flags.has("--help") || flags.has("-h")) { console.log(HELP); return ns ? 0 : 2; }
133
+ const agentsMd = opts["agents-md"];
134
+ if (agentsMd !== undefined && !AGENTS_MD.includes(agentsMd)) { console.error(`--agents-md must be one of: ${AGENTS_MD.join(", ")}`); return 2; }
135
+ if (ns !== "agents" || !COMMANDS.includes(command)) {
136
+ console.error(`unknown command: chew ${[ns, command].filter(Boolean).join(" ")}\n\n${HELP}`);
137
+ return 2;
138
+ }
139
+
140
+ if (command === "setup:print") { // stdout carries only the content, so it can be redirected or piped
141
+ const config = fetchConfig(flags.has("--local"));
142
+ try {
143
+ const engine = await import(pathToFileURL(join(config.dir, "scripts", "bootstrap.mjs")).href);
144
+ const raw = readFileSync(join(config.dir, "AGENTS.md"), "utf8");
145
+ process.stdout.write(flags.has("--raw") || !engine.agentsBlock ? raw : engine.agentsBlock(config.dir));
146
+ return 0;
147
+ } finally {
148
+ config.cleanup();
149
+ }
150
+ }
151
+
152
+ const target = resolve(dir ?? process.cwd());
153
+ if (!existsSync(target)) throw new Error(`target is not a directory: ${target}`);
154
+ try { if (JSON.parse(readFileSync(join(target, "package.json"), "utf8")).name === SELF.name) throw new Error("this is the config repo itself; run chew inside a project"); }
155
+ catch (e) { if (e.message.startsWith("this is the config repo")) throw e; }
156
+
157
+ const config = fetchConfig(flags.has("--local"));
158
+ try {
159
+ const engine = await import(pathToFileURL(join(config.dir, "scripts", "bootstrap.mjs")).href);
160
+ const mode = command === "setup:check" ? "check" : "update";
161
+ const r = engine.install({ src: config.dir, target, mode, force: flags.has("--force"), dryRun: flags.has("--dry-run") });
162
+ engine.report(r);
163
+ if (mode === "update" && engine.settleAgentsMd) {
164
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
165
+ await engine.settleAgentsMd({ src: config.dir, target, r, choice: agentsMd, dryRun: flags.has("--dry-run"), interactive });
166
+ }
167
+ if (mode === "update" && !flags.has("--dry-run") && r.written.some((w) => w.startsWith("CLAUDE.md"))) {
168
+ console.log("\nnext: fill in purpose and commands in CLAUDE.md; optional: `npx impeccable install` (for /ui) and `/graphify .`");
169
+ }
170
+ if (newer(config.version, SELF.version)) console.log(`\nchew itself is ${SELF.version} (latest ${config.version}); update it with: npm install -g ${SELF.name}@latest`);
171
+ return mode === "check" && engine.drift(r) ? 1 : 0;
172
+ } finally {
173
+ config.cleanup();
174
+ }
175
+ }
176
+
177
+ main(process.argv.slice(2)).then((code) => { process.exitCode = code; }, (e) => { console.error(e.message); process.exitCode = 2; });
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@evertonferreira96/agents",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Personal agents, rules, hooks and skills for Claude Code (portable to Cursor and Codex through AGENTS.md), installable into any project.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "agents": "bin/agents.mjs"
7
+ "agents": "bin/agents.mjs",
8
+ "chew": "bin/chew.mjs"
8
9
  },
9
10
  "files": [
10
11
  "bin/",
@@ -25,6 +25,12 @@ export const GITIGNORE = [".claude/worktrees/", ".claude/handoffs/", ".claude/.c
25
25
  export const MANIFEST = ".claude/config-manifest.json";
26
26
  export const SOURCE_FILE = ".claude/config-source.json";
27
27
  export const DEFAULT_SRC = fileURLToPath(new URL("..", import.meta.url));
28
+ // AGENTS.md of a project that already had its own: the config version can live inside it as a managed block,
29
+ // which `update` refreshes in place without touching the rest of the file.
30
+ export const AGENTS_FILE = "AGENTS.md";
31
+ export const BLOCK_START = "<!-- agents-config:start -->";
32
+ export const BLOCK_END = "<!-- agents-config:end -->";
33
+ export const AGENTS_MD_CHOICES = ["keep", "overwrite", "append", "print"];
28
34
 
29
35
  const sha = (p) => createHash("sha256").update(readFileSync(p)).digest("hex");
30
36
  const readJson = (p) => { try { return JSON.parse(readFileSync(p, "utf8")); } catch { return null; } };
@@ -61,6 +67,82 @@ function removeEmptyParents(target, rel) {
61
67
  }
62
68
  }
63
69
 
70
+ // The config's AGENTS.md wrapped in the managed-block markers (LF line endings, trailing newline).
71
+ export function agentsBlock(src = DEFAULT_SRC) {
72
+ const body = readFileSync(join(src, AGENTS_FILE), "utf8").replace(/\r\n/g, "\n").trimEnd();
73
+ const note = `<!-- Managed by ${readPackage(src).name}: \`chew agents setup:pull\` refreshes this block. Edit outside the markers. -->`;
74
+ return `${BLOCK_START}\n${note}\n${body}\n${BLOCK_END}\n`;
75
+ }
76
+
77
+ // Position of the managed block in `text` (normalized to LF), or null when the file has none.
78
+ function findBlock(text) {
79
+ const t = text.replace(/\r\n/g, "\n");
80
+ const start = t.indexOf(BLOCK_START);
81
+ const end = start < 0 ? -1 : t.indexOf(BLOCK_END, start + BLOCK_START.length);
82
+ return end < 0 ? null : { text: t, start, end: end + BLOCK_END.length, eol: text.includes("\r\n") ? "\r\n" : "\n" };
83
+ }
84
+
85
+ function recordHash(target, rel, hash) {
86
+ const p = join(target, MANIFEST);
87
+ const m = readJson(p);
88
+ if (!m?.files) return;
89
+ m.files[rel] = hash;
90
+ writeFileSync(p, JSON.stringify(m, null, 2) + "\n");
91
+ }
92
+
93
+ // What to do with a project AGENTS.md that differs from the config and has no managed block:
94
+ // keep (nothing), overwrite (config version, refreshed by later updates), append (managed block at the end, refreshed by
95
+ // later updates), print (returns the managed block for the user to paste). The manifest records the config hash, so a
96
+ // file with a block never counts as untouched and is never overwritten whole.
97
+ export function applyAgentsMd({ src = DEFAULT_SRC, target, choice, dryRun = false }) {
98
+ if (!AGENTS_MD_CHOICES.includes(choice)) throw new Error(`unknown AGENTS.md choice: ${choice} (${AGENTS_MD_CHOICES.join(", ")})`);
99
+ const abs = join(src, AGENTS_FILE);
100
+ const dst = join(target, AGENTS_FILE);
101
+ if (choice === "print") return { choice, text: agentsBlock(src) };
102
+ if (choice === "keep" || dryRun) return { choice };
103
+ if (choice === "overwrite") copyFileSync(abs, dst);
104
+ if (choice === "append") {
105
+ const cur = existsSync(dst) ? readFileSync(dst, "utf8") : "";
106
+ const eol = cur.includes("\r\n") ? "\r\n" : "\n";
107
+ const block = agentsBlock(src).replace(/\n/g, eol);
108
+ const found = findBlock(cur);
109
+ if (found) writeFileSync(dst, (found.text.slice(0, found.start) + agentsBlock(src).trimEnd() + found.text.slice(found.end)).replace(/\n/g, eol));
110
+ else writeFileSync(dst, cur + (cur === "" || cur.endsWith(eol + eol) ? "" : cur.endsWith(eol) ? eol : eol + eol) + block);
111
+ }
112
+ recordHash(target, AGENTS_FILE, sha(abs));
113
+ return { choice };
114
+ }
115
+
116
+ // Ask once in a terminal (default keep).
117
+ export async function askAgentsMd({ input = process.stdin, output = process.stdout } = {}) {
118
+ const { createInterface } = await import("node:readline/promises");
119
+ output.write(`\nAGENTS.md already exists in this project and differs from the config. What should happen to it?
120
+ [k] keep yours as it is (default)
121
+ [o] overwrite it with the config version
122
+ [a] append the config version at the end, in a managed block that later pulls keep current
123
+ [p] print the config version here, to concatenate it yourself\n`);
124
+ const rl = createInterface({ input, output });
125
+ try {
126
+ const answer = (await rl.question("Choice [k/o/a/p]: ")).trim().toLowerCase();
127
+ return { o: "overwrite", a: "append", p: "print" }[answer[0]] ?? "keep";
128
+ } finally {
129
+ rl.close();
130
+ }
131
+ }
132
+
133
+ // After init/update: settle an AGENTS.md conflict with `choice`, or ask when interactive, or keep and say how to choose.
134
+ export async function settleAgentsMd({ src = DEFAULT_SRC, target, r, choice, dryRun = false, interactive = false, log = console.log }) {
135
+ if (!r.agentsMdConflict) return null;
136
+ const picked = choice || (interactive ? await askAgentsMd() : "keep");
137
+ const out = applyAgentsMd({ src, target, choice: picked, dryRun });
138
+ const dry = dryRun ? " (dry run: nothing written)" : "";
139
+ if (picked === "keep") log(`\nAGENTS.md: kept yours. To change that: --agents-md=overwrite, --agents-md=append or --agents-md=print`);
140
+ if (picked === "overwrite") log(`\nAGENTS.md: overwritten with the config version${dry}`);
141
+ if (picked === "append") log(`\nAGENTS.md: config version appended at the end in a managed block${dry}; later pulls refresh only that block`);
142
+ if (picked === "print") log(`\nAGENTS.md: the config version is below. Paste it into yours with the markers, so later pulls refresh it:\n\n${out.text}`);
143
+ return picked;
144
+ }
145
+
64
146
  // mode: "init" (copy what is missing), "update" (also refresh untouched files and drop obsolete ones), "check" (report only).
65
147
  export function install({ src = DEFAULT_SRC, target, mode = "init", force = false, dryRun = false }) {
66
148
  if (!["init", "update", "check"].includes(mode)) throw new Error(`unknown mode: ${mode}`);
@@ -86,6 +168,20 @@ export function install({ src = DEFAULT_SRC, target, mode = "init", force = fals
86
168
  }
87
169
  const have = sha(dst);
88
170
  if (have === want) { r.same++; next[rel] = want; continue; }
171
+ if (rel === AGENTS_FILE) {
172
+ const cur = readFileSync(dst, "utf8");
173
+ const found = findBlock(cur);
174
+ if (found) { // the project's own AGENTS.md carrying the config in a managed block: only the block is ours
175
+ next[rel] = want; // never the project file's hash, so no engine treats the file as untouched and replaces it whole
176
+ const block = agentsBlock(src).trimEnd();
177
+ if (found.text.slice(found.start, found.end) === block) { r.same++; continue; }
178
+ if (mode === "check") { r.outdated.push(`${rel} (managed block)`); continue; }
179
+ if (mode === "init") { r.kept.push(`${rel} (managed block outdated; run update)`); continue; }
180
+ if (write) writeFileSync(dst, (found.text.slice(0, found.start) + block + found.text.slice(found.end)).replace(/\n/g, found.eol));
181
+ r.updated.push(`${rel} (managed block)`);
182
+ continue;
183
+ }
184
+ }
89
185
  const untouched = installed?.[rel] === have;
90
186
  if (mode === "check") { (untouched ? r.outdated : r.edited).push(rel); continue; }
91
187
  if (mode === "update" && (untouched || force) && !PROTECTED.test(rel)) {
@@ -94,6 +190,7 @@ export function install({ src = DEFAULT_SRC, target, mode = "init", force = fals
94
190
  continue;
95
191
  }
96
192
  r.kept.push(rel);
193
+ if (rel === AGENTS_FILE) r.agentsMdConflict = true; // the CLI offers keep / overwrite / append / print
97
194
  if (installed?.[rel]) next[rel] = installed[rel];
98
195
  }
99
196
 
@@ -155,9 +252,10 @@ export function report(r, log = console.log) {
155
252
  list("kept", r.kept, r.mode === "init" ? " (differs; init never overwrites)" : " (edited here)");
156
253
  list("kept", r.obsoleteKept, " (left the config, but edited here)");
157
254
  if (r.written.length) { log("written:"); for (const w of r.written) log(` ${w}`); }
158
- if (r.mode === "update" && !r.hadManifest && r.kept.length) log("no manifest from a previous install, so differing files count as edited; review them and run `update --force` once to adopt the config versions");
159
- else if (r.kept.length && r.mode === "update") log("`update --force` overwrites the files kept above (protected files excepted)");
160
- else if (r.kept.length) log("run `update` to refresh differing files");
255
+ const others = r.kept.filter((k) => !(r.agentsMdConflict && k === AGENTS_FILE)); // AGENTS.md gets its own choice (settleAgentsMd)
256
+ if (r.mode === "update" && !r.hadManifest && others.length) log("no manifest from a previous install, so differing files count as edited; review them and run `update --force` once to adopt the config versions");
257
+ else if (others.length && r.mode === "update") log("`update --force` overwrites the files kept above (protected files excepted)");
258
+ else if (others.length) log("run `update` to refresh differing files");
161
259
  }
162
260
 
163
261
  // Legacy CLI (scripts/bootstrap.mjs <target> [--check|--force] | --sync).
@@ -168,7 +266,7 @@ function legacy(argv) {
168
266
  const info = readJson(join(process.cwd(), SOURCE_FILE));
169
267
  if (!info) { console.error(`--sync needs ${SOURCE_FILE} (written by a previous install)`); return 2; }
170
268
  if (!info.source || !existsSync(info.source)) {
171
- console.log(`This project was installed from the ${info.package || "config"} package. Update it with: npx ${info.package || "<package>"}@latest update`);
269
+ console.log(`This project was installed from the ${info.package || "config"} package. Update it with: chew agents setup:pull (global CLI) or npx ${info.package || "<package>"}@latest update`);
172
270
  return 0;
173
271
  }
174
272
  src = info.source; target = process.cwd();