@eric0117/agentforge 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +275 -0
  3. package/dist/add-agent.js +145 -0
  4. package/dist/add-skill.js +185 -0
  5. package/dist/agent-prompt.js +211 -0
  6. package/dist/agentforge-config.js +106 -0
  7. package/dist/agents/claude.js +46 -0
  8. package/dist/agents/codex.js +67 -0
  9. package/dist/agents/cursor.js +54 -0
  10. package/dist/agents/index.js +15 -0
  11. package/dist/agents/io.js +252 -0
  12. package/dist/agents/types.js +1 -0
  13. package/dist/cli.js +374 -0
  14. package/dist/confirm.js +20 -0
  15. package/dist/doctor.js +223 -0
  16. package/dist/enter.js +85 -0
  17. package/dist/init.js +272 -0
  18. package/dist/lang-prompt.js +88 -0
  19. package/dist/list-skills.js +120 -0
  20. package/dist/logo.js +181 -0
  21. package/dist/path-prompt.js +148 -0
  22. package/dist/remove-agent.js +63 -0
  23. package/dist/remove-skill.js +88 -0
  24. package/dist/rename.js +222 -0
  25. package/dist/skill-prompt.js +199 -0
  26. package/dist/skills-data.js +727 -0
  27. package/dist/sync-skills.js +59 -0
  28. package/dist/templates/CLAUDE.md.tpl +141 -0
  29. package/dist/templates/context-handoff.SKILL.md.tpl +222 -0
  30. package/dist/templates/cross-repo-impact.SKILL.md.tpl +241 -0
  31. package/dist/templates/feature-retro.SKILL.md.tpl +312 -0
  32. package/dist/templates/feature-start.SKILL.md.tpl +631 -0
  33. package/dist/templates/history.SKILL.md.tpl +165 -0
  34. package/dist/templates/incident-context.SKILL.md.tpl +260 -0
  35. package/dist/templates/pr-create.SKILL.md.tpl +403 -0
  36. package/dist/templates/pr-review-analyze.SKILL.md.tpl +303 -0
  37. package/dist/templates/pre-deploy-check.SKILL.md.tpl +350 -0
  38. package/dist/templates/project-router.SKILL.md.tpl +55 -0
  39. package/dist/templates/release-coordinate.SKILL.md.tpl +209 -0
  40. package/package.json +54 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eric0117
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,275 @@
1
+ # agentforge
2
+
3
+ <p align="center">
4
+ <img src="https://github.com/user-attachments/assets/68277906-3c7f-442e-a68d-2ab2631698ab" width="720" alt="agentforge" />
5
+ </p>
6
+
7
+ <p align="center">
8
+ <a href="https://www.npmjs.com/package/@eric0117/agentforge"><img src="https://img.shields.io/npm/v/@eric0117/agentforge.svg?style=flat-square" alt="npm version" /></a>
9
+ <a href="https://www.npmjs.com/package/@eric0117/agentforge"><img src="https://img.shields.io/npm/dm/@eric0117/agentforge.svg?style=flat-square" alt="npm downloads" /></a>
10
+ <a href="./LICENSE"><img src="https://img.shields.io/npm/l/@eric0117/agentforge.svg?style=flat-square" alt="license" /></a>
11
+ <img src="https://img.shields.io/node/v/@eric0117/agentforge.svg?style=flat-square" alt="node" />
12
+ </p>
13
+
14
+ > Multi-repo workspace bootstrapper for **Claude Code**, **Cursor**, and **OpenAI Codex CLI**.
15
+
16
+ **You're a developer. You don't work on one thing at a time. And when you do, it never lives in one repo.**
17
+
18
+ A bug fix touches the API and the admin panel. A feature ships PRs across three services. Tomorrow you're back on yesterday's work — and a teammate just edited the same files.
19
+
20
+ `agentforge` makes that the default workflow — not the chaos. One feature = one slug = a git worktree per repo, side by side on disk. Multiple features in flight, each with its own AI session. Skills triggered by natural language — no commands to memorize. Finished work archived with enough context that next month's "how did we handle X?" returns a real answer.
21
+
22
+ It does **not** ship its own AI runtime — bring your own Claude Code / Cursor / Codex CLI.
23
+
24
+ ---
25
+
26
+ ## Why
27
+
28
+ Working across several repos at once is painful with a single AI agent:
29
+
30
+ - Your session is rooted in one repo, but the change touches three.
31
+ - You lose context when you switch terminals to a different repo.
32
+ - Parallel features step on each other's branches.
33
+ - "How did we solve this last time?" disappears the moment a PR is merged.
34
+
35
+ agentforge gives you a flat directory layout where every feature has its own per-repo git worktrees, every AI agent gets the same set of skills (in the same language), and finished work is archived with enough metadata to be queried later.
36
+
37
+ ---
38
+
39
+ ## Quick start
40
+
41
+ ```bash
42
+ # Install (scoped package — command on PATH is still `agentforge`)
43
+ npm install -g @eric0117/agentforge
44
+
45
+ # Bootstrap a workspace — interactive prompts walk you through
46
+ # language (en / ko / ja) and which agents to install (Claude / Cursor / Codex)
47
+ mkdir my-workspace && cd my-workspace
48
+ agentforge init
49
+ ```
50
+
51
+ <p align="center">
52
+ <img src="https://github.com/user-attachments/assets/0eb690b2-afaf-475e-85f2-5ef33a99b118" width="720" alt="agentforge init — interactive prompts" />
53
+ </p>
54
+
55
+ ```bash
56
+ # Clone your repos into repos/
57
+ git clone https://github.com/your-org/backend-api.git repos/backend-api
58
+ git clone https://github.com/your-org/admin-web.git repos/admin-web
59
+
60
+ # Start working — open the AI CLI of your choice from the workspace root
61
+ claude # or: cursor . / codex
62
+ ```
63
+
64
+ > Prefer non-interactive? `agentforge init . --agent all --lang en --yes` skips every prompt.
65
+
66
+ From inside the session, describe what you want in natural language. agentforge skills pick the right action — no command memorization needed:
67
+
68
+ > "Let's start a new feature: tighten the rate limit"
69
+
70
+ → `agentforge-feature-start` proposes a slug, asks which repos are in scope, creates worktrees under `anvil/<slug>/<repo>/`, and (in Claude Code) dispatches a background session you can switch to with `←`.
71
+
72
+ ---
73
+
74
+ ## Directory layout
75
+
76
+ ```
77
+ my-workspace/
78
+ ├── repos/ # main branch of each repo (read-only / explore)
79
+ │ ├── backend-api/
80
+ │ └── admin-web/
81
+ ├── anvil/ # IN-PROGRESS features only
82
+ │ └── <slug>/ # e.g. 260524-feat-rate-limit
83
+ │ ├── backend-api/ # git worktree on a feature branch
84
+ │ ├── admin-web/ # git worktree on a feature branch
85
+ │ └── CLAUDE.md # feature description + context + repo list
86
+ ├── artifacts/ # closed features, by completion date
87
+ │ └── 20260524/
88
+ │ └── <slug>/
89
+ │ ├── CLAUDE.md # moved here at retro time
90
+ │ ├── RETRO.md # retrospective
91
+ │ ├── refs.json # per-repo branch / HEAD / PR pointers
92
+ │ ├── plans/ # plan files
93
+ │ └── sessions/ # AI session transcripts
94
+ ├── agentforge/ # workspace metadata
95
+ │ ├── config.json # which agents, which language
96
+ │ ├── skills/ # master skill files (single source of truth)
97
+ │ └── log.jsonl # append-only activity log
98
+ └── .claude/skills/ # per-agent skill copies (auto-generated)
99
+ .cursor/rules/
100
+ .agents/skills/ # codex
101
+ ```
102
+
103
+ The filesystem is the source of truth — there is no separate metadata file to drift. `ls anvil/` shows what's in flight; `ls artifacts/` shows what's done.
104
+
105
+ ---
106
+
107
+ ## How a feature flows
108
+
109
+ | Step | What you say | Skill that fires |
110
+ |---|---|---|
111
+ | 1. Question / explore | "Where is the auth handler in the backend API?" | `agentforge-project-router` |
112
+ | 2. Discover something to change | "Let's fix this — start a feature" | `agentforge-feature-start` |
113
+ | 3. (Optional) plan the work | "How should we split this?" | (any agent — plan it together) |
114
+ | 4. Implement | (regular coding in the dispatched session) | — |
115
+ | 5. Check blast radius before merging | "Where else is `X` used?" | `agentforge-cross-repo-impact` |
116
+ | 6. Pre-merge ops sanity check | "Anything ops needs before I ship?" | `agentforge-pre-deploy-check` |
117
+ | 7. Open PRs for the feature | "Open PRs for this feature" | `agentforge-pr-create` |
118
+ | 8. Plan the merge / deploy order | "Which PR first?" | `agentforge-release-coordinate` |
119
+ | 9. Audit review comments | "What do we need to fix from the review?" | `agentforge-pr-review-analyze` |
120
+ | 10. Hand off mid-flight (optional) | "I'm going on vacation — package this up" | `agentforge-context-handoff` |
121
+ | 11. Close the feature | "We're done — write a retro" | `agentforge-feature-retro` |
122
+
123
+ You don't have to remember the skill names — they're triggered by natural language, in English / 한국어 / 日本語.
124
+
125
+ ---
126
+
127
+ ## Skills
128
+
129
+ All skills live in `agentforge/skills/` (master) and are auto-propagated to every installed agent (`.claude/skills/`, `.cursor/rules/`, `.agents/skills/`) by `agentforge sync-skills`.
130
+
131
+ | Skill | What it does |
132
+ |---|---|
133
+ | `agentforge-project-router` | Routes a natural-language question to the right `repos/<name>/`. |
134
+ | `agentforge-feature-start` | Creates per-repo git worktrees for a new feature; re-runnable to add repos to an existing one. Detects per-repo branch-naming conventions from history. |
135
+ | `agentforge-cross-repo-impact` | Traces the blast radius of a change across every repo in the workspace. |
136
+ | `agentforge-pre-deploy-check` | Surfaces non-code changes (migrations, env vars, cache keys, queue contracts, infra files) that ops needs to handle before merge. Read-only. |
137
+ | `agentforge-pr-create` | Opens one PR per repo for a feature; cross-links the PRs. Never force-pushes, never merges. |
138
+ | `agentforge-pr-review-analyze` | Pulls every review thread, verifies each against the live code, returns a prioritized action list. |
139
+ | `agentforge-release-coordinate` | Plans the multi-repo merge / deploy order with preconditions, wait conditions, and a reverse-order rollback playbook. Read-only. |
140
+ | `agentforge-context-handoff` | Packages a feature's current state into `HANDOFF.md` so another developer (or future-you) can pick up without context loss. |
141
+ | `agentforge-feature-retro` | Closes a feature: writes the retrospective, archives session logs and PR refs into `artifacts/`, removes worktrees, cleans up. |
142
+ | `agentforge-incident-context` | First-responder context for a production page: searches every repo for the alert clue, names recent committers, traces the call path. Read-only. |
143
+ | `agentforge-history` | Queries past features — "how did we handle X last time?", "which feature added Y?", with grounded file / commit / PR references. Read-only. |
144
+
145
+ Every skill that modifies state asks before destructive operations and writes activity to `agentforge/log.jsonl`.
146
+
147
+ You can also add **your own** skills (`agentforge add-skill`) — they get propagated to every agent the same way.
148
+
149
+ ---
150
+
151
+ ## CLI reference
152
+
153
+ ```
154
+ agentforge init [path] # bootstrap a workspace
155
+ agentforge add-agent [agents] [path] # add Claude / Cursor / Codex to an existing workspace
156
+ agentforge remove-agent <agent> [path]
157
+ agentforge list-skills [path] # show all installed skills
158
+ agentforge add-skill [path] # author a new skill
159
+ agentforge remove-skill <name> [path]
160
+ agentforge sync-skills [path] # propagate master skill edits to every agent
161
+ agentforge enter [slug] # cd into a feature worktree + launch claude
162
+ agentforge rename <old-slug> <new-slug> # rename a feature (worktrees, branch, CLAUDE.md)
163
+ agentforge doctor [path] # diagnose a workspace
164
+ agentforge help
165
+ ```
166
+
167
+ Flags:
168
+ - `--force` — overwrite per-agent files (always backs up to `.bak` first).
169
+ - `--yes` — non-interactive; assume yes on confirmation prompts.
170
+ - `--lang en|ko|ja` — language for skill bodies.
171
+ - `--agent claude,cursor,codex` or `--agent all` — which agents to scaffold for.
172
+
173
+ ---
174
+
175
+ ## Multi-agent support
176
+
177
+ agentforge writes the same skill set into the file layout each AI CLI expects:
178
+
179
+ | Agent | Skill location | Workspace guide |
180
+ |---|---|---|
181
+ | **Claude Code** | `.claude/skills/<id>/SKILL.md` | `CLAUDE.md` |
182
+ | **Cursor** | `.cursor/rules/<id>.mdc` | `.cursor/rules/CLAUDE.mdc` |
183
+ | **OpenAI Codex CLI** | `.agents/skills/<id>.md` | `AGENTS.md` |
184
+
185
+ Edit a file in `agentforge/skills/` and run `agentforge sync-skills` — every agent picks up the change with the previous version backed up to `.bak`.
186
+
187
+ ### How skills flow
188
+
189
+ ```
190
+ User runs: Source of truth:
191
+ ───────────── ─────────────────
192
+ agentforge add-skill ─────→ agentforge/skills/<id>.md
193
+
194
+ agentforge sync-skills ─────→ │ (renders the template into
195
+ │ each agent's expected layout)
196
+
197
+ ┌─────────────────┼─────────────────┐
198
+ ▼ ▼ ▼
199
+ .claude/skills/ .cursor/rules/ .agents/skills/
200
+ (Claude Code) (Cursor) (Codex CLI)
201
+ ```
202
+
203
+ The master copy in `agentforge/skills/` is the **single source of truth**. The per-agent
204
+ files are regenerated from it — never edit them directly; your changes will be
205
+ overwritten on the next `sync-skills`.
206
+
207
+ ---
208
+
209
+ ## Internationalization
210
+
211
+ Skills are stored as templates with a `{{OUTPUT_LANGUAGE_INSTRUCTION}}` placeholder. The workspace's `agentforge/config.json` `lang` field decides which language is baked in at install time (`en` / `ko` / `ja`). Switch languages by re-running `agentforge init --force-skills --lang <code>` — your master files in `agentforge/skills/` are preserved.
212
+
213
+ ---
214
+
215
+ ## Requirements
216
+
217
+ - Node.js ≥ 18
218
+ - `git` ≥ 2.20 (for `git worktree`)
219
+ - `gh` (GitHub CLI) — only for PR-related skills (`pr-create`, `pr-review-analyze`, `release-coordinate`)
220
+ - The AI CLI of your choice (Claude Code, Cursor, or Codex CLI) — for skill invocation
221
+ - Optional: `jq` — speeds up the activity log writer; the skills fall back to hand-built JSON if missing
222
+
223
+ ---
224
+
225
+ ## Conventions
226
+
227
+ - `repos/<name>/` is **read-only** — no code edits there. Use `agentforge-feature-start` to spawn a worktree.
228
+ - One feature = one slug = one directory under `anvil/` (and later `artifacts/<date>/`).
229
+ - Slug format: `<YYMMDD>-<kind>-<core>` where `<kind>` is `feat` / `fix` / `refactor` / `chore`.
230
+ - Branch names per repo follow each repo's own convention, detected from recent branches at feature-start time. They may differ from the slug.
231
+ - `anvil/` only contains in-progress work. Completed features move to `artifacts/<YYYYMMDD>/<slug>/`.
232
+
233
+ ---
234
+
235
+ ## FAQ
236
+
237
+ **Do I need Claude Code?**
238
+ No. agentforge ships skills for Claude Code, Cursor, and OpenAI Codex CLI — pick one or install all three with `--agent all`. Skills are plain markdown rendered into each agent's expected layout.
239
+
240
+ **I edited a skill, but my AI session doesn't seem to use the new version.**
241
+ Skill files load at AI session startup. After `agentforge sync-skills` (or any edit to a master file), restart your AI CLI session — the in-memory copy in the running session won't pick up file changes.
242
+
243
+ **My `.claude/skills/<id>/` is filling up with `SKILL.md.bak.N` files. Is that normal?**
244
+ Yes — `sync-skills` always backs up the previous version before overwriting. Safe to clean up periodically:
245
+ ```bash
246
+ find .claude/skills .cursor/rules .agents/skills -name "*.bak*" -delete
247
+ ```
248
+
249
+ **Can I use this alongside Nx / Turborepo / Lerna / Bazel?**
250
+ Yes — they're orthogonal. agentforge structures the **dev workflow** (per-feature worktrees, skill triggers, archives). Monorepo build tools structure the **build graph**. Use both.
251
+
252
+ **Why git worktrees instead of just branches?**
253
+ With worktrees, every feature is a separate working directory on disk. You can have one AI session open per feature, working in parallel without `git stash` / branch-switching dances. Each worktree has its own `node_modules` / build output if needed.
254
+
255
+ **Branch name per repo isn't the slug — why?**
256
+ Each repo can follow its own convention (`feature/<TICKET>-<topic>`, `feat/<slug>`, `<user>/<topic>`). `agentforge-feature-start` samples recent branches per repo to detect the pattern, then proposes a branch name that fits — the slug just names the worktree directory.
257
+
258
+ **How do I write my own skill?**
259
+ Run `agentforge add-skill` from inside the workspace — it scaffolds a master file under `agentforge/skills/<id>.md`, optionally seeded from `--from <file>`. Edit the markdown, then `agentforge sync-skills`. Existing skills in `agentforge/skills/` are good references for structure (frontmatter + body + `## Output language` block).
260
+
261
+ **Windows support?**
262
+ agentforge itself is plain Node.js and works on Windows. `git worktree` works on Windows too, but long path edge cases and shell-script-style command examples in some skills may surface issues — file an issue if you hit one.
263
+
264
+ ---
265
+
266
+ ## Contributing
267
+
268
+ Architecture, build flow, and skill-author conventions live in [`CLAUDE.md`](./CLAUDE.md) — read that before opening a PR. Issues and pull requests welcome.
269
+
270
+ ---
271
+
272
+ ## License
273
+
274
+ MIT.
275
+
@@ -0,0 +1,145 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { pickAgents } from "./agent-prompt.js";
4
+ import { readMasterDir } from "./agents/io.js";
5
+ import { AGENTS, getAgent } from "./agents/index.js";
6
+ import { masterDir, readConfig, upsertConfig, } from "./agentforge-config.js";
7
+ import { pickLanguage } from "./lang-prompt.js";
8
+ import { SKILLS } from "./skills-data.js";
9
+ const DIM = "\x1b[2m";
10
+ const GREEN = "\x1b[32m";
11
+ const YELLOW = "\x1b[33m";
12
+ const CYAN = "\x1b[36m";
13
+ const BOLD = "\x1b[1m";
14
+ const RED = "\x1b[31m";
15
+ const RESET = "\x1b[0m";
16
+ export async function runAddAgent(opts) {
17
+ const root = resolve(opts.pathArg ?? process.cwd());
18
+ if (!isAgentforgeWorkspace(root)) {
19
+ process.stderr.write(`\n${YELLOW}⚠${RESET} Not an agentforge workspace yet.\n` +
20
+ ` ${DIM}${root}${RESET}\n\n` +
21
+ ` Run ${CYAN}agentforge init${RESET} here first to set one up.\n\n`);
22
+ process.exit(1);
23
+ }
24
+ // master must exist before we can install adapters from it
25
+ if (!existsSync(masterDir(root))) {
26
+ process.stderr.write(`\n${YELLOW}⚠${RESET} No master skills directory at ${DIM}${masterDir(root)}${RESET}\n\n` +
27
+ ` Run ${CYAN}agentforge init${RESET} here first to set up the workspace.\n\n`);
28
+ process.exit(1);
29
+ }
30
+ const cfg = readConfig(root);
31
+ // If user passed --lang and it conflicts with what's already written into
32
+ // master files, we'd silently produce a mixed-lang workspace (new agent's
33
+ // CLAUDE.md/.cursorrules/AGENTS.md in --lang, but the actual skill files
34
+ // copied from master in the original lang). Refuse and point at the
35
+ // re-init path.
36
+ if (opts.lang && cfg?.lang && opts.lang !== cfg.lang) {
37
+ process.stderr.write(`\n${YELLOW}⚠${RESET} workspace was initialized with ${BOLD}lang=${cfg.lang}${RESET}, ` +
38
+ `you passed ${BOLD}--lang ${opts.lang}${RESET}.\n\n` +
39
+ ` Master skills are already in ${cfg.lang} — mixing langs would produce inconsistent output.\n` +
40
+ ` To switch the whole workspace to ${opts.lang}: ${CYAN}agentforge init ${root} --lang ${opts.lang} --force-skills${RESET}\n` +
41
+ ` To keep ${cfg.lang}: drop the ${CYAN}--lang${RESET} flag.\n\n`);
42
+ process.exit(1);
43
+ }
44
+ const installed = new Set(cfg?.agents ?? Array.from(detectInstalledAgentsFallback(root)));
45
+ const lang = await resolveLanguage(opts, cfg?.lang);
46
+ const agentsToAdd = await resolveAgentList(opts, installed, lang, root);
47
+ if (agentsToAdd.length === 0) {
48
+ console.log(`${YELLOW}no agents selected — nothing to do.${RESET}`);
49
+ return;
50
+ }
51
+ const { skills: masterSkills, skipped } = readMasterDir(masterDir(root));
52
+ if (skipped.length > 0) {
53
+ for (const sk of skipped) {
54
+ console.log(` ${DIM}skipped master file ${sk.file} — ${sk.reason}${RESET}`);
55
+ }
56
+ }
57
+ console.log("");
58
+ console.log(`${BOLD}${GREEN}+${RESET} adding agents to ${CYAN}${root}${RESET}`);
59
+ console.log("");
60
+ for (const id of agentsToAdd) {
61
+ const adapter = getAgent(id);
62
+ console.log(`${BOLD}${CYAN}▸ ${adapter.label}${RESET}`);
63
+ adapter.install({
64
+ root,
65
+ masterSkills,
66
+ skillCatalog: SKILLS.slice(),
67
+ lang,
68
+ forceSkills: opts.forceSkills,
69
+ forceClaude: opts.forceClaude,
70
+ });
71
+ console.log("");
72
+ }
73
+ // update config.agents (union)
74
+ upsertConfig(root, { agents: [...installed, ...agentsToAdd] });
75
+ printNextSteps(root, agentsToAdd);
76
+ }
77
+ async function resolveLanguage(opts, configLang) {
78
+ if (opts.lang)
79
+ return opts.lang;
80
+ if (configLang)
81
+ return configLang; // honor what's already in config
82
+ if (opts.yes)
83
+ return "en";
84
+ return pickLanguage();
85
+ }
86
+ async function resolveAgentList(opts, installed, lang, root) {
87
+ if (opts.agents && opts.agents.length > 0) {
88
+ const dup = opts.agents.filter((id) => installed.has(id));
89
+ if (dup.length > 0) {
90
+ console.log(`${DIM} note: already installed → skipping: ${dup.join(", ")}${RESET}`);
91
+ }
92
+ return opts.agents.filter((id) => !installed.has(id));
93
+ }
94
+ if (opts.yes) {
95
+ const remaining = AGENTS.map((a) => a.id).filter((id) => !installed.has(id));
96
+ if (remaining.length === 0) {
97
+ console.log(`${YELLOW}all known agents are already installed in ${root}.${RESET}`);
98
+ }
99
+ return remaining;
100
+ }
101
+ if (installed.size === AGENTS.length) {
102
+ console.log(`${YELLOW}all known agents are already installed in ${root}.${RESET}`);
103
+ return [];
104
+ }
105
+ return pickAgents(lang, {
106
+ disabled: installed,
107
+ headerLabel: "Agents to add",
108
+ });
109
+ }
110
+ /** fallback when config.json doesn't exist yet — same logic as before */
111
+ function detectInstalledAgentsFallback(root) {
112
+ const installed = new Set();
113
+ if (existsSync(join(root, ".claude/skills")) ||
114
+ existsSync(join(root, "CLAUDE.md")))
115
+ installed.add("claude");
116
+ if (existsSync(join(root, ".cursor/rules")) ||
117
+ existsSync(join(root, ".cursorrules")))
118
+ installed.add("cursor");
119
+ if (existsSync(join(root, ".agents/skills")) ||
120
+ existsSync(join(root, "AGENTS.md")))
121
+ installed.add("codex");
122
+ return installed;
123
+ }
124
+ function isAgentforgeWorkspace(root) {
125
+ const markers = [
126
+ "agentforge",
127
+ "repos",
128
+ "anvil",
129
+ "artifacts",
130
+ ".claude",
131
+ ".cursor",
132
+ ".agents",
133
+ "CLAUDE.md",
134
+ "AGENTS.md",
135
+ ".cursorrules",
136
+ ];
137
+ return markers.some((m) => existsSync(join(root, m)));
138
+ }
139
+ function printNextSteps(root, agentIds) {
140
+ const labels = agentIds
141
+ .map((id) => AGENTS.find((a) => a.id === id)?.label ?? id)
142
+ .join(", ");
143
+ console.log(`${BOLD}${GREEN}✓${RESET} added: ${labels} ${DIM}(in ${root})${RESET}`);
144
+ console.log("");
145
+ }
@@ -0,0 +1,185 @@
1
+ import { spawn } from "node:child_process";
2
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync, } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import * as readline from "node:readline/promises";
5
+ import { splitFrontmatter } from "./agents/io.js";
6
+ import { confirm } from "./confirm.js";
7
+ import { masterDir, requireWorkspace } from "./agentforge-config.js";
8
+ import { LANG_INSTRUCTIONS } from "./skills-data.js";
9
+ import { runSyncSkills } from "./sync-skills.js";
10
+ const DIM = "\x1b[2m";
11
+ const GREEN = "\x1b[32m";
12
+ const YELLOW = "\x1b[33m";
13
+ const CYAN = "\x1b[36m";
14
+ const BOLD = "\x1b[1m";
15
+ const RED = "\x1b[31m";
16
+ const RESET = "\x1b[0m";
17
+ export async function runAddSkill(opts) {
18
+ const root = resolve(opts.pathArg ?? process.cwd());
19
+ const cfg = requireWorkspace(root);
20
+ if (opts.fromFile) {
21
+ await addFromFile(root, opts.fromFile);
22
+ }
23
+ else {
24
+ await addInteractive(root, cfg.lang, opts.noEdit, opts.yes);
25
+ }
26
+ if (!opts.noEdit) {
27
+ console.log("");
28
+ console.log(`${BOLD}${CYAN}↻${RESET} propagating to agents...`);
29
+ // adding a skill changes root-level skill indexes (e.g. AGENTS.md Skills
30
+ // section), so we ask for a root-guide refresh as well.
31
+ await runSyncSkills({
32
+ pathArg: root,
33
+ forceSkills: false,
34
+ forceClaude: true,
35
+ });
36
+ }
37
+ else {
38
+ console.log("");
39
+ console.log(`${DIM}master file ready. Edit it, then run \`agentforge sync-skills\` to propagate.${RESET}`);
40
+ }
41
+ }
42
+ async function addFromFile(root, fromFile) {
43
+ const src = resolve(fromFile);
44
+ if (!existsSync(src)) {
45
+ process.stderr.write(`\n${RED}✗${RESET} Source file not found.\n ${DIM}${src}${RESET}\n\n`);
46
+ process.exit(1);
47
+ }
48
+ if (!statSync(src).isFile()) {
49
+ process.stderr.write(`\n${RED}✗${RESET} ${DIM}${src}${RESET} is not a regular file.\n ${DIM}--from expects a .md file path${RESET}\n\n`);
50
+ process.exit(1);
51
+ }
52
+ const content = readFileSync(src, "utf8");
53
+ const { frontmatter } = splitFrontmatter(content);
54
+ const name = frontmatter["name"];
55
+ if (!name || !frontmatter["description"]) {
56
+ process.stderr.write(`\n${RED}✗${RESET} Missing required frontmatter in ${DIM}${src}${RESET}\n\n` +
57
+ ` The file needs a frontmatter block at the top:\n` +
58
+ ` ${DIM}---${RESET}\n` +
59
+ ` ${CYAN}name:${RESET} my-skill\n` +
60
+ ` ${CYAN}description:${RESET} One-line summary of what this skill does.\n` +
61
+ ` ${DIM}---${RESET}\n\n`);
62
+ process.exit(1);
63
+ }
64
+ if (!isValidSkillName(name)) {
65
+ process.stderr.write(`\n${RED}✗${RESET} Invalid skill name: "${name}"\n\n` +
66
+ ` Use kebab-case: letters, digits, hyphens. Must start with a letter.\n` +
67
+ ` ${DIM}Example: my-debug-helper${RESET}\n\n`);
68
+ process.exit(1);
69
+ }
70
+ const dst = join(masterDir(root), `${name}.md`);
71
+ if (existsSync(dst)) {
72
+ process.stderr.write(`\n${RED}✗${RESET} Skill "${name}" already exists.\n ${DIM}${dst}${RESET}\n\n` +
73
+ ` Run ${CYAN}agentforge remove-skill ${name}${RESET} to remove it, or choose a different name.\n\n`);
74
+ process.exit(1);
75
+ }
76
+ mkdirSync(masterDir(root), { recursive: true });
77
+ copyFileSync(src, dst);
78
+ console.log(`${GREEN}+${RESET} added master skill: ${name} ${DIM}(from ${src})${RESET}`);
79
+ }
80
+ async function addInteractive(root, lang, noEdit, yes) {
81
+ const rl = readline.createInterface({
82
+ input: process.stdin,
83
+ output: process.stdout,
84
+ });
85
+ let name = "";
86
+ while (true) {
87
+ name = (await rl.question(`${CYAN}?${RESET} ${BOLD}Skill name${RESET} ${DIM}(kebab-case, e.g. my-debug-helper)${RESET} `)).trim();
88
+ if (!isValidSkillName(name)) {
89
+ console.log(` ${RED}invalid name.${RESET} ${DIM}letters/digits/hyphens, must start with a letter.${RESET}`);
90
+ continue;
91
+ }
92
+ if (existsSync(join(masterDir(root), `${name}.md`))) {
93
+ console.log(` ${RED}already exists:${RESET} ${name}`);
94
+ continue;
95
+ }
96
+ break;
97
+ }
98
+ const description = (await rl.question(`${CYAN}?${RESET} ${BOLD}Description${RESET} ${DIM}(one line — what this skill does)${RESET}\n${DIM}›${RESET} `)).trim();
99
+ if (description === "") {
100
+ rl.close();
101
+ process.stderr.write(`\n${RED}✗${RESET} Description is required — it's how agents decide when to use this skill.\n\n`);
102
+ process.exit(1);
103
+ }
104
+ rl.close();
105
+ // build placeholder body
106
+ const body = buildPlaceholderBody(name, description, lang);
107
+ const dst = join(masterDir(root), `${name}.md`);
108
+ mkdirSync(dirname(dst), { recursive: true });
109
+ writeFileSync(dst, body);
110
+ console.log(`${GREEN}+${RESET} wrote master file: agentforge/skills/${name}.md`);
111
+ if (noEdit)
112
+ return;
113
+ const openEditor = yes || (await confirm("Open this skill in $EDITOR now?", true));
114
+ if (!openEditor)
115
+ return;
116
+ await openInEditor(dst);
117
+ }
118
+ function isValidSkillName(name) {
119
+ return /^[a-z][a-z0-9-]*$/.test(name);
120
+ }
121
+ function buildPlaceholderBody(name, description, lang) {
122
+ return `---
123
+ name: ${name}
124
+ description: ${description}
125
+ ---
126
+
127
+ # ${name}
128
+
129
+ <!-- Write your skill instructions here. -->
130
+ <!-- When the user's request matches the description above, this content -->
131
+ <!-- becomes the agent's playbook for the response. -->
132
+
133
+ ## When to apply
134
+
135
+ <!-- describe trigger conditions -->
136
+
137
+ ## How to do it
138
+
139
+ <!-- step-by-step procedure -->
140
+
141
+ ## Rules
142
+
143
+ <!-- constraints, safety, do/don't -->
144
+
145
+ ## Output language
146
+
147
+ ${LANG_INSTRUCTIONS[lang]}
148
+ `;
149
+ }
150
+ async function openInEditor(path) {
151
+ const editor = process.env.VISUAL ||
152
+ process.env.EDITOR ||
153
+ (await firstAvailable(["nano", "vim", "vi"]));
154
+ if (!editor) {
155
+ console.log(`${YELLOW}no $VISUAL/$EDITOR set and no fallback editor (nano/vim/vi) found.${RESET}`);
156
+ console.log(` ${DIM}Edit the file manually: ${path}${RESET}`);
157
+ return;
158
+ }
159
+ // split editor string in case it has args (e.g. "code -w")
160
+ const parts = editor.split(/\s+/).filter(Boolean);
161
+ const cmd = parts[0];
162
+ const args = [...parts.slice(1), path];
163
+ await new Promise((resolveProm, rejectProm) => {
164
+ const child = spawn(cmd, args, { stdio: "inherit" });
165
+ child.on("error", rejectProm);
166
+ child.on("exit", (code) => {
167
+ if (code === 0 || code === null)
168
+ resolveProm();
169
+ else
170
+ rejectProm(new Error(`editor exited with code ${code}`));
171
+ });
172
+ });
173
+ }
174
+ async function firstAvailable(cmds) {
175
+ for (const c of cmds) {
176
+ const ok = await new Promise((res) => {
177
+ const child = spawn("which", [c], { stdio: "ignore" });
178
+ child.on("exit", (code) => res(code === 0));
179
+ child.on("error", () => res(false));
180
+ });
181
+ if (ok)
182
+ return c;
183
+ }
184
+ return null;
185
+ }