archgen-skill 0.0.5 → 0.0.7

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 (39) hide show
  1. package/README.md +20 -7
  2. package/bin/archgen.mjs +142 -15
  3. package/lib/block.js +52 -11
  4. package/lib/bootstrap.js +107 -0
  5. package/lib/config.js +3 -2
  6. package/lib/doctor.js +197 -77
  7. package/lib/init.js +77 -19
  8. package/lib/install.js +79 -7
  9. package/lib/mcp-server.js +241 -0
  10. package/lib/migrations/001-stamp-provenance.mjs +15 -8
  11. package/lib/migrations/index.mjs +19 -8
  12. package/lib/restore.js +52 -16
  13. package/lib/spawn-npm.js +99 -0
  14. package/lib/store.js +175 -28
  15. package/lib/uninstall-project.js +38 -7
  16. package/lib/version.js +20 -4
  17. package/package.json +3 -3
  18. package/vendor/skills/archgen/SKILL.md +363 -213
  19. package/vendor/skills/archgen/archgen.config.json +1 -1
  20. package/vendor/skills/archgen/references/code-standards.md +2 -2
  21. package/vendor/skills/archgen/assets/mcp-template.json +0 -42
  22. package/vendor/skills/archgen/assets/skill-registry.json +0 -24
  23. package/vendor/skills/archgen/references/artifact-templates.md +0 -406
  24. package/vendor/skills/archgen/references/interview.md +0 -275
  25. package/vendor/skills/archgen/references/mcp-and-skills.md +0 -117
  26. package/vendor/skills/archgen/references/orchestration.md +0 -292
  27. package/vendor/skills/archgen/references/platforms.md +0 -138
  28. package/vendor/skills/archgen/scripts/doc-index.mjs +0 -457
  29. package/vendor/skills/archgen/scripts/impact.mjs +0 -77
  30. package/vendor/skills/archgen/scripts/lib/graph.mjs +0 -222
  31. package/vendor/skills/archgen/scripts/lib/plan-coverage.mjs +0 -32
  32. package/vendor/skills/archgen/scripts/lib/scope.mjs +0 -65
  33. package/vendor/skills/archgen/scripts/lib/yaml.mjs +0 -492
  34. package/vendor/skills/archgen/scripts/next-tasks.mjs +0 -55
  35. package/vendor/skills/archgen/scripts/plan-graph.mjs +0 -329
  36. package/vendor/skills/archgen/scripts/set-status.mjs +0 -82
  37. package/vendor/skills/archgen/scripts/update-agents.mjs +0 -213
  38. package/vendor/skills/archgen/scripts/validate.mjs +0 -87
  39. 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,
@@ -42,6 +42,13 @@ copies are backed up to `.archgen/.backup/<timestamp>/` before refresh, and lega
42
42
  dual-copy layouts are migrated to symlinks automatically. Verify anytime with
43
43
  `archgen-skill doctor`.
44
44
 
45
+ Every destructive step is behind a rail: the project root is asserted **before**
46
+ anything is written or deleted, a symlinked target is never written through, the
47
+ store swap is atomic (the old store is moved aside, not deleted, and restored if
48
+ the swap fails), and a restore puts back exactly the paths the snapshot recorded
49
+ — never a sibling skill. `archgen-skill uninstall <dir>` removes that project;
50
+ the global uninstall happens only when no project is named.
51
+
45
52
  **Or install globally** into every detected harness:
46
53
 
47
54
  ```bash
@@ -49,8 +56,13 @@ npx archgen-skill install # symlinks; --copy for real copies
49
56
  ```
50
57
 
51
58
  Prefer shell? Clone the repo and run `./install.sh` (same behavior, plus
52
- `--init`, `--project`, `--uninstall`). Requires Node.js ≥ 18. The skill itself
53
- has zero npm dependencies.
59
+ `--init`, `--project`, `--uninstall`). Requires Node.js ≥ 20 — the official MCP
60
+ SDK v2 floor.
61
+
62
+ **What `init` actually does:** installs the skill (guidance only — no code),
63
+ wires the `archgen-mcp` MCP server into every detected harness, and ensures
64
+ CodeGraph (its CLI, its MCP entry, and the project index). The CLI itself is
65
+ zero-dependency; the MCP server it installs depends on the official MCP SDK.
54
66
 
55
67
  ## Quick start
56
68
 
@@ -75,7 +87,7 @@ by git.
75
87
  | `npx archgen-skill init [dir]` | Prepare a project: canonical skill store at `.agents/skills/archgen`, Claude symlink adapter, AGENTS.md hub + CLAUDE.md bridge |
76
88
  | `npx archgen-skill install [--copy]` | Install into global harness skill dirs (symlinks by default, real copies with `--copy`) |
77
89
  | `npx archgen-skill uninstall` | Remove globally-installed copies (manifest-recorded, safe removal) |
78
- | `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 |
90
+ | `npx archgen-skill uninstall [dir]` | Remove a **project** install (same as `--project [dir]`): strips managed blocks, symlink, and store **only if unmodified** — `.archgen/` feature folders and all user content are preserved |
79
91
  | `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) |
80
92
  | `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 |
81
93
  | `npx archgen-skill restore` | List backup snapshots; `--snapshot <ts>` restores one (current state is backed up first) |
@@ -125,11 +137,12 @@ are supported for all of them.
125
137
 
126
138
  ## VS Code extension (optional)
127
139
 
128
- A read-only visual layer that renders `.archgen/` as a live task DAG, your
140
+ A visual layer that renders `.archgen/` as a live task DAG, your
129
141
  code-dependency graph, and rendered docs — with a ▶ button that hands any task to
130
142
  your agent. Install it from the VS Code Marketplace or from a `.vsix` on
131
143
  [GitHub Releases](https://github.com/akashmaurya99/archgen/releases). It never
132
- edits anything: uninstalling it loses nothing.
144
+ edits your plan, your tasks, or your code — only its own runtime state under
145
+ `.archgen/.runtime/`, so uninstalling it loses nothing.
133
146
 
134
147
  ## Documentation
135
148
 
@@ -145,7 +158,7 @@ See [CONTRIBUTING.md](https://github.com/akashmaurya99/archgen/blob/main/CONTRIB
145
158
  Skill suite and CLI tests run with zero dependencies:
146
159
 
147
160
  ```sh
148
- node --test skill/scripts/test/*.test.mjs # skill suite
161
+ node --test packages/mcp/test/**/*.test.mjs # MCP server suite
149
162
  cd packages/cli && npm test # CLI suite
150
163
  ```
151
164
 
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, globalBinPath } from '../lib/spawn-npm.js';
25
26
  import { spawnSync } from 'node:child_process';
26
27
  import { readFileSync } from 'node:fs';
27
28
 
@@ -34,7 +35,13 @@ function cliVersion() {
34
35
  function runInitAndDoctor(dir) {
35
36
  const r = initProject(dir, PACKAGE_ROOT);
36
37
  console.log(' = store refreshed at ' + r.storePath);
37
- doctorProject(dir, PACKAGE_ROOT, {});
38
+ const d = doctorProject(dir, PACKAGE_ROOT, {});
39
+ // A failed doctor must fail the update too: the project was NOT brought to
40
+ // the new version.
41
+ if (d.failures > 0) {
42
+ console.error(' ! doctor reported ' + d.failures + ' failure(s)');
43
+ process.exitCode = 1;
44
+ }
38
45
  }
39
46
 
40
47
  const HELP = `archgen — architecture generation & autonomous task execution for coding agents
@@ -44,16 +51,26 @@ Usage:
44
51
  store at .agents/skills/archgen, symlink
45
52
  .claude/skills/archgen -> ../../.agents/skills/archgen,
46
53
  and generate AGENTS.md / CLAUDE.md pointer blocks.
54
+ [--project <dir>] the project directory (same as the positional
55
+ form; give it once, not both)
47
56
  [--force] proceed automatically; divergent copies are
48
57
  backed up to .archgen/.backup/ (never destroyed)
58
+ [--no-codegraph] skip the CodeGraph bootstrap
59
+ [--no-mcp] skip wiring the archgen MCP server
49
60
  npx archgen-skill doctor [dir] Health-check a project install: store integrity,
50
61
  version stamp, claude link, managed blocks,
51
62
  manifest. Repairs safe issues in place.
63
+ [--project <dir>] the project directory (same as the positional form)
52
64
  [--check] report only - change nothing
53
65
  npx archgen-skill install Install into global harness skill dirs
54
66
  [--copy] real copies instead of symlinks
55
67
  [--uninstall] remove manifest-recorded entries
56
- npx archgen-skill uninstall Remove globally-installed copies (manifest-based)
68
+ [--no-codegraph] skip the CodeGraph bootstrap
69
+ [--no-mcp] skip wiring the archgen MCP server
70
+ npx archgen-skill uninstall [dir] Remove archgen from a project (the same
71
+ as the positional form); without a directory
72
+ the globally-installed copies are removed
73
+ (manifest-based)
57
74
  --project [dir] remove archgen from a project: claude link,
58
75
  managed blocks, canonical store (kept when
59
76
  customized) and manifest; .archgen feature
@@ -84,15 +101,74 @@ function flag(name) {
84
101
  return true;
85
102
  }
86
103
 
87
- function positionalDir() {
88
- return rest[0] ? resolve(rest[0]) : process.cwd();
104
+ /**
105
+ * Consume an option that takes a value: `--name <value>` or `--name=<value>`.
106
+ *
107
+ * Returns null when the option is absent, '' when it is present with no
108
+ * value ("use the default"), and the value otherwise.
109
+ */
110
+ function takeValue(name) {
111
+ const eq = rest.findIndex((a) => a.startsWith(name + '='));
112
+ if (eq !== -1) {
113
+ const v = rest[eq].slice(name.length + 1);
114
+ rest.splice(eq, 1);
115
+ return v;
116
+ }
117
+ const i = rest.indexOf(name);
118
+ if (i === -1) return null;
119
+ rest.splice(i, 1);
120
+ const v = rest[i];
121
+ if (v === undefined || v.startsWith('-')) return '';
122
+ rest.splice(i, 1);
123
+ return v;
124
+ }
125
+
126
+ /**
127
+ * The project directory for a command.
128
+ *
129
+ * Accepts a POSITIONAL dir (`init myproj`) and `--project <dir>` alike. The
130
+ * same flag name is used by `uninstall` and `restore`, and the MCP server's
131
+ * own commands take `--project` too — so a user who typed one form must
132
+ * never get a directory literally named "--project" created in their cwd.
133
+ *
134
+ * Extra positional arguments are REFUSED: silently ignoring the second one
135
+ * would install into a directory the user did not name.
136
+ */
137
+ function targetDir() {
138
+ const explicit = takeValue('--project');
139
+ const positionals = rest.filter((a) => !a.startsWith('-'));
140
+ if (positionals.length > 1) {
141
+ throw new Error(`unexpected argument: ${positionals[1]} — give the project directory once`);
142
+ }
143
+ const positional = positionals[0];
144
+ const explicitDir = explicit === null ? null : (explicit === '' ? process.cwd() : resolve(explicit));
145
+ if (explicitDir !== null && positional !== undefined && explicitDir !== resolve(positional)) {
146
+ throw new Error('give the project directory once: either positionally or with --project');
147
+ }
148
+ return explicitDir ?? (positional !== undefined ? resolve(positional) : process.cwd());
149
+ }
150
+
151
+ /**
152
+ * Refuse an option we do not understand.
153
+ *
154
+ * An unknown flag is a TYPO. It is never a directory name — creating a
155
+ * directory called "--no-mcp" because the flag was misspelled is exactly the
156
+ * kind of silent mess this check exists to prevent.
157
+ */
158
+ function assertNoUnknownFlags() {
159
+ const unknown = rest.find((a) => a.startsWith('-'));
160
+ if (unknown) throw new Error(`unknown option: ${unknown}`);
89
161
  }
90
162
 
91
163
  try {
92
164
  switch (cmd) {
93
165
  case 'init': {
94
166
  flag('--force');
95
- const r = initProject(positionalDir(), PACKAGE_ROOT);
167
+ const noCodegraph = flag('--no-codegraph');
168
+ const noMcp = flag('--no-mcp');
169
+ const dir = targetDir();
170
+ assertNoUnknownFlags();
171
+ const r = initProject(dir, PACKAGE_ROOT, { codegraph: !noCodegraph, mcp: !noMcp });
96
172
  console.log('archgen: project ready (single-store layout)');
97
173
  console.log(' + store ' + r.storePath);
98
174
  if (r.claudeLink === 'created') console.log(' + link .claude/skills/archgen -> ../../.agents/skills/archgen');
@@ -110,7 +186,9 @@ try {
110
186
  }
111
187
  case 'doctor': {
112
188
  const check = flag('--check');
113
- const r = doctorProject(positionalDir(), PACKAGE_ROOT, { check });
189
+ const dir = targetDir();
190
+ assertNoUnknownFlags();
191
+ const r = doctorProject(dir, PACKAGE_ROOT, { check });
114
192
  console.log('archgen doctor — ' + r.root + (check ? ' (check mode: no changes made)' : ''));
115
193
  for (const c of r.checks) console.log(` [${c.status.toLowerCase().padEnd(9)}] ${c.msg}`);
116
194
  const t = r.tally;
@@ -120,8 +198,14 @@ try {
120
198
  }
121
199
  case 'install': {
122
200
  const copy = flag('--copy');
123
- if (flag('--uninstall')) { uninstall(); break; }
124
- const r = installGlobal({ copy });
201
+ const noCodegraph = flag('--no-codegraph');
202
+ const noMcp = flag('--no-mcp');
203
+ const uninstallMode = flag('--uninstall');
204
+ assertNoUnknownFlags();
205
+ const stray = rest.find((a) => !a.startsWith('-'));
206
+ if (stray) throw new Error(`unexpected argument: ${stray} — install takes no directory`);
207
+ if (uninstallMode) { uninstall(); break; }
208
+ const r = installGlobal({ copy, codegraph: !noCodegraph, mcp: !noMcp });
125
209
  console.log('archgen: global harness install (mode: ' + (copy ? 'copy' : 'symlink') + ')');
126
210
  for (const [status, mode, path] of r.rows) console.log(` ${status.padEnd(7)} ${mode.padEnd(5)} ${path}`);
127
211
  console.log('\nPer-project setup (recommended): npx archgen-skill init');
@@ -129,14 +213,32 @@ try {
129
213
  break;
130
214
  }
131
215
  case 'uninstall': {
132
- if (flag('--project')) {
133
- const r = uninstallProject(positionalDir(), PACKAGE_ROOT);
216
+ // `--project` may carry a directory (`uninstall --project myproj`) or
217
+ // stand alone (`uninstall --project` → this directory). A POSITIONAL
218
+ // directory (`uninstall myproj`) is a project uninstall as well: the
219
+ // GLOBAL uninstall happens only when no project was named at all.
220
+ const explicit = takeValue('--project');
221
+ const positionals = rest.filter((a) => !a.startsWith('-'));
222
+ assertNoUnknownFlags();
223
+ if (positionals.length > 1) {
224
+ throw new Error(`unexpected argument: ${positionals[1]} — give the project directory once`);
225
+ }
226
+ const positional = positionals[0];
227
+ const projectTarget = explicit !== null
228
+ ? (explicit === '' ? process.cwd() : resolve(explicit))
229
+ : (positional !== undefined ? resolve(positional) : null);
230
+ if (explicit !== null && positional !== undefined && projectTarget !== resolve(positional)) {
231
+ throw new Error('give the project directory once: either positionally or with --project');
232
+ }
233
+ if (projectTarget !== null) {
234
+ const r = uninstallProject(projectTarget, PACKAGE_ROOT);
134
235
  console.log('archgen: project uninstall — ' + r.root);
135
236
  if (r.linkRemoved) console.log(' - .claude/skills/archgen (link)');
136
237
  if (r.strippedBlocks.length) console.log(' - managed blocks stripped from ' + r.strippedBlocks.join(', '));
137
238
  if (r.storeRemoved) console.log(' - .agents/skills/archgen (unmodified store)');
138
239
  if (r.storeKept) console.log(' = .agents/skills/archgen KEPT (customized)');
139
240
  if (r.manifestRemoved) console.log(' - .archgen/.install-manifest.json');
241
+ for (const m of r.mcpRemoved ?? []) console.log(` - ${m} (archgen MCP entry)`);
140
242
  for (const w of r.warnings) console.log(' note: ' + w);
141
243
  console.log(' .archgen feature folders and backups are preserved.');
142
244
  } else {
@@ -156,7 +258,8 @@ try {
156
258
  break;
157
259
  }
158
260
  case 'update': {
159
- const dir = positionalDir();
261
+ const dir = targetDir();
262
+ assertNoUnknownFlags();
160
263
  const current = cliVersion();
161
264
  console.log('archgen update — running v' + current + ', checking npm registry…');
162
265
  const latest = fetchLatestVersion('archgen-skill');
@@ -165,6 +268,14 @@ try {
165
268
  break;
166
269
  }
167
270
  const cmp = compareSemver(latest, current);
271
+ if (cmp === null) {
272
+ // The registry answered with something that is not a semver (e.g. a
273
+ // "beta" dist-tag). It is NOT "already the latest version" — refuse
274
+ // instead, and never interpolate it into a command line.
275
+ console.error(' ! registry returned a non-semver version string; refusing: ' + JSON.stringify(latest));
276
+ process.exitCode = 1;
277
+ break;
278
+ }
168
279
  if (cmp === 0) {
169
280
  console.log(' = already on the latest published version (' + latest + ').');
170
281
  console.log(' Refreshing this project against it anyway…');
@@ -177,17 +288,26 @@ try {
177
288
  runInitAndDoctor(dir);
178
289
  break;
179
290
  }
291
+ if (!/^\d+\.\d+\.\d+[-+]?[0-9A-Za-z.-]*$/.test(latest)) {
292
+ // Registry returned something that is not a semver tag — never
293
+ // interpolate it into a command line.
294
+ console.error(' ! registry returned a non-semver version string; refusing: ' + JSON.stringify(latest));
295
+ process.exitCode = 1;
296
+ break;
297
+ }
180
298
  console.log(' ↑ newer version available: ' + latest);
181
299
  console.log(' upgrading global install: npm install -g archgen-skill@' + latest);
182
- const up = spawnSync('npm', ['install', '-g', 'archgen-skill@' + latest], { stdio: 'inherit' });
300
+ const inv = npmInvocation(['install', '-g', 'archgen-skill@' + latest]);
301
+ const up = spawnSync(inv.command, inv.args, { stdio: 'inherit', shell: inv.shell });
183
302
  if (up.status !== 0) {
184
303
  console.error(' ! global upgrade failed — run it manually, then re-run: archgen-skill update');
185
304
  process.exitCode = 1;
186
305
  break;
187
306
  }
188
307
  // 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();
308
+ // The PATH search would find the FIRST match — often the OLD global
309
+ // install — so the binary npm actually wrote is preferred.
310
+ const freshBin = globalBinPath('archgen-skill') ?? whichOnPath('archgen-skill');
191
311
  if (!freshBin) {
192
312
  console.log('\nUpgraded to v' + latest + '. Now finish per project with:\n npx archgen-skill@latest init');
193
313
  break;
@@ -201,7 +321,13 @@ try {
201
321
  process.exitCode = upInit.status ?? 1;
202
322
  break;
203
323
  }
204
- spawnSync(freshBin, ['doctor', dir], { stdio: 'inherit' });
324
+ const upDoctor = spawnSync(freshBin, ['doctor', dir], { stdio: 'inherit' });
325
+ if (upDoctor.status !== 0) {
326
+ // A failed doctor must fail the update too — the project was NOT
327
+ // brought to the new version.
328
+ console.error(' ! doctor failed during update (see message above)');
329
+ process.exitCode = upDoctor.status ?? 1;
330
+ }
205
331
  break;
206
332
  }
207
333
  case '--version': case '-v': {
@@ -226,5 +352,6 @@ function uninstall() {
226
352
  const r = uninstallGlobal();
227
353
  if (r.noop) { console.log('archgen: nothing to uninstall (no manifest found).'); return; }
228
354
  console.log(`archgen: removed ${r.removed} entr${r.removed === 1 ? 'y' : 'ies'}` + (r.failed ? `, ${r.failed} FAILED` : ''));
355
+ for (const b of r.backups ?? []) console.log(' ~ backup ' + b + ' (edited copy saved before removal)');
229
356
  if (r.failed > 0) process.exitCode = 1;
230
357
  }
package/lib/block.js CHANGED
@@ -16,7 +16,7 @@
16
16
  // place on the next init/doctor pass and detectBlockVersion() lets doctor
17
17
  // report stale formats precisely.
18
18
 
19
- import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
19
+ import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
20
20
  import { loadConfig } from './config.js';
21
21
  import { assertNotSymlink } from './store.js';
22
22
 
@@ -98,7 +98,7 @@ export function renderBlock(skillRelPath = '.agents/skills/archgen', eol = '\n')
98
98
  '',
99
99
  'Rules of the road:',
100
100
  '- Generated artifacts live ONLY under `.archgen/<slug>/`',
101
- '- 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)',
102
102
  '- Two gates before any execution: verifier approval, then human approval',
103
103
  '',
104
104
  '## Features registry',
@@ -171,11 +171,14 @@ export function upsertBlock(existing, block, eol = '\n') {
171
171
  if (s === -1 || e === -1) {
172
172
  throw new Error('found only one archgen marker - fix or remove it manually');
173
173
  }
174
+ const prefix = trimmed.slice(0, s);
174
175
  const tail = stripCompleteBlocks(trimmed.slice(e + END.length));
175
- if (tail.includes(START) || tail.includes(END)) {
176
+ // An orphan marker ANYWHERE (before the first block as well as after it)
177
+ // would survive the rewrite and leave the file broken — refuse instead.
178
+ if (prefix.includes(START) || prefix.includes(END) || tail.includes(START) || tail.includes(END)) {
176
179
  throw new Error('found only one archgen marker - fix or remove it manually');
177
180
  }
178
- return trimmed.slice(0, s) + fresh + tail;
181
+ return prefix + fresh + tail;
179
182
  }
180
183
 
181
184
  const startIdx = trimmed.indexOf(START);
@@ -209,13 +212,37 @@ function splitBom(raw) {
209
212
  return { bom, body: bom ? raw.slice(1) : raw };
210
213
  }
211
214
 
212
- /** Write via sibling temp file + rename so readers never observe partial content. */
213
- function atomicWrite(absPath, data) {
214
- const tmp = absPath + '.tmp-' + process.pid + '-' + Date.now();
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' });
218
- renameSync(tmp, absPath);
215
+ /**
216
+ * Write via sibling temp file + rename so readers never observe partial content.
217
+ * @param {string} absPath target file
218
+ * @param {string} data contents
219
+ */
220
+ export function atomicWrite(absPath, data) {
221
+ let tmp = absPath + '.tmp-' + process.pid + '-' + Date.now();
222
+ for (let attempt = 0; ; attempt++) {
223
+ try {
224
+ // 'wx' (O_CREAT|O_EXCL): if the tmp name already exists — e.g. an attacker
225
+ // planted a symlink there to redirect the write — fail instead of following.
226
+ writeFileSync(tmp, data, { flag: 'wx' });
227
+ break;
228
+ } catch (e) {
229
+ // A collision (two writes inside the same millisecond) is not a failure:
230
+ // retry with a fresh name instead of failing the whole write.
231
+ if (e && e.code === 'EEXIST' && attempt < 10) {
232
+ tmp = absPath + '.tmp-' + process.pid + '-' + Date.now() + '-' + (attempt + 1);
233
+ continue;
234
+ }
235
+ throw e;
236
+ }
237
+ }
238
+ try {
239
+ renameSync(tmp, absPath);
240
+ } catch (e) {
241
+ // Never leave the temp file behind: a failed rename must not litter the
242
+ // project with half-written siblings.
243
+ try { unlinkSync(tmp); } catch { /* original intact; leftover tmp is harmless */ }
244
+ throw e;
245
+ }
219
246
  }
220
247
 
221
248
  /**
@@ -250,6 +277,20 @@ export function importsAgents(absPath) {
250
277
  return body.split(/\r?\n/).some((l) => l.trim() === '@AGENTS.md');
251
278
  }
252
279
 
280
+ /**
281
+ * True when the file carries a COMPLETE managed block (both markers).
282
+ *
283
+ * A file with a hand-written `@AGENTS.md` import has none — that import is
284
+ * the user's, not ours, and must survive untouched. A file WITH a managed
285
+ * block is ours: its provenance stamp is refreshed on every init, exactly
286
+ * like the AGENTS.md block.
287
+ */
288
+ export function hasManagedBlock(absPath) {
289
+ let raw = '';
290
+ try { raw = readFileSync(absPath, 'utf8'); } catch { return false; }
291
+ return raw.includes(START) && raw.includes(END);
292
+ }
293
+
253
294
  /** Strip the managed block from a file on disk, preserving BOM. */
254
295
  export function stripManagedFile(absPath) {
255
296
  assertNotSymlink(absPath);
@@ -0,0 +1,107 @@
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 to
15
+ // the caller (ok:false + detail) and rendered as a warning by it — never
16
+ // thrown, and never swallowed.
17
+ import { bootstrapMcp, resolveMcpBinary } from './mcp-server.js';
18
+
19
+ /**
20
+ * @param {{
21
+ * projectDir: string,
22
+ * location?: 'project'|'global'|'both',
23
+ * log?: (line: string) => void,
24
+ * env?: Record<string, string|undefined>,
25
+ * noCodegraph?: boolean,
26
+ * noMcp?: boolean,
27
+ * }} opts
28
+ * @returns {{ok: boolean, skipped?: string, detail?: string, report?: any}}
29
+ */
30
+ export function bootstrapServers(opts) {
31
+ const {
32
+ projectDir,
33
+ location = 'both',
34
+ log = () => {},
35
+ env = process.env,
36
+ noCodegraph = false,
37
+ noMcp = false,
38
+ } = opts;
39
+
40
+ // ARCHGEN_CODEGRAPH=0 skips the bootstrap (no spawn, no network, no writes)
41
+ // — without it the test suite would wire the developer's real MCP configs.
42
+ if (env.ARCHGEN_CODEGRAPH === '0' || env.ARCHGEN_CODEGRAPH === 'off') {
43
+ return { ok: true, skipped: 'ARCHGEN_CODEGRAPH=0 (bootstrap disabled)' };
44
+ }
45
+ if (noMcp && noCodegraph) {
46
+ return { ok: true, skipped: '--no-mcp and --no-codegraph passed' };
47
+ }
48
+
49
+ const resolved = resolveMcpBinary({ projectRoot: projectDir, env });
50
+ log(` + mcp server: ${resolved.bin ?? 'npx -y archgen-mcp'} (${resolved.source})`);
51
+
52
+ if (noMcp) {
53
+ log(' ! archgen MCP wiring skipped (--no-mcp)');
54
+ }
55
+ if (noCodegraph) {
56
+ log(' ! CodeGraph bootstrap skipped (--no-codegraph)');
57
+ }
58
+
59
+ const r = bootstrapMcp({
60
+ projectDir,
61
+ location,
62
+ approved: true,
63
+ env,
64
+ });
65
+
66
+ if (!r.report) {
67
+ // The bootstrap did NOT run: report it as a failure so the caller tells
68
+ // the user. The skill install itself still succeeded — this is a warning,
69
+ // not a fatal error — but it must never be swallowed silently.
70
+ return {
71
+ ok: false,
72
+ detail: `server bootstrap incomplete: ${r.detail ?? 'unknown error'}`,
73
+ };
74
+ }
75
+
76
+ const report = r.report;
77
+ const mcp = report.mcp ?? {};
78
+ const codegraph = report.codegraph ?? {};
79
+
80
+ for (const w of mcp.wired ?? []) {
81
+ log(` + harness: ${w.harness} → ${w.path} [${w.key}] ${w.status}${w.detail ? ' — ' + w.detail : ''}`);
82
+ }
83
+ if (codegraph.cli) {
84
+ log(
85
+ ` + codegraph ${codegraph.cli.version ?? 'unknown'}${codegraph.cli.action !== 'none' ? ' (' + codegraph.cli.action + ')' : ''}${codegraph.cli.latest ? ' · latest available ' + codegraph.cli.latest : ''}`,
86
+ );
87
+ }
88
+ if (codegraph.telemetry) {
89
+ log(` + telemetry: ${codegraph.telemetry.state ?? 'unknown'}${codegraph.telemetry.action !== 'none' ? ' → ' + codegraph.telemetry.action : ''}`);
90
+ }
91
+ for (const w of codegraph.wiring?.native ?? []) {
92
+ log(` + harness: ${w.targets} (${w.location}) → ${w.status}${w.detail ? ' — ' + w.detail : ''}`);
93
+ }
94
+ if (codegraph.index) {
95
+ log(` + index: ${codegraph.index.action ?? 'none'}${codegraph.index.files != null ? ' (' + codegraph.index.files + ' files)' : ''}`);
96
+ }
97
+ log(' + realtime: the MCP server watches files — the index stays live, no re-indexing needed');
98
+
99
+ if (r.ok === false || codegraph.ok === false) {
100
+ return {
101
+ ok: false,
102
+ report,
103
+ detail: r.detail ? `server bootstrap incomplete: ${r.detail}` : 'one or more server bootstrap steps failed',
104
+ };
105
+ }
106
+ return { ok: true, report };
107
+ }
package/lib/config.js CHANGED
@@ -2,8 +2,9 @@
2
2
  //
3
3
  // WHY: the version, managed-block marker strings, and well-known filenames
4
4
  // were hardcoded independently in block.js, store.js, install.js, install.sh,
5
- // and SKILL.md frontmatter — and drifted (SKILL.md advertised 1.0.0 while the
6
- // CLI shipped 0.0.x). The canonical file lives at <repo-root>/archgen.config.json;
5
+ // and SKILL.md frontmatter — and drifted (SKILL.md advertised a version the
6
+ // CLI had never shipped). The canonical file lives at
7
+ // <repo-root>/archgen.config.json;
7
8
  // a byte-identical derived copy ships inside the published npm package at
8
9
  // vendor/skills/archgen/archgen.config.json (kept in sync by
9
10
  // scripts/sync-config.mjs + scripts/sync-vendor.mjs).