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.
- package/README.md +39 -45
- package/bin/archgen.mjs +30 -6
- package/lib/block.js +55 -9
- package/lib/bootstrap.js +103 -0
- package/lib/doctor.js +67 -5
- package/lib/init.js +43 -4
- package/lib/install.js +40 -4
- package/lib/mcp-server.js +159 -0
- package/lib/migrations/001-stamp-provenance.mjs +3 -3
- package/lib/spawn-npm.js +54 -0
- package/lib/store.js +174 -9
- package/lib/uninstall-project.js +16 -3
- package/lib/version-stamp.js +3 -1
- package/lib/version.js +11 -3
- package/package.json +5 -4
- package/vendor/skills/archgen/SKILL.md +370 -315
- package/vendor/skills/archgen/archgen.config.json +1 -1
- package/vendor/skills/archgen/references/code-standards.md +2 -2
- package/vendor/skills/archgen/assets/mcp-template.json +0 -42
- package/vendor/skills/archgen/assets/skill-registry.json +0 -24
- package/vendor/skills/archgen/references/artifact-templates.md +0 -349
- package/vendor/skills/archgen/references/interview.md +0 -230
- package/vendor/skills/archgen/references/mcp-and-skills.md +0 -117
- package/vendor/skills/archgen/references/orchestration.md +0 -169
- package/vendor/skills/archgen/references/platforms.md +0 -83
- package/vendor/skills/archgen/scripts/doc-index.mjs +0 -457
- package/vendor/skills/archgen/scripts/impact.mjs +0 -77
- package/vendor/skills/archgen/scripts/lib/graph.mjs +0 -207
- package/vendor/skills/archgen/scripts/lib/plan-coverage.mjs +0 -32
- package/vendor/skills/archgen/scripts/lib/scope.mjs +0 -65
- package/vendor/skills/archgen/scripts/lib/yaml.mjs +0 -492
- package/vendor/skills/archgen/scripts/next-tasks.mjs +0 -55
- package/vendor/skills/archgen/scripts/plan-graph.mjs +0 -329
- package/vendor/skills/archgen/scripts/set-status.mjs +0 -60
- package/vendor/skills/archgen/scripts/update-agents.mjs +0 -204
- package/vendor/skills/archgen/scripts/validate.mjs +0 -87
- package/vendor/skills/archgen/scripts/verify-plan.mjs +0 -70
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
[](https://github.com/akashmaurya99/archgen/actions/workflows/ci.yml)
|
|
8
8
|
[](https://github.com/akashmaurya99/archgen/blob/main/LICENSE)
|
|
9
|
-
[](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
|
|
37
|
-
| `AGENTS.md` | Managed hub block: skill pointer
|
|
38
|
-
| `CLAUDE.md` | One-line bridge: `@AGENTS.md` (existing
|
|
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
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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 ≥
|
|
53
|
-
|
|
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
|
|
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
|
|
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]` |
|
|
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,
|
|
77
|
-
| `npx archgen-skill doctor [dir] [--check]` | Verify
|
|
78
|
-
| `npx archgen-skill
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
95
|
+
guarantee:
|
|
96
96
|
|
|
97
97
|
| Guarantee | Mechanism |
|
|
98
98
|
| --- | --- |
|
|
99
|
-
| Plans are valid before work starts | Verifier gate —
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
[GitHub Releases](https://github.com/akashmaurya99/archgen/releases). It
|
|
143
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 -
|
|
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
|
|
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
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 |',
|
package/lib/bootstrap.js
ADDED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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 };
|