archgen-skill 0.0.4 → 0.0.6

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 (37) hide show
  1. package/README.md +39 -45
  2. package/bin/archgen.mjs +30 -6
  3. package/lib/block.js +55 -9
  4. package/lib/bootstrap.js +103 -0
  5. package/lib/doctor.js +67 -5
  6. package/lib/init.js +43 -4
  7. package/lib/install.js +40 -4
  8. package/lib/mcp-server.js +159 -0
  9. package/lib/migrations/001-stamp-provenance.mjs +3 -3
  10. package/lib/spawn-npm.js +54 -0
  11. package/lib/store.js +174 -9
  12. package/lib/uninstall-project.js +16 -3
  13. package/lib/version-stamp.js +3 -1
  14. package/lib/version.js +11 -3
  15. package/package.json +5 -4
  16. package/vendor/skills/archgen/SKILL.md +370 -315
  17. package/vendor/skills/archgen/archgen.config.json +1 -1
  18. package/vendor/skills/archgen/references/code-standards.md +2 -2
  19. package/vendor/skills/archgen/assets/mcp-template.json +0 -42
  20. package/vendor/skills/archgen/assets/skill-registry.json +0 -24
  21. package/vendor/skills/archgen/references/artifact-templates.md +0 -349
  22. package/vendor/skills/archgen/references/interview.md +0 -230
  23. package/vendor/skills/archgen/references/mcp-and-skills.md +0 -117
  24. package/vendor/skills/archgen/references/orchestration.md +0 -169
  25. package/vendor/skills/archgen/references/platforms.md +0 -83
  26. package/vendor/skills/archgen/scripts/doc-index.mjs +0 -457
  27. package/vendor/skills/archgen/scripts/impact.mjs +0 -77
  28. package/vendor/skills/archgen/scripts/lib/graph.mjs +0 -207
  29. package/vendor/skills/archgen/scripts/lib/plan-coverage.mjs +0 -32
  30. package/vendor/skills/archgen/scripts/lib/scope.mjs +0 -65
  31. package/vendor/skills/archgen/scripts/lib/yaml.mjs +0 -492
  32. package/vendor/skills/archgen/scripts/next-tasks.mjs +0 -55
  33. package/vendor/skills/archgen/scripts/plan-graph.mjs +0 -329
  34. package/vendor/skills/archgen/scripts/set-status.mjs +0 -60
  35. package/vendor/skills/archgen/scripts/update-agents.mjs +0 -204
  36. package/vendor/skills/archgen/scripts/validate.mjs +0 -87
  37. package/vendor/skills/archgen/scripts/verify-plan.mjs +0 -70
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  [![CI](https://github.com/akashmaurya99/archgen/actions/workflows/ci.yml/badge.svg)](https://github.com/akashmaurya99/archgen/actions/workflows/ci.yml)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/akashmaurya99/archgen/blob/main/LICENSE)
9
- [![Node](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org)
9
+ [![Node](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](https://nodejs.org)
10
10
 
11
11
  Describe what you want to build. archgen interviews you (or surveys your existing
12
12
  codebase), produces an architecture contract plus a dependency-ordered task graph,
@@ -32,15 +32,15 @@ What `init` writes (nothing is ever duplicated):
32
32
 
33
33
  | Path | What |
34
34
  | --- | --- |
35
- | `.agents/skills/archgen/` | **The only real copy** — the agentskills.io standard location read natively by OpenCode, Cursor, Codex, Gemini CLI, Copilot and Antigravity |
36
- | `.claude/skills/archgen → ../../.agents/skills/archgen` | Relative symlink for Claude Code (the one harness needing it). Skipped gracefully where symlinks need privileges — Claude still works via the bridge below |
37
- | `AGENTS.md` | Managed hub block: skill pointer + quick triggers + a features registry that stays current as you add features |
38
- | `CLAUDE.md` | One-line bridge: `@AGENTS.md` (existing files keep all their content; if it already imports AGENTS.md we write nothing) |
35
+ | `.agents/skills/archgen/` | **The only real copy** — the agentskills.io standard location, read natively by OpenCode, Cursor, Codex, Gemini CLI, Copilot, and Antigravity |
36
+ | `.claude/skills/archgen → ../../.agents/skills/archgen` | Relative symlink for Claude Code (the one harness that needs it); skipped gracefully where symlinks require privileges |
37
+ | `AGENTS.md` | Managed hub block: skill pointer, quick triggers, and a features registry that stays current as you add features |
38
+ | `CLAUDE.md` | One-line bridge: `@AGENTS.md` (existing content is preserved; nothing is written if it already imports AGENTS.md) |
39
39
 
40
- Existing projects with user content are safe: managed blocks upsert between
41
- markers, modified skill copies are backed up to `.archgen/.backup/<timestamp>/`
42
- before refresh, and legacy dual-copy layouts are migrated to symlinks
43
- automatically. Verify anytime with `archgen-skill doctor`.
40
+ Existing projects are safe: managed blocks upsert between markers, modified skill
41
+ copies are backed up to `.archgen/.backup/<timestamp>/` before refresh, and legacy
42
+ dual-copy layouts are migrated to symlinks automatically. Verify anytime with
43
+ `archgen-skill doctor`.
44
44
 
45
45
  **Or install globally** into every detected harness:
46
46
 
@@ -49,12 +49,18 @@ npx archgen-skill install # symlinks; --copy for real copies
49
49
  ```
50
50
 
51
51
  Prefer shell? Clone the repo and run `./install.sh` (same behavior, plus
52
- `--init`, `--project`, `--uninstall`). Requires Node.js ≥ 18.
53
- The skill itself has zero npm dependencies.
52
+ `--init`, `--project`, `--uninstall`). Requires Node.js ≥ 20 — the official MCP
53
+ SDK v2 floor.
54
+
55
+ **What `init` actually does:** installs the skill (guidance only — no code),
56
+ wires the `archgen-mcp` MCP server into every detected harness, and ensures
57
+ CodeGraph (its CLI, its MCP entry, and the project index). The CLI itself is
58
+ zero-dependency; the MCP server it installs depends on the official MCP SDK.
54
59
 
55
60
  ## Quick start
56
61
 
57
- After `npx archgen-skill init`, open your project in any supported agent and talk naturally:
62
+ After `npx archgen-skill init`, open your project in any supported agent and talk
63
+ naturally:
58
64
 
59
65
  | You say | What runs |
60
66
  | --- | --- |
@@ -64,39 +70,33 @@ After `npx archgen-skill init`, open your project in any supported agent and tal
64
70
  | *"roll back the auth changes"* | Impact analysis → reverse-order revert plan → approval |
65
71
 
66
72
  Everything archgen produces lives in one reviewable `.archgen/<slug>/` folder —
67
- architecture contract, docs, ADRs, plans, and a `tasks.yaml` task graph — versioned by git.
73
+ architecture contract, docs, ADRs, plans, and a `tasks.yaml` task graph — versioned
74
+ by git.
68
75
 
69
76
  ## Commands
70
77
 
71
78
  | Command | What it does |
72
79
  | --- | --- |
73
- | `npx archgen-skill init [dir]` | Install the single canonical skill store at `.agents/skills/archgen`, add the Claude symlink adapter, write the AGENTS.md hub + CLAUDE.md `@AGENTS.md` bridge |
80
+ | `npx archgen-skill init [dir]` | Prepare a project: canonical skill store at `.agents/skills/archgen`, Claude symlink adapter, AGENTS.md hub + CLAUDE.md bridge |
74
81
  | `npx archgen-skill install [--copy]` | Install into global harness skill dirs (symlinks by default, real copies with `--copy`) |
75
82
  | `npx archgen-skill uninstall` | Remove globally-installed copies (manifest-recorded, safe removal) |
76
- | `npx archgen-skill uninstall --project [dir]` | Remove project install: strips managed blocks, our symlink and the store **only if unmodified** — feature folders in `.archgen/` and all user content are preserved |
77
- | `npx archgen-skill doctor [dir] [--check]` | Verify + auto-repair an installation: store integrity, version stamp, Claude link, blocks present exactly once, manifest resolution (`--check` reports without fixing) |
78
- | `npx archgen-skill --help` | Help |
79
-
80
- ### Version stamp (`.archgen-version`)
81
-
82
- Every skill tree the CLI writes carries a one-line `MAJOR.MINOR.PATCH\n` stamp
83
- at its root — the canonical store from `init`, each `install --copy` tree, and
84
- the real directory behind global symlink installs. `update` refreshes it via
85
- its init/doctor pass. `doctor` lists every detected install with its stamped
86
- version (`missing` / `unknown (pre-0.1 stamp)` when absent or unparseable).
87
- The VS Code extension reads the same file to warn about outdated skills;
88
- absent or corrupt means "unknown (legacy)", never an error.
83
+ | `npx archgen-skill uninstall --project [dir]` | Remove a project install: strips managed blocks, symlink, and store **only if unmodified** — `.archgen/` feature folders and all user content are preserved |
84
+ | `npx archgen-skill doctor [dir] [--check]` | Verify and auto-repair an installation: store integrity, version stamp, Claude link, managed blocks, manifest (`--check` reports without fixing) |
85
+ | `npx archgen-skill update [dir]` | Check npm for a newer `archgen-skill`, upgrade the global install if outdated, then re-init the project and run doctor against the new version |
86
+ | `npx archgen-skill restore` | List backup snapshots; `--snapshot <ts>` restores one (current state is backed up first) |
87
+ | `npx archgen-skill migrate [dir] [--apply]` | Evolve generated-artifact formats in place (`--check` by default; every touched file is backed up) |
88
+ | `npx archgen-skill --version` / `--help` | Print the CLI version / show help |
89
89
 
90
90
  ## Why archgen
91
91
 
92
92
  AI coding agents fail at scale for predictable reasons: plans with hidden cycles,
93
93
  workers editing the same files, tasks without objective acceptance criteria, and no
94
94
  human checkpoint before execution. archgen makes each of these a *structural*
95
- guarantee rather than a hope:
95
+ guarantee:
96
96
 
97
97
  | Guarantee | Mechanism |
98
98
  | --- | --- |
99
- | Plans are valid before work starts | Verifier gate — catches cycles, dangling refs, ownership overlaps, plan↔task coverage gaps |
99
+ | Plans are valid before work starts | Verifier gate — cycles, dangling refs, ownership overlaps, plan↔task coverage |
100
100
  | You approve before code is written | Explicit user gate after verifier approval |
101
101
  | Parallel workers never collide | Disjoint `file_ownership` globs enforced per wave |
102
102
  | Task order is always correct | Topological waves from `depends_on`; chains stay sequential |
@@ -124,23 +124,18 @@ guarantee rather than a hope:
124
124
 
125
125
  ## Supported harnesses
126
126
 
127
- | Harness | Global install | Project-local (`init`) |
128
- | --- | --- | --- |
129
- | Claude Code | ✅ | ✅ |
130
- | OpenCode | ✅ | ✅ |
131
- | Cursor | ✅ | ✅ |
132
- | Codex | ✅ | ✅ |
133
- | Gemini CLI | ✅ | ✅ |
134
- | Antigravity | ✅ | ✅ |
135
- | Any agentskills.io harness | ✅ | ✅ |
127
+ Claude Code, OpenCode, Cursor, Codex, Gemini CLI, Antigravity — and any
128
+ agentskills.io-compatible harness. Both global install and project-local `init`
129
+ are supported for all of them.
136
130
 
137
131
  ## VS Code extension (optional)
138
132
 
139
- A read-only visual layer that renders `.archgen/` as a live task DAG (running tasks
140
- pulse, edges animate), your code-dependency graph, and rendered docs — with a ▶
141
- button that launches your agent on any task. Install the `.vsix` from
142
- [GitHub Releases](https://github.com/akashmaurya99/archgen/releases). It is strictly
143
- a viewer: uninstalling it loses nothing.
133
+ A visual layer that renders `.archgen/` as a live task DAG, your
134
+ code-dependency graph, and rendered docs — with a ▶ button that hands any task to
135
+ your agent. Install it from the VS Code Marketplace or from a `.vsix` on
136
+ [GitHub Releases](https://github.com/akashmaurya99/archgen/releases). It never
137
+ edits your plan, your tasks, or your code — only its own runtime state under
138
+ `.archgen/.runtime/`, so uninstalling it loses nothing.
144
139
 
145
140
  ## Documentation
146
141
 
@@ -149,7 +144,6 @@ a viewer: uninstalling it loses nothing.
149
144
  | [Architecture walkthrough](https://github.com/akashmaurya99/archgen/blob/main/docs/architecture.md) | Dual-mode design, gates, wave mechanics, platform detection |
150
145
  | [The skill contract](https://github.com/akashmaurya99/archgen/blob/main/skill/SKILL.md) | The full skill specification agents execute |
151
146
  | [Changelog](https://github.com/akashmaurya99/archgen/blob/main/CHANGELOG.md) | Release history |
152
- | [Contributing](https://github.com/akashmaurya99/archgen/blob/main/CONTRIBUTING.md) | Development setup and PR guide |
153
147
 
154
148
  ## Contributing
155
149
 
@@ -157,7 +151,7 @@ See [CONTRIBUTING.md](https://github.com/akashmaurya99/archgen/blob/main/CONTRIB
157
151
  Skill suite and CLI tests run with zero dependencies:
158
152
 
159
153
  ```sh
160
- node --test skill/scripts/test/*.test.mjs # skill suite
154
+ node --test packages/mcp/test/**/*.test.mjs # MCP server suite
161
155
  cd packages/cli && npm test # CLI suite
162
156
  ```
163
157
 
package/bin/archgen.mjs CHANGED
@@ -22,6 +22,7 @@ import { uninstallProject } from '../lib/uninstall-project.js';
22
22
  import { restoreMain } from '../lib/restore.js';
23
23
  import { migrateMain } from '../lib/migrate.js';
24
24
  import { compareSemver, fetchLatestVersion } from '../lib/version.js';
25
+ import { npmInvocation, whichOnPath } from '../lib/spawn-npm.js';
25
26
  import { spawnSync } from 'node:child_process';
26
27
  import { readFileSync } from 'node:fs';
27
28
 
@@ -46,6 +47,8 @@ Usage:
46
47
  and generate AGENTS.md / CLAUDE.md pointer blocks.
47
48
  [--force] proceed automatically; divergent copies are
48
49
  backed up to .archgen/.backup/ (never destroyed)
50
+ [--no-codegraph] skip the CodeGraph bootstrap
51
+ [--no-mcp] skip wiring the archgen MCP server
49
52
  npx archgen-skill doctor [dir] Health-check a project install: store integrity,
50
53
  version stamp, claude link, managed blocks,
51
54
  manifest. Repairs safe issues in place.
@@ -53,6 +56,8 @@ Usage:
53
56
  npx archgen-skill install Install into global harness skill dirs
54
57
  [--copy] real copies instead of symlinks
55
58
  [--uninstall] remove manifest-recorded entries
59
+ [--no-codegraph] skip the CodeGraph bootstrap
60
+ [--no-mcp] skip wiring the archgen MCP server
56
61
  npx archgen-skill uninstall Remove globally-installed copies (manifest-based)
57
62
  --project [dir] remove archgen from a project: claude link,
58
63
  managed blocks, canonical store (kept when
@@ -92,7 +97,9 @@ try {
92
97
  switch (cmd) {
93
98
  case 'init': {
94
99
  flag('--force');
95
- const r = initProject(positionalDir(), PACKAGE_ROOT);
100
+ const noCodegraph = flag('--no-codegraph');
101
+ const noMcp = flag('--no-mcp');
102
+ const r = initProject(positionalDir(), PACKAGE_ROOT, { codegraph: !noCodegraph, mcp: !noMcp });
96
103
  console.log('archgen: project ready (single-store layout)');
97
104
  console.log(' + store ' + r.storePath);
98
105
  if (r.claudeLink === 'created') console.log(' + link .claude/skills/archgen -> ../../.agents/skills/archgen');
@@ -120,8 +127,10 @@ try {
120
127
  }
121
128
  case 'install': {
122
129
  const copy = flag('--copy');
130
+ const noCodegraph = flag('--no-codegraph');
131
+ const noMcp = flag('--no-mcp');
123
132
  if (flag('--uninstall')) { uninstall(); break; }
124
- const r = installGlobal({ copy });
133
+ const r = installGlobal({ copy, codegraph: !noCodegraph, mcp: !noMcp });
125
134
  console.log('archgen: global harness install (mode: ' + (copy ? 'copy' : 'symlink') + ')');
126
135
  for (const [status, mode, path] of r.rows) console.log(` ${status.padEnd(7)} ${mode.padEnd(5)} ${path}`);
127
136
  console.log('\nPer-project setup (recommended): npx archgen-skill init');
@@ -137,6 +146,7 @@ try {
137
146
  if (r.storeRemoved) console.log(' - .agents/skills/archgen (unmodified store)');
138
147
  if (r.storeKept) console.log(' = .agents/skills/archgen KEPT (customized)');
139
148
  if (r.manifestRemoved) console.log(' - .archgen/.install-manifest.json');
149
+ for (const m of r.mcpRemoved ?? []) console.log(` - ${m} (archgen MCP entry)`);
140
150
  for (const w of r.warnings) console.log(' note: ' + w);
141
151
  console.log(' .archgen feature folders and backups are preserved.');
142
152
  } else {
@@ -177,23 +187,37 @@ try {
177
187
  runInitAndDoctor(dir);
178
188
  break;
179
189
  }
190
+ if (!/^\d+\.\d+\.\d+[-+]?[0-9A-Za-z.-]*$/.test(latest)) {
191
+ // Registry returned something that is not a semver tag — never
192
+ // interpolate it into a command line.
193
+ console.error(' ! registry returned a non-semver version string; refusing: ' + JSON.stringify(latest));
194
+ process.exitCode = 1;
195
+ break;
196
+ }
180
197
  console.log(' ↑ newer version available: ' + latest);
181
198
  console.log(' upgrading global install: npm install -g archgen-skill@' + latest);
182
- const up = spawnSync('npm', ['install', '-g', 'archgen-skill@' + latest], { stdio: 'inherit' });
199
+ const inv = npmInvocation(['install', '-g', 'archgen-skill@' + latest]);
200
+ const up = spawnSync(inv.command, inv.args, { stdio: 'inherit', shell: inv.shell });
183
201
  if (up.status !== 0) {
184
202
  console.error(' ! global upgrade failed — run it manually, then re-run: archgen-skill update');
185
203
  process.exitCode = 1;
186
204
  break;
187
205
  }
188
206
  // The running process is the OLD code; re-exec the freshly installed binary.
189
- const bin = spawnSync('command', ['-v', 'archgen-skill'], { encoding: 'utf8', shell: true });
190
- const freshBin = (bin.stdout || '').trim();
207
+ const freshBin = whichOnPath('archgen-skill');
191
208
  if (!freshBin) {
192
209
  console.log('\nUpgraded to v' + latest + '. Now finish per project with:\n npx archgen-skill@latest init');
193
210
  break;
194
211
  }
195
212
  console.log('\nre-running init + doctor with the new binary (' + latest + ')…');
196
- spawnSync(freshBin, ['init', dir], { stdio: 'inherit' });
213
+ const upInit = spawnSync(freshBin, ['init', dir], { stdio: 'inherit' });
214
+ if (upInit.status !== 0) {
215
+ // An aborted init (e.g. symlink refusal) must surface as a failure —
216
+ // running doctor over a half-finished install would mask it.
217
+ console.error(' ! init failed during update - doctor skipped (see message above)');
218
+ process.exitCode = upInit.status ?? 1;
219
+ break;
220
+ }
197
221
  spawnSync(freshBin, ['doctor', dir], { stdio: 'inherit' });
198
222
  break;
199
223
  }
package/lib/block.js CHANGED
@@ -18,6 +18,7 @@
18
18
 
19
19
  import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
20
20
  import { loadConfig } from './config.js';
21
+ import { assertNotSymlink } from './store.js';
21
22
 
22
23
  // Markers + block-format version come from the single source of truth.
23
24
  // Module-level load is safe: config resolution is module-relative and
@@ -97,7 +98,7 @@ export function renderBlock(skillRelPath = '.agents/skills/archgen', eol = '\n')
97
98
  '',
98
99
  'Rules of the road:',
99
100
  '- Generated artifacts live ONLY under `.archgen/<slug>/`',
100
- '- Never hand-edit task statuses - use `scripts/set-status.mjs` (comment-safe)',
101
+ '- Never hand-edit task statuses - call the `archgen_tasks` MCP tool (comment-safe)',
101
102
  '- Two gates before any execution: verifier approval, then human approval',
102
103
  '',
103
104
  '## Features registry',
@@ -127,8 +128,31 @@ export function renderClaudeBridgeText() {
127
128
  return [START, provenanceLine(), '@AGENTS.md', END].join('\n');
128
129
  }
129
130
 
131
+ function countOccurrences(haystack, needle) {
132
+ let n = 0;
133
+ for (let i = haystack.indexOf(needle); i !== -1; i = haystack.indexOf(needle, i + needle.length)) n++;
134
+ return n;
135
+ }
136
+
137
+ function stripCompleteBlocks(text) {
138
+ let out = text;
139
+ for (;;) {
140
+ const s = out.indexOf(START);
141
+ if (s === -1) break;
142
+ const e = out.indexOf(END, s + START.length);
143
+ if (e === -1) break;
144
+ out = out.slice(0, s) + out.slice(e + END.length);
145
+ }
146
+ return out;
147
+ }
148
+
130
149
  /**
131
150
  * Insert or replace the managed block inside existing file content.
151
+ * Corrupted input with DUPLICATED markers (more than one START or END, e.g. a
152
+ * twice-appended block) is normalized: the first complete block is replaced
153
+ * in place, every later complete block is removed, and anything outside the
154
+ * markers survives — the result carries exactly one block. Orphan markers
155
+ * left over after normalization still throw, as does a single orphan marker.
132
156
  * @param {string} existing file contents ('' when creating new)
133
157
  * @param {string} block rendered block
134
158
  * @param {string} eol line ending used when appending to empty/missing files
@@ -138,6 +162,22 @@ export function upsertBlock(existing, block, eol = '\n') {
138
162
  // caller rendered — this is what upgrades legacy unversioned blocks in place.
139
163
  const fresh = injectProvenance(block);
140
164
  const trimmed = existing ?? '';
165
+ const starts = countOccurrences(trimmed, START);
166
+ const ends = countOccurrences(trimmed, END);
167
+
168
+ if (starts > 1 || ends > 1) {
169
+ const s = trimmed.indexOf(START);
170
+ const e = s === -1 ? -1 : trimmed.indexOf(END, s + START.length);
171
+ if (s === -1 || e === -1) {
172
+ throw new Error('found only one archgen marker - fix or remove it manually');
173
+ }
174
+ const tail = stripCompleteBlocks(trimmed.slice(e + END.length));
175
+ if (tail.includes(START) || tail.includes(END)) {
176
+ throw new Error('found only one archgen marker - fix or remove it manually');
177
+ }
178
+ return trimmed.slice(0, s) + fresh + tail;
179
+ }
180
+
141
181
  const startIdx = trimmed.indexOf(START);
142
182
  const endIdx = trimmed.indexOf(END);
143
183
  if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
@@ -152,18 +192,16 @@ export function upsertBlock(existing, block, eol = '\n') {
152
192
  }
153
193
 
154
194
  /**
155
- * Strip the managed block from file contents.
195
+ * Strip EVERY managed block from file contents (duplicated markers normalize
196
+ * to zero blocks, matching upsertBlock's one-block guarantee).
156
197
  * @returns {{content: string, hadBlock: boolean}}
157
198
  */
158
199
  export function stripBlock(existing) {
159
200
  const trimmed = existing ?? '';
160
- const startIdx = trimmed.indexOf(START);
161
- const endIdx = trimmed.indexOf(END);
162
- if (startIdx === -1 || endIdx === -1) return { content: trimmed, hadBlock: false };
163
- let out = trimmed.slice(0, startIdx) + trimmed.slice(endIdx + END.length);
201
+ const out = stripCompleteBlocks(trimmed);
202
+ if (out === trimmed) return { content: trimmed, hadBlock: false };
164
203
  // Collapse whitespace left behind; drop file if nothing else remains.
165
- out = out.replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
166
- return { content: out, hadBlock: true };
204
+ return { content: out.replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n'), hadBlock: true };
167
205
  }
168
206
 
169
207
  function splitBom(raw) {
@@ -174,18 +212,24 @@ function splitBom(raw) {
174
212
  /** Write via sibling temp file + rename so readers never observe partial content. */
175
213
  function atomicWrite(absPath, data) {
176
214
  const tmp = absPath + '.tmp-' + process.pid + '-' + Date.now();
177
- writeFileSync(tmp, data);
215
+ // 'wx' (O_CREAT|O_EXCL): if the tmp name already exists — e.g. an attacker
216
+ // planted a symlink there to redirect the write — fail instead of following.
217
+ writeFileSync(tmp, data, { flag: 'wx' });
178
218
  renameSync(tmp, absPath);
179
219
  }
180
220
 
181
221
  /**
182
222
  * BOM/EOL-aware upsert of a managed block directly into a file.
183
223
  * Preserves a leading UTF-8 BOM and the file's CRLF/LF convention.
224
+ * Refuses (throws) when the target is a symlink: managed files must never be
225
+ * written through a link (the final rename replaces the link itself, but the
226
+ * read+upsert would otherwise operate on attacker-chosen content).
184
227
  * @param {string} absPath target file (created when missing)
185
228
  * @param {string[]} blockLines lines between the managed markers
186
229
  * @returns {boolean} whether the file existed before the call
187
230
  */
188
231
  export function upsertManagedFile(absPath, blockLines) {
232
+ assertNotSymlink(absPath);
189
233
  const existed = existsSync(absPath);
190
234
  const raw = existed ? readFileSync(absPath, 'utf8') : '';
191
235
  const { bom, body } = splitBom(raw);
@@ -208,6 +252,7 @@ export function importsAgents(absPath) {
208
252
 
209
253
  /** Strip the managed block from a file on disk, preserving BOM. */
210
254
  export function stripManagedFile(absPath) {
255
+ assertNotSymlink(absPath);
211
256
  let raw = '';
212
257
  try { raw = readFileSync(absPath, 'utf8'); } catch { return { hadBlock: false }; }
213
258
  const { bom, body } = splitBom(raw);
@@ -233,6 +278,7 @@ function renderRow(row) {
233
278
  * @returns {string} the new file content
234
279
  */
235
280
  export function upsertFeaturesRegistry(fileAbsPath, rows = []) {
281
+ assertNotSymlink(fileAbsPath);
236
282
  const raw = readFileSync(fileAbsPath, 'utf8');
237
283
  const table = [
238
284
  '| Feature | Status | Updated |',
@@ -0,0 +1,103 @@
1
+ // bootstrap.js — the install-time server bootstrap for `archgen init` /
2
+ // `archgen install`.
3
+ //
4
+ // The CLI used to shell out to the skill's scripts/codegraph.mjs. That script
5
+ // no longer exists: ALL deterministic capability now lives in the archgen MCP
6
+ // server, so the CLI drives THAT instead. One implementation, three ways in
7
+ // (agent -> MCP stdio, CLI -> `archgen-mcp bootstrap`, extension -> exec).
8
+ //
9
+ // The bootstrap does two things in one pass:
10
+ // 1. wires the archgen MCP entry into every detected harness
11
+ // 2. ensures CodeGraph: its CLI, its MCP entry, and the project index
12
+ //
13
+ // NEVER FATAL: no npm, no network, a read-only config, or a missing MCP
14
+ // server must still leave a working skill install. Failures are REPORTED,
15
+ // never thrown.
16
+ import { bootstrapMcp, resolveMcpBinary } from './mcp-server.js';
17
+
18
+ /**
19
+ * @param {{
20
+ * projectDir: string,
21
+ * location?: 'project'|'global'|'both',
22
+ * log?: (line: string) => void,
23
+ * env?: Record<string, string|undefined>,
24
+ * noCodegraph?: boolean,
25
+ * noMcp?: boolean,
26
+ * }} opts
27
+ * @returns {{ok: boolean, skipped?: string, detail?: string, report?: any}}
28
+ */
29
+ export function bootstrapServers(opts) {
30
+ const {
31
+ projectDir,
32
+ location = 'both',
33
+ log = () => {},
34
+ env = process.env,
35
+ noCodegraph = false,
36
+ noMcp = false,
37
+ } = opts;
38
+
39
+ // ARCHGEN_CODEGRAPH=0 skips the bootstrap (no spawn, no network, no writes)
40
+ // — without it the test suite would wire the developer's real MCP configs.
41
+ if (env.ARCHGEN_CODEGRAPH === '0' || env.ARCHGEN_CODEGRAPH === 'off') {
42
+ return { ok: true, skipped: 'ARCHGEN_CODEGRAPH=0 (bootstrap disabled)' };
43
+ }
44
+ if (noMcp && noCodegraph) {
45
+ return { ok: true, skipped: '--no-mcp and --no-codegraph passed' };
46
+ }
47
+
48
+ const resolved = resolveMcpBinary({ projectRoot: projectDir, env });
49
+ log(` + mcp server: ${resolved.bin ?? 'npx -y archgen-mcp'} (${resolved.source})`);
50
+
51
+ if (noMcp) {
52
+ log(' ! archgen MCP wiring skipped (--no-mcp)');
53
+ }
54
+ if (noCodegraph) {
55
+ log(' ! CodeGraph bootstrap skipped (--no-codegraph)');
56
+ }
57
+
58
+ const r = bootstrapMcp({
59
+ projectDir,
60
+ location,
61
+ approved: true,
62
+ env,
63
+ });
64
+
65
+ if (!r.report) {
66
+ return {
67
+ ok: true, // never fatal — the skill install itself succeeded
68
+ detail: `server bootstrap incomplete: ${r.detail ?? 'unknown error'}`,
69
+ };
70
+ }
71
+
72
+ const report = r.report;
73
+ const mcp = report.mcp ?? {};
74
+ const codegraph = report.codegraph ?? {};
75
+
76
+ for (const w of mcp.wired ?? []) {
77
+ log(` + harness: ${w.harness} → ${w.path} [${w.key}] ${w.status}${w.detail ? ' — ' + w.detail : ''}`);
78
+ }
79
+ if (codegraph.cli) {
80
+ log(
81
+ ` + codegraph ${codegraph.cli.version ?? 'unknown'}${codegraph.cli.action !== 'none' ? ' (' + codegraph.cli.action + ')' : ''}${codegraph.cli.latest ? ' · latest available ' + codegraph.cli.latest : ''}`,
82
+ );
83
+ }
84
+ if (codegraph.telemetry) {
85
+ log(` + telemetry: ${codegraph.telemetry.state ?? 'unknown'}${codegraph.telemetry.action !== 'none' ? ' → ' + codegraph.telemetry.action : ''}`);
86
+ }
87
+ for (const w of codegraph.wiring?.native ?? []) {
88
+ log(` + harness: ${w.targets} (${w.location}) → ${w.status}${w.detail ? ' — ' + w.detail : ''}`);
89
+ }
90
+ if (codegraph.index) {
91
+ log(` + index: ${codegraph.index.action ?? 'none'}${codegraph.index.files != null ? ' (' + codegraph.index.files + ' files)' : ''}`);
92
+ }
93
+ log(' + realtime: the MCP server watches files — the index stays live, no re-indexing needed');
94
+
95
+ if (r.ok === false || codegraph.ok === false) {
96
+ return {
97
+ ok: false,
98
+ report,
99
+ detail: 'one or more server bootstrap steps failed',
100
+ };
101
+ }
102
+ return { ok: true, report };
103
+ }
package/lib/doctor.js CHANGED
@@ -7,7 +7,7 @@
7
7
  // reported and counted in the summary but do NOT fail the run: the process
8
8
  // exits non-zero only when a repair operation itself throws.
9
9
 
10
- import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, readlinkSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs';
10
+ import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, readlinkSync, symlinkSync, unlinkSync } from 'node:fs';
11
11
  import { homedir } from 'node:os';
12
12
  import { dirname, join, resolve } from 'node:path';
13
13
  import { globalTargets } from './install.js';
@@ -22,9 +22,10 @@ import {
22
22
  upsertManagedFile,
23
23
  } from './block.js';
24
24
  import { resolveSkillSource } from './init.js';
25
- import { CLAUDE_LINK_REL, MANIFEST_REL, STORE_REL, VERSION_FILE, cliVersion, hashDir, loadManifest, saveManifest } from './store.js';
25
+ import { CLAUDE_LINK_REL, DEV_ONLY_RELPATHS, MANIFEST_REL, STORE_REL, VERSION_FILE, cliVersion, findObsolete, hashDir, loadManifest, pruneDevOnly, pruneObsolete, saveManifest } from './store.js';
26
26
  import { compareSemver, parseSemver } from './version.js';
27
27
  import { readStamp, writeStamp } from './version-stamp.js';
28
+ import { mcpDoctor } from './mcp-server.js';
28
29
 
29
30
  function lstatSafe(p) {
30
31
  try { return lstatSync(p); } catch { return null; }
@@ -83,7 +84,21 @@ export function doctorProject(projectDir, packageRoot, opts = {}) {
83
84
  if (!existsSync(join(storeAbs, 'SKILL.md'))) {
84
85
  add('FAIL', 'store .agents/skills/archgen missing or incomplete - run `archgen init`');
85
86
  } else {
86
- const missingCore = ['scripts', 'references'].filter((d) => !existsSync(join(storeAbs, d)));
87
+ // MIGRATION: the skill no longer ships code. An install carried over from
88
+ // the pre-MCP layout still has a scripts/ tree — dead weight that would
89
+ // otherwise read as "diverges from packaged skill (customized?)".
90
+ // Pruned BEFORE the divergence check so an upgrade is not misreported.
91
+ const packaged = (() => {
92
+ try { return resolveSkillSource(packageRoot); } catch { return null; }
93
+ })();
94
+ if (packaged) {
95
+ // --check reports WITHOUT mutating; repair mode prunes.
96
+ const obsolete = checkOnly ? findObsolete(storeAbs, packaged) : pruneObsolete(storeAbs, packaged);
97
+ for (const rel of obsolete) {
98
+ add(checkOnly ? 'WOULD-FIX' : 'FIXED', `store carried the obsolete ${rel}/ tree (pre-MCP layout) -> ${checkOnly ? 'would prune' : 'pruned'}`);
99
+ }
100
+ }
101
+ const missingCore = ['references'].filter((d) => !existsSync(join(storeAbs, d)));
87
102
  if (missingCore.length > 0) {
88
103
  // Deferred so the managed-block FAIL rows below stay FIRST: existing
89
104
  // consumers read checks.find(FAIL) expecting the marker verdict.
@@ -98,6 +113,23 @@ export function doctorProject(projectDir, packageRoot, opts = {}) {
98
113
  }
99
114
  }
100
115
 
116
+ // 1b. Prune dev-only artifacts (scripts/test) that older installers shipped
117
+ // into the store. They are repo-only CI tooling and never belong in an
118
+ // installed tree; removing them here cleans existing stores without a
119
+ // full backup+re-copy. hashDir ignores the same subtrees, so this prune
120
+ // neither masks real customization nor reads as divergence.
121
+ if (existsSync(storeAbs)) {
122
+ const devOnlyPresent = DEV_ONLY_RELPATHS.filter((rel) => existsSync(join(storeAbs, ...rel.split('/'))));
123
+ if (devOnlyPresent.length > 0) {
124
+ if (checkOnly) {
125
+ add('WOULD-FIX', `store carries dev-only ${devOnlyPresent.join(', ')} -> pruned`);
126
+ } else {
127
+ attempt(() => pruneDevOnly(storeAbs), 'FIXED',
128
+ `dev-only ${devOnlyPresent.join(', ')} pruned from store`, 'cannot prune dev-only artifacts');
129
+ }
130
+ }
131
+ }
132
+
101
133
  // 2. Version stamp currency.
102
134
  const stampAbs = join(storeAbs, VERSION_FILE);
103
135
  let stamp = null;
@@ -146,8 +178,10 @@ export function doctorProject(projectDir, packageRoot, opts = {}) {
146
178
  if (!existsSync(agentsAbs)) {
147
179
  if (checkOnly) add('WOULD-FIX', 'AGENTS.md missing -> created with managed block');
148
180
  else {
149
- writeFileSync(agentsAbs, renderBlock(STORE_REL) + '\n');
150
- add('FIXED', 'AGENTS.md created with managed block');
181
+ // Through the guarded upsert (not raw writeFileSync): existsSync follows
182
+ // symlinks, so a dangling symlink would otherwise be written through.
183
+ attempt(() => upsertManagedFile(agentsAbs, renderBlock(STORE_REL).split('\n')), 'FIXED',
184
+ 'AGENTS.md created with managed block', 'cannot create AGENTS.md managed block');
151
185
  }
152
186
  } else {
153
187
  let raw = readFileSync(agentsAbs, 'utf8');
@@ -277,6 +311,34 @@ export function doctorProject(projectDir, packageRoot, opts = {}) {
277
311
  add(v ? 'OK' : 'WARN', `skill ${p}: ${slot}`);
278
312
  }
279
313
 
314
+ // 8. MCP server wiring — the archgen MCP server is what actually gives the
315
+ // agent its tools. A skill with no MCP entry is a skill the agent cannot
316
+ // use, so this is a first-class check, not a footnote.
317
+ //
318
+ // READ-ONLY and NEVER FATAL: doctor must work on a machine with no npm, no
319
+ // network, and no MCP server installed. The MCP server is asked for its own
320
+ // report; if it cannot run, that is a WARN with the remediation command.
321
+ const mcp = mcpDoctor({ projectRoot: root, env: { ...process.env, HOME: homeDir } });
322
+ if (!mcp.report) {
323
+ add('WARN', `archgen MCP server not reachable${mcp.detail ? ` — ${mcp.detail}` : ''} — install with: npx archgen-mcp bootstrap --project . --yes`);
324
+ } else {
325
+ const rep = mcp.report;
326
+ add(
327
+ rep.skill ? 'OK' : 'WARN',
328
+ rep.skill
329
+ ? `archgen MCP server v${rep.server} ready (skill ${rep.skill.version ?? 'unknown'})`
330
+ : 'archgen MCP server ready, but no skill was found',
331
+ );
332
+ const wired = (rep.mcpTargets ?? []).filter((t) => t.exists).length;
333
+ if (wired === 0) {
334
+ add('WARN', 'archgen MCP is not wired into any harness config — run: npx archgen-mcp bootstrap --project . --yes');
335
+ } else {
336
+ add('OK', `archgen MCP wiring present in ${wired} harness config(s)`);
337
+ }
338
+ if (rep.codegraph) add('OK', `codegraph ${rep.codegraph.version}`);
339
+ else add('WARN', 'codegraph not installed — run: npx archgen-mcp bootstrap --project . --yes');
340
+ }
341
+
280
342
  const tally = { OK: 0, FIXED: 0, 'WOULD-FIX': 0, WARN: 0, FAIL: 0 };
281
343
  for (const c of checks) tally[c.status]++;
282
344
  return { root, checks, tally, failures: opFailures };