@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 +499 -10
- package/VERSION +1 -1
- package/bin/agents.mjs +46 -22
- package/bin/chew.mjs +177 -0
- package/package.json +3 -2
- package/scripts/bootstrap.mjs +102 -4
package/README.md
CHANGED
|
@@ -1,15 +1,504 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
1
3
|
# agents
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
**Agents, rules, hooks and skills for Claude Code, installed into any project with one command.**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@evertonferreira96/agents)
|
|
8
|
+
[](package.json)
|
|
9
|
+
[](docs/README.md)
|
|
10
|
+
[](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
|
-
|
|
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
|
-
##
|
|
495
|
+
## Further reading
|
|
14
496
|
|
|
15
|
-
|
|
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
|
+
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
|
-
|
|
9
|
-
import {
|
|
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] [
|
|
14
|
+
const HELP = `Usage: agents <command> [dir] [flags]
|
|
12
15
|
|
|
13
16
|
Commands:
|
|
14
|
-
init
|
|
15
|
-
update
|
|
16
|
-
check
|
|
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
|
|
20
|
-
|
|
21
|
-
--dry-run
|
|
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(
|
|
27
|
-
|
|
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
|
-
|
|
31
|
-
if (
|
|
32
|
-
if (!
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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/",
|
package/scripts/bootstrap.mjs
CHANGED
|
@@ -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
|
-
|
|
159
|
-
|
|
160
|
-
else if (r.
|
|
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();
|