archgen-skill 0.0.3 → 0.0.5
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 +31 -33
- package/bin/archgen.mjs +31 -1
- package/lib/block.js +147 -16
- package/lib/config.js +96 -0
- package/lib/doctor.js +144 -28
- package/lib/init.js +14 -3
- package/lib/install.js +124 -16
- package/lib/migrate.js +88 -0
- package/lib/migrations/001-stamp-provenance.mjs +77 -0
- package/lib/migrations/index.mjs +170 -0
- package/lib/restore.js +239 -0
- package/lib/store.js +125 -15
- package/lib/uninstall-project.js +6 -3
- package/lib/version-stamp.js +49 -0
- package/package.json +39 -10
- package/vendor/skills/archgen/SKILL.md +218 -145
- package/vendor/skills/archgen/archgen.config.json +16 -0
- package/vendor/skills/archgen/assets/skill-registry.json +4 -3
- package/vendor/skills/archgen/references/artifact-templates.md +308 -7
- package/vendor/skills/archgen/references/interview.md +226 -17
- package/vendor/skills/archgen/references/mcp-and-skills.md +88 -17
- package/vendor/skills/archgen/references/orchestration.md +230 -44
- package/vendor/skills/archgen/references/platforms.md +56 -14
- package/vendor/skills/archgen/scripts/doc-index.mjs +457 -0
- package/vendor/skills/archgen/scripts/impact.mjs +14 -1
- package/vendor/skills/archgen/scripts/lib/graph.mjs +99 -5
- package/vendor/skills/archgen/scripts/lib/scope.mjs +65 -0
- package/vendor/skills/archgen/scripts/lib/yaml.mjs +19 -3
- package/vendor/skills/archgen/scripts/plan-graph.mjs +329 -0
- package/vendor/skills/archgen/scripts/set-status.mjs +51 -9
- package/vendor/skills/archgen/scripts/update-agents.mjs +16 -5
- package/vendor/skills/archgen/scripts/validate.mjs +18 -1
- package/vendor/skills/archgen/scripts/verify-plan.mjs +8 -3
package/lib/store.js
CHANGED
|
@@ -11,7 +11,11 @@
|
|
|
11
11
|
// - hashDir() fingerprints a tree (stable-order walk of relpaths + contents,
|
|
12
12
|
// sha256) so mutations only ever happen after verifying ownership/divergence;
|
|
13
13
|
// - GUARDED_RELPATHS (.claude, .agents, .claude/skills, .agents/skills) are
|
|
14
|
-
// never deleted: parent pruning stops before touching those levels
|
|
14
|
+
// never deleted: parent pruning stops before touching those levels;
|
|
15
|
+
// - assertNotSymlink()/assertWithinRoot() lstat BEFORE every write: a
|
|
16
|
+
// symlinked target (supply-chain attack: a malicious clone shipping
|
|
17
|
+
// .agents/skills/archgen or AGENTS.md as a link to an unrelated directory)
|
|
18
|
+
// is refused instead of written through.
|
|
15
19
|
|
|
16
20
|
import { createHash } from 'node:crypto';
|
|
17
21
|
import {
|
|
@@ -22,20 +26,91 @@ import {
|
|
|
22
26
|
readFileSync,
|
|
23
27
|
readdirSync,
|
|
24
28
|
readlinkSync,
|
|
29
|
+
realpathSync,
|
|
25
30
|
renameSync,
|
|
26
31
|
rmSync,
|
|
27
32
|
writeFileSync,
|
|
28
33
|
} from 'node:fs';
|
|
29
|
-
import { dirname, join } from 'node:path';
|
|
34
|
+
import { dirname, join, sep } from 'node:path';
|
|
35
|
+
import { loadConfig } from './config.js';
|
|
30
36
|
|
|
31
37
|
export const STORE_REL = '.agents/skills/archgen';
|
|
32
38
|
export const CLAUDE_LINK_REL = '.claude/skills/archgen';
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
|
|
40
|
+
// Filenames routed through the canonical config (STORE_REL/CLAUDE_LINK_REL
|
|
41
|
+
// stay literal: they are path semantics, not config).
|
|
42
|
+
const CONFIG_FILES = loadConfig().files;
|
|
43
|
+
export const MANIFEST_REL = '.archgen/' + CONFIG_FILES.projectManifest;
|
|
44
|
+
export const BACKUP_ROOT_REL = '.archgen/' + CONFIG_FILES.backupDir;
|
|
45
|
+
export const VERSION_FILE = CONFIG_FILES.stamp;
|
|
36
46
|
|
|
37
47
|
const GUARDED_RELPATHS = new Set(['.claude', '.agents', '.claude/skills', '.agents/skills']);
|
|
38
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Dev-only skill subtrees that must NEVER be distributed to end users. The
|
|
51
|
+
* repo's own node:test suite (scripts/test) is repo-only CI tooling: nothing
|
|
52
|
+
* imports it at runtime and it references repo-only fixtures
|
|
53
|
+
* (fixtures/yaml-corpus), so it cannot run in an installed project — shipping
|
|
54
|
+
* it into installed stores or the npm payload is dead weight. It is excluded
|
|
55
|
+
* consistently from the vendor mirror, every installed-store copy, and BOTH
|
|
56
|
+
* sides of hashDir comparisons (so its presence/absence never reads as drift).
|
|
57
|
+
* scripts/lib is deliberately NOT here — it is a runtime dependency of the
|
|
58
|
+
* skill scripts (imported by next-tasks/plan-graph/set-status/doc-index/etc).
|
|
59
|
+
*/
|
|
60
|
+
export const DEV_ONLY_RELPATHS = ['scripts/test'];
|
|
61
|
+
|
|
62
|
+
/** True when relpath `r` is (or lives inside) a dev-only excluded subtree. */
|
|
63
|
+
function isDevOnlyRelPath(r) {
|
|
64
|
+
for (const prefix of DEV_ONLY_RELPATHS) {
|
|
65
|
+
if (r === prefix || r.startsWith(prefix + '/')) return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Canonical refusal message for every symlink-write guard (CLI + install.sh). */
|
|
71
|
+
export function refuseSymlinkMessage(absPath) {
|
|
72
|
+
return 'Refusing to write through symlink at ' + absPath + ' — use --copy or remove symlink';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Supply-chain guard: lstat (never stat — the LINK itself, not its target) a
|
|
77
|
+
* path before writing it. Absent paths pass (safe to create); a symlink at
|
|
78
|
+
* the target throws so no write can ever be redirected through the link.
|
|
79
|
+
*/
|
|
80
|
+
export function assertNotSymlink(absPath) {
|
|
81
|
+
let st = null;
|
|
82
|
+
try { st = lstatSync(absPath); } catch { return; }
|
|
83
|
+
if (st.isSymbolicLink()) throw new Error(refuseSymlinkMessage(absPath));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Realpath-escape guard: resolve `absPath` (climbing to the nearest existing
|
|
88
|
+
* ancestor when it does not exist yet, so a symlinked PARENT dir is caught
|
|
89
|
+
* before anything is created beneath it) and refuse when the real location
|
|
90
|
+
* lands outside `rootAbs`. Catches `.agents -> /elsewhere` style redirects
|
|
91
|
+
* that a final-component lstat cannot see.
|
|
92
|
+
*/
|
|
93
|
+
export function assertWithinRoot(rootAbs, absPath) {
|
|
94
|
+
let rootReal;
|
|
95
|
+
try { rootReal = realpathSync(rootAbs); } catch { return; }
|
|
96
|
+
let probe = absPath;
|
|
97
|
+
for (;;) {
|
|
98
|
+
let exists = true;
|
|
99
|
+
try { lstatSync(probe); } catch { exists = false; }
|
|
100
|
+
if (exists) {
|
|
101
|
+
let real;
|
|
102
|
+
try { real = realpathSync(probe); } catch { return; } // broken chain: nothing writable through it
|
|
103
|
+
if (real !== rootReal && !real.startsWith(rootReal + sep)) {
|
|
104
|
+
throw new Error(refuseSymlinkMessage(absPath));
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const parent = dirname(probe);
|
|
109
|
+
if (parent === probe) return;
|
|
110
|
+
probe = parent;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
39
114
|
/** Version string of this CLI package (used for the in-store stamp). */
|
|
40
115
|
export function cliVersion(packageRoot) {
|
|
41
116
|
return JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8')).version;
|
|
@@ -48,17 +123,24 @@ function byName(a, b) {
|
|
|
48
123
|
/**
|
|
49
124
|
* Stable-order directory fingerprint: sha256 over sorted relpaths plus file
|
|
50
125
|
* contents (symlinks hashed via their targets). Deterministic across machines.
|
|
126
|
+
* The version stamp is ALWAYS ignored on both sides of a comparison: it is
|
|
127
|
+
* CLI metadata written at destination time (global symlink installs stamp the
|
|
128
|
+
* shared real copy), never skill content, so it must not read as divergence.
|
|
129
|
+
* DEV_ONLY_RELPATHS (scripts/test) are ALSO always ignored on both sides: they
|
|
130
|
+
* are pruned from every distributed copy, so a source tree that still carries
|
|
131
|
+
* them (the dev-checkout skill/ fallback) must hash equal to an installed store
|
|
132
|
+
* that does not — while a real edit anywhere else must still read as divergence.
|
|
51
133
|
* @param {string} dir absolute directory to hash
|
|
52
|
-
* @param {{ignore?: string[]}} opts relpaths to skip
|
|
134
|
+
* @param {{ignore?: string[]}} opts EXTRA relpaths to skip
|
|
53
135
|
*/
|
|
54
136
|
export function hashDir(dir, opts = {}) {
|
|
55
|
-
const ignore = opts.ignore ?? [];
|
|
137
|
+
const ignore = new Set([VERSION_FILE, ...(opts.ignore ?? [])]);
|
|
56
138
|
const hash = createHash('sha256');
|
|
57
139
|
(function walk(abs, rel) {
|
|
58
140
|
const entries = readdirSync(abs, { withFileTypes: true }).sort((a, b) => byName(a.name, b.name));
|
|
59
141
|
for (const ent of entries) {
|
|
60
142
|
const r = rel ? rel + '/' + ent.name : ent.name;
|
|
61
|
-
if (ignore.
|
|
143
|
+
if (ignore.has(r) || isDevOnlyRelPath(r)) continue;
|
|
62
144
|
const p = join(abs, ent.name);
|
|
63
145
|
if (ent.isSymbolicLink()) hash.update('l ' + r + ' -> ' + readlinkSync(p) + '\n');
|
|
64
146
|
else if (ent.isDirectory()) { hash.update('d ' + r + '\n'); walk(p, r); }
|
|
@@ -68,6 +150,20 @@ export function hashDir(dir, opts = {}) {
|
|
|
68
150
|
return hash.digest('hex');
|
|
69
151
|
}
|
|
70
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Remove every dev-only subtree (DEV_ONLY_RELPATHS) from an absolute skill-tree
|
|
155
|
+
* root; idempotent no-op when a subtree is absent. Called after EVERY copy of
|
|
156
|
+
* the skill into a distributed location (installed store, global --copy tree,
|
|
157
|
+
* vendor mirror) so dev-only artifacts never reach end users regardless of which
|
|
158
|
+
* source resolveSkillSource picked — vendor is already pruned by sync-vendor,
|
|
159
|
+
* but the dev-checkout skill/ fallback is not, and this normalizes both.
|
|
160
|
+
*/
|
|
161
|
+
export function pruneDevOnly(absRoot) {
|
|
162
|
+
for (const rel of DEV_ONLY_RELPATHS) {
|
|
163
|
+
rmSync(join(absRoot, ...rel.split('/')), { recursive: true, force: true });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
71
167
|
/** Load `.archgen/.install-manifest.json`; null when absent/corrupt. */
|
|
72
168
|
export function loadManifest(root) {
|
|
73
169
|
const p = join(root, ...MANIFEST_REL.split('/'));
|
|
@@ -83,6 +179,7 @@ export function loadManifest(root) {
|
|
|
83
179
|
export function saveManifest(root, manifest) {
|
|
84
180
|
const p = join(root, ...MANIFEST_REL.split('/'));
|
|
85
181
|
mkdirSync(dirname(p), { recursive: true });
|
|
182
|
+
assertNotSymlink(p);
|
|
86
183
|
writeFileSync(p, JSON.stringify(manifest, null, 2) + '\n');
|
|
87
184
|
}
|
|
88
185
|
|
|
@@ -99,25 +196,38 @@ export function appendEntry(root, kind, relPath) {
|
|
|
99
196
|
}
|
|
100
197
|
|
|
101
198
|
/**
|
|
102
|
-
* Move an existing
|
|
103
|
-
*
|
|
104
|
-
*
|
|
199
|
+
* Move an existing path into `<root>/<backupRootRel>/<ts>/<relPath>`, preserving
|
|
200
|
+
* the relative layout so backups restore unambiguously. Generalized core shared
|
|
201
|
+
* by the project vault (.archgen/.backup) and the global vault
|
|
202
|
+
* (<skills-dir>/.archgen-backups); `ts` can be injected so one operation lands
|
|
203
|
+
* every entry under a single timestamp.
|
|
204
|
+
* @returns {string} backup location relative to `root`
|
|
105
205
|
*/
|
|
106
|
-
export function
|
|
107
|
-
const
|
|
108
|
-
const destRel = BACKUP_ROOT_REL + '/' + ts + '/' + relPath;
|
|
206
|
+
export function moveToBackupInto(root, relPath, backupRootRel, ts = new Date().toISOString().replace(/:/g, '-')) {
|
|
207
|
+
const destRel = backupRootRel + '/' + ts + '/' + relPath;
|
|
109
208
|
const destAbs = join(root, ...destRel.split('/'));
|
|
110
209
|
mkdirSync(dirname(destAbs), { recursive: true });
|
|
111
210
|
const srcAbs = join(root, ...relPath.split('/'));
|
|
112
211
|
try {
|
|
113
212
|
renameSync(srcAbs, destAbs);
|
|
114
|
-
} catch {
|
|
213
|
+
} catch (err) {
|
|
214
|
+
// Fallback copy+remove must NEVER run on a symlinked source: cpSync would
|
|
215
|
+
// read through the link and rmSync would remove it — rethrow instead so
|
|
216
|
+
// the caller refuses the replace rather than touching the link's target.
|
|
217
|
+
let st = null;
|
|
218
|
+
try { st = lstatSync(srcAbs); } catch { /* vanished mid-move */ }
|
|
219
|
+
if (st && st.isSymbolicLink()) throw err;
|
|
115
220
|
cpSync(srcAbs, destAbs, { recursive: true });
|
|
116
221
|
rmSync(srcAbs, { recursive: true, force: true });
|
|
117
222
|
}
|
|
118
223
|
return destRel;
|
|
119
224
|
}
|
|
120
225
|
|
|
226
|
+
/** Project-root flavor: move `relPath` under `.archgen/.backup/<ts>/<relPath>`. */
|
|
227
|
+
export function moveToBackup(root, relPath) {
|
|
228
|
+
return moveToBackupInto(root, relPath, BACKUP_ROOT_REL);
|
|
229
|
+
}
|
|
230
|
+
|
|
121
231
|
export function rmIfExists(absPath) {
|
|
122
232
|
let st = null;
|
|
123
233
|
try { st = lstatSync(absPath); } catch { /* absent */ }
|
package/lib/uninstall-project.js
CHANGED
|
@@ -69,9 +69,12 @@ export function uninstallProject(projectDir, packageRoot) {
|
|
|
69
69
|
let storeKept = false;
|
|
70
70
|
if (lstatSafe(storeAbs)) {
|
|
71
71
|
const st = lstatSafe(storeAbs);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
let mine = null;
|
|
73
|
+
if (st.isDirectory() && !st.isSymbolicLink()) {
|
|
74
|
+
// An unreadable/diverging-mid-walk tree must degrade to "kept", never
|
|
75
|
+
// crash the uninstall.
|
|
76
|
+
try { mine = hashDir(storeAbs, { ignore: [VERSION_FILE] }); } catch { mine = null; }
|
|
77
|
+
}
|
|
75
78
|
let vendor = null;
|
|
76
79
|
try { vendor = hashDir(resolveSkillSource(packageRoot)); } catch { vendor = null; }
|
|
77
80
|
if (vendor !== null && mine === vendor) {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// version-stamp.js — the `.archgen-version` stamp contract.
|
|
2
|
+
//
|
|
3
|
+
// WHY: the VS Code extension reads `<skillRoot>/.archgen-version` to tell an
|
|
4
|
+
// outdated skill copy from a current one and warns non-blockingly (missing or
|
|
5
|
+
// unparseable content means "unknown (legacy)" — a pre-stamp install). The
|
|
6
|
+
// CLI is the only writer: every operation that lays down skill files (init,
|
|
7
|
+
// global install in symlink AND --copy mode, update via its init/doctor
|
|
8
|
+
// refresh) rewrites the stamp with the CLI's own package version.
|
|
9
|
+
//
|
|
10
|
+
// Format: EXACTLY `MAJOR.MINOR.PATCH\n`, utf8, no BOM. Readers elsewhere trim
|
|
11
|
+
// before validating, so CRLF/whitespace is tolerated on read — this writer
|
|
12
|
+
// never produces it. Absent file and invalid content both read as null.
|
|
13
|
+
|
|
14
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { dirname, join } from 'node:path';
|
|
16
|
+
import { VERSION_FILE, assertNotSymlink } from './store.js';
|
|
17
|
+
|
|
18
|
+
const SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Read the stamp from a skill root (the directory containing SKILL.md).
|
|
22
|
+
* @returns {string|null} the trimmed `MAJOR.MINOR.PATCH` string, or null when
|
|
23
|
+
* the stamp is absent, unreadable, or not plain semver ("unknown").
|
|
24
|
+
*/
|
|
25
|
+
export function readStamp(skillRoot) {
|
|
26
|
+
if (!skillRoot) return null;
|
|
27
|
+
let raw;
|
|
28
|
+
try {
|
|
29
|
+
raw = readFileSync(join(skillRoot, VERSION_FILE), 'utf8');
|
|
30
|
+
} catch {
|
|
31
|
+
return null; // absent (legacy pre-stamp install) or unreadable
|
|
32
|
+
}
|
|
33
|
+
const body = raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw;
|
|
34
|
+
const v = body.trim();
|
|
35
|
+
return SEMVER_RE.test(v) ? v : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Write/refresh the stamp inside a skill root. Idempotent: overwriting an
|
|
40
|
+
* existing stamp with the same version yields byte-identical content.
|
|
41
|
+
* Creates parent directories as needed.
|
|
42
|
+
*/
|
|
43
|
+
export function writeStamp(skillRoot, version) {
|
|
44
|
+
const p = join(skillRoot, VERSION_FILE);
|
|
45
|
+
assertNotSymlink(skillRoot);
|
|
46
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
47
|
+
assertNotSymlink(p);
|
|
48
|
+
writeFileSync(p, version + '\n', 'utf8');
|
|
49
|
+
}
|
package/package.json
CHANGED
|
@@ -1,20 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "archgen-skill",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "Architecture generation & autonomous task execution for coding agents - conversational requirements in, dependency-ordered tasks out, executed wave-by-wave with verifier gates. Works with Claude Code, OpenCode, Cursor, Codex, Gemini CLI and any agentskills.io harness.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"bin": {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"
|
|
11
|
-
|
|
7
|
+
"bin": {
|
|
8
|
+
"archgen-skill": "bin/archgen.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"lib/",
|
|
13
|
+
"vendor/",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"ai",
|
|
21
|
+
"agents",
|
|
22
|
+
"claude",
|
|
23
|
+
"codex",
|
|
24
|
+
"cursor",
|
|
25
|
+
"opencode",
|
|
26
|
+
"gemini",
|
|
27
|
+
"architecture",
|
|
28
|
+
"skills",
|
|
29
|
+
"orchestration",
|
|
30
|
+
"task-graph",
|
|
31
|
+
"planning"
|
|
32
|
+
],
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/akashmaurya99/archgen.git"
|
|
36
|
+
},
|
|
12
37
|
"homepage": "https://github.com/akashmaurya99/archgen#readme",
|
|
13
|
-
"bugs": {
|
|
14
|
-
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/akashmaurya99/archgen/issues"
|
|
40
|
+
},
|
|
41
|
+
"//": "vendor/skills/archgen is a full mirror of ../../skill synced by scripts/sync-vendor.mjs (run automatically via prepublishOnly). The extension ships separately as a VS Code vsix.",
|
|
15
42
|
"scripts": {
|
|
16
|
-
"sync": "node scripts/sync-
|
|
17
|
-
"
|
|
43
|
+
"sync:config": "node scripts/sync-config.mjs",
|
|
44
|
+
"sync:check": "node scripts/sync-config.mjs --check",
|
|
45
|
+
"sync": "node scripts/sync-config.mjs && node scripts/sync-vendor.mjs",
|
|
46
|
+
"prepublishOnly": "node scripts/sync-config.mjs && node scripts/sync-vendor.mjs",
|
|
18
47
|
"test": "node --test test/*.test.mjs"
|
|
19
48
|
}
|
|
20
49
|
}
|