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.
Files changed (33) hide show
  1. package/README.md +31 -33
  2. package/bin/archgen.mjs +31 -1
  3. package/lib/block.js +147 -16
  4. package/lib/config.js +96 -0
  5. package/lib/doctor.js +144 -28
  6. package/lib/init.js +14 -3
  7. package/lib/install.js +124 -16
  8. package/lib/migrate.js +88 -0
  9. package/lib/migrations/001-stamp-provenance.mjs +77 -0
  10. package/lib/migrations/index.mjs +170 -0
  11. package/lib/restore.js +239 -0
  12. package/lib/store.js +125 -15
  13. package/lib/uninstall-project.js +6 -3
  14. package/lib/version-stamp.js +49 -0
  15. package/package.json +39 -10
  16. package/vendor/skills/archgen/SKILL.md +218 -145
  17. package/vendor/skills/archgen/archgen.config.json +16 -0
  18. package/vendor/skills/archgen/assets/skill-registry.json +4 -3
  19. package/vendor/skills/archgen/references/artifact-templates.md +308 -7
  20. package/vendor/skills/archgen/references/interview.md +226 -17
  21. package/vendor/skills/archgen/references/mcp-and-skills.md +88 -17
  22. package/vendor/skills/archgen/references/orchestration.md +230 -44
  23. package/vendor/skills/archgen/references/platforms.md +56 -14
  24. package/vendor/skills/archgen/scripts/doc-index.mjs +457 -0
  25. package/vendor/skills/archgen/scripts/impact.mjs +14 -1
  26. package/vendor/skills/archgen/scripts/lib/graph.mjs +99 -5
  27. package/vendor/skills/archgen/scripts/lib/scope.mjs +65 -0
  28. package/vendor/skills/archgen/scripts/lib/yaml.mjs +19 -3
  29. package/vendor/skills/archgen/scripts/plan-graph.mjs +329 -0
  30. package/vendor/skills/archgen/scripts/set-status.mjs +51 -9
  31. package/vendor/skills/archgen/scripts/update-agents.mjs +16 -5
  32. package/vendor/skills/archgen/scripts/validate.mjs +18 -1
  33. 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
- export const MANIFEST_REL = '.archgen/.install-manifest.json';
34
- export const BACKUP_ROOT_REL = '.archgen/.backup';
35
- export const VERSION_FILE = '.archgen-version';
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 (e.g. our version stamp)
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.includes(r)) continue;
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 project-relative path into `.archgen/.backup/<ts>/<rel>`.
103
- * Preserves the relative layout so backups restore unambiguously.
104
- * @returns {string} backup location relative to the project root
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 moveToBackup(root, relPath) {
107
- const ts = new Date().toISOString().replace(/:/g, '-');
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 */ }
@@ -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
- const mine = st.isDirectory() && !st.isSymbolicLink()
73
- ? hashDir(storeAbs, { ignore: [VERSION_FILE] })
74
- : null;
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",
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": { "archgen-skill": "bin/archgen.mjs" },
8
- "files": ["bin/", "lib/", "vendor/", "README.md"],
9
- "engines": { "node": ">=18" },
10
- "keywords": ["ai", "agents", "claude", "codex", "cursor", "opencode", "gemini", "architecture", "skills", "orchestration", "task-graph", "planning"],
11
- "repository": { "type": "git", "url": "git+https://github.com/akashmaurya99/archgen.git" },
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": { "url": "https://github.com/akashmaurya99/archgen/issues" },
14
- "//": "vendor/skills/archgen is synced from ../skills/archgen by scripts/sync-vendor.mjs (run automatically via prepublishOnly). The extension ships separately as a VS Code vsix.",
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-vendor.mjs",
17
- "prepublishOnly": "node scripts/sync-vendor.mjs",
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
  }