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
@@ -1,77 +0,0 @@
1
- #!/usr/bin/env node
2
- // impact.mjs — reverse-dependency query: what is affected if <target> changes?
3
- // Usage: node impact.mjs <tasks.yaml> <taskId-or-artifactPath>
4
- // stdout: JSON {direct:[taskIds], transitive:[taskIds], artifacts:[paths]}
5
- import { readFileSync } from 'node:fs';
6
- import { parseYaml } from './lib/yaml.mjs';
7
-
8
- const [, , file, target] = process.argv;
9
- if (!file || !target) { console.error('usage: impact.mjs <tasks.yaml> <taskId-or-artifactPath>'); process.exit(4); }
10
-
11
- let raw;
12
- try {
13
- raw = readFileSync(file, 'utf8');
14
- } catch {
15
- console.error(`cannot read ${file} (missing or unreadable)`);
16
- process.exit(4);
17
- }
18
- let data;
19
- try {
20
- ({ data } = parseYaml(raw, { filename: file }));
21
- } catch (e) {
22
- console.error(`${file}: unparseable YAML (${String(e?.message ?? e).replace(/^[^:]*:\d+:\s*/, '')})`);
23
- process.exit(4);
24
- }
25
- const tasks = data?.tasks;
26
- if (!Array.isArray(tasks)) { console.error(`${file}: missing 'tasks:' sequence`); process.exit(4); }
27
-
28
- // Resolve target to a seed set of task ids.
29
- const byId = new Map(tasks.map((t) => [t.id, t]));
30
- let seeds;
31
- if (byId.has(target)) {
32
- seeds = [target];
33
- } else {
34
- // Artifact-path mode: any task whose artifacts[] or file_ownership[] covers it.
35
- seeds = tasks
36
- .filter((t) => (t.artifacts ?? []).includes(target) || (t.file_ownership ?? []).includes(target))
37
- .map((t) => t.id);
38
- if (seeds.length === 0) { console.error(`no task claims '${target}' (not an id, artifact, or ownership glob)`); process.exit(4); }
39
- }
40
-
41
- // Reverse edges: dependents[prereq] = tasks that depend on it.
42
- const dependents = new Map();
43
- for (const t of tasks) {
44
- for (const dep of t.depends_on ?? []) {
45
- if (!dependents.has(dep)) dependents.set(dep, new Set());
46
- dependents.get(dep).add(t.id);
47
- }
48
- }
49
-
50
- // Direct = immediate dependents of seeds; transitive = everything beyond.
51
- const direct = new Set();
52
- const transitive = new Set();
53
- for (const s of seeds) for (const d of dependents.get(s) ?? []) direct.add(d);
54
- transitive.clear();
55
- {
56
- const seen = new Set([...seeds, ...direct]);
57
- const f = [...direct];
58
- while (f.length) {
59
- const cur = f.pop();
60
- for (const d of dependents.get(cur) ?? []) {
61
- if (seen.has(d)) continue;
62
- seen.add(d); transitive.add(d); f.push(d);
63
- }
64
- }
65
- }
66
-
67
- // Artifacts owned by affected tasks (what files the ripple touches).
68
- const artifacts = new Set();
69
- for (const id of [...direct, ...transitive]) {
70
- for (const a of byId.get(id)?.artifacts ?? []) artifacts.add(a);
71
- }
72
-
73
- console.log(JSON.stringify({
74
- direct: [...direct].sort(),
75
- transitive: [...transitive].sort(),
76
- artifacts: [...artifacts].sort(),
77
- }, null, 2));
@@ -1,222 +0,0 @@
1
- // graph.mjs — shared dependency-graph utilities for archgen scripts.
2
- // WHY a lib: validate.mjs, next-tasks.mjs and verify-plan.mjs must agree on
3
- // cycle detection and wave semantics; three copies would drift (Metis finding).
4
-
5
- /** True iff `t` is a mapping object (not null, not an array, not a scalar). */
6
- function isTaskMapping(t) {
7
- return t !== null && typeof t === 'object' && !Array.isArray(t);
8
- }
9
-
10
- /** Build adjacency (prerequisite → dependents) + reverse maps from tasks.
11
- * Normative semantics: depends_on lists PREREQUISITE ids; a task is ready iff
12
- * every prerequisite is done; wave 1 = empty depends_on.
13
- * Entries must be mappings with a non-empty string id — anything else is a
14
- * GraphError naming the index, so consumers exit with a clean contract code
15
- * instead of an uncaught TypeError on malformed tasks.yaml shapes. */
16
- export function buildGraph(tasks) {
17
- /** @type {Map<string, string[]>} prereq -> [dependent ids] */
18
- const dependents = new Map();
19
- /** @type {Map<string, string[]>} id -> prerequisite ids */
20
- const prerequisites = new Map();
21
- const byId = new Map();
22
- for (const [i, t] of tasks.entries()) {
23
- if (!isTaskMapping(t)) throw new GraphError(`tasks[${i}]: not a mapping (task entries must be 'key: value' mappings)`);
24
- if (typeof t.id !== 'string' || t.id.length === 0) throw new GraphError(`tasks[${i}]: missing string 'id' (every task needs a non-empty string id)`);
25
- if (byId.has(t.id)) throw new GraphError(`duplicate task id: ${t.id}`);
26
- byId.set(t.id, t);
27
- prerequisites.set(t.id, Array.isArray(t.depends_on) ? t.depends_on : []);
28
- }
29
- for (const t of tasks) {
30
- for (const dep of prerequisites.get(t.id)) {
31
- if (!byId.has(dep)) throw new GraphError(`dangling depends_on reference: '${t.id}' depends on unknown task '${dep}'`);
32
- if (!dependents.has(dep)) dependents.set(dep, []);
33
- dependents.get(dep).push(t.id);
34
- }
35
- }
36
- return { byId, prerequisites, dependents };
37
- }
38
-
39
- export class GraphError extends Error {}
40
-
41
- /** Return an array of ids forming a cycle (empty if acyclic). Iterative DFS. */
42
- export function findCycle(prerequisites) {
43
- const WHITE = 0, GRAY = 1, BLACK = 2;
44
- const color = new Map();
45
- const parent = new Map();
46
- for (const id of prerequisites.keys()) color.set(id, WHITE);
47
-
48
- for (const start of prerequisites.keys()) {
49
- if (color.get(start) !== WHITE) continue;
50
- const stack = [[start, 0]];
51
- color.set(start, GRAY);
52
- while (stack.length) {
53
- const [id, pi] = stack[stack.length - 1];
54
- const deps = prerequisites.get(id);
55
- if (pi < deps.length) {
56
- stack[stack.length - 1] = [id, pi + 1];
57
- const d = deps[pi];
58
- if (!color.has(d)) continue; // dangling handled elsewhere
59
- if (color.get(d) === GRAY) {
60
- // Found a back-edge: reconstruct the cycle path d ← id ← … ← d
61
- const cycle = [d];
62
- let cur = id;
63
- while (cur !== d) { cycle.push(cur); cur = parent.get(cur); }
64
- cycle.push(d);
65
- return cycle.reverse();
66
- }
67
- if (color.get(d) === WHITE) { color.set(d, GRAY); parent.set(d, id); stack.push([d, 0]); }
68
- } else {
69
- color.set(id, BLACK);
70
- stack.pop();
71
- }
72
- }
73
- }
74
- return [];
75
- }
76
-
77
- /** Topological waves over NOT-DONE tasks.
78
- * @param {Map<string,any>} byId
79
- * @param {Map<string,string[]>} prerequisites
80
- * @returns {{waves: Array<Array<any>>, blockedByFailure: string[]}}
81
- * Wave membership: every prerequisite is done. Tasks whose TRANSITIVE
82
- * prerequisites include any failed task are excluded entirely and reported in
83
- * blockedByFailure (no silent cascades — orchestrator re-plans around them). */
84
- export function computeWaves(byId, prerequisites) {
85
- const done = new Set();
86
- const failed = new Set();
87
- for (const [id, t] of byId) {
88
- if (t.status === 'done') done.add(id);
89
- if (t.status === 'failed') failed.add(id);
90
- }
91
- // Propagate failure transitively.
92
- const blockedByFailure = new Set();
93
- let changed = true;
94
- while (changed) {
95
- changed = false;
96
- for (const [id, deps] of prerequisites) {
97
- if (blockedByFailure.has(id) || failed.has(id)) continue;
98
- if (deps.some((d) => failed.has(d) || blockedByFailure.has(d))) {
99
- blockedByFailure.add(id); changed = true;
100
- }
101
- }
102
- }
103
-
104
- const remaining = [...byId.keys()].filter((id) =>
105
- !done.has(id) && !failed.has(id) && !blockedByFailure.has(id));
106
- const waves = [];
107
- const satisfied = new Set(done);
108
- let frontier = remaining.filter((id) => prerequisites.get(id).every((d) => satisfied.has(d)));
109
- const queued = new Set();
110
-
111
- while (frontier.length) {
112
- frontier.sort(); // deterministic output for identical inputs
113
- waves.push(frontier.map((id) => byId.get(id)));
114
- for (const id of frontier) { satisfied.add(id); queued.add(id); }
115
- const next = [];
116
- for (const id of remaining) {
117
- if (queued.has(id) || satisfied.has(id)) continue;
118
- if (prerequisites.get(id).every((d) => satisfied.has(d))) next.push(id);
119
- }
120
- frontier = next;
121
- }
122
- return { waves, blockedByFailure: [...blockedByFailure].sort() };
123
- }
124
-
125
- /** First ownership conflict inside a single wave, or null.
126
- * Two rules, both conservative:
127
- * 1. Literal equality — two tasks must never claim the SAME glob string.
128
- * 2. Glob intersection — when a '**' is involved, patterns whose non-glob
129
- * prefix segments overlap compatibly are flagged (e.g. "src/**" vs a
130
- * TypeScript-scoped variant of it). When segment comparison is uncertain
131
- * (wildcards), the pair IS flagged: false positives merely force a rename;
132
- * false negatives let parallel workers corrupt each other's files.
133
- */
134
- export function findOwnershipConflict(wave) {
135
- const seen = new Map();
136
- const claimed = [];
137
- for (const t of wave) {
138
- for (const g of t.file_ownership ?? []) {
139
- if (seen.has(g)) return { glob: g, a: seen.get(g), b: t.id };
140
- for (const prev of claimed) {
141
- if (globsMayOverlap(prev.glob, g)) return { glob: g, a: prev.id, b: t.id };
142
- }
143
- seen.set(g, t.id);
144
- claimed.push({ glob: g, id: t.id });
145
- }
146
- }
147
- return null;
148
- }
149
-
150
- /** Conservative may-intersect test for two ownership globs. */
151
- function globsMayOverlap(a, b) {
152
- if (a === b) return true;
153
- // Deepen the check only when a recursive '**' is involved; plain literal
154
- // globs keep the cheap equality rule above.
155
- if (!a.includes('**') && !b.includes('**')) return false;
156
- const as = a.split('/');
157
- const bs = b.split('/');
158
- const n = Math.min(as.length, bs.length);
159
- for (let i = 0; i < n; i++) {
160
- const x = as[i];
161
- const y = bs[i];
162
- if (x === y) continue;
163
- if (x === '**' || y === '**') return true; // '**' swallows any divergence below it
164
- if (x.includes('*') || y.includes('*') || x.includes('?') || y.includes('?')) return true; // uncertain -> flag
165
- return false; // both segments literal and different: paths diverge here
166
- }
167
- return true; // one pattern is a segment-prefix of the other
168
- }
169
-
170
- // --- additive (hardening pass): dedup normalization + quality surfacing -----
171
-
172
- /** Collapse redundant depends_on entries BEFORE graph construction so no
173
- * downstream consumer can ever see the same edge twice (idempotency: identical
174
- * input ⇒ byte-identical output with zero duplicated entities).
175
- * First occurrence wins; order otherwise preserved. Tasks without an array
176
- * depends_on pass through untouched.
177
- * @param {Array<any>} tasks
178
- * @returns {{tasks: Array<any>, duplicatesCollapsed: number}} */
179
- export function dedupeDependencies(tasks) {
180
- let duplicatesCollapsed = 0;
181
- const normalized = tasks.map((t) => {
182
- // Malformed entries pass through untouched; buildGraph owns reporting them
183
- // (touching t.depends_on here would throw before the contract check runs).
184
- if (!isTaskMapping(t) || !Array.isArray(t.depends_on)) return t;
185
- const seen = new Set();
186
- const deps = [];
187
- for (const d of t.depends_on) {
188
- if (seen.has(d)) { duplicatesCollapsed++; continue; }
189
- seen.add(d);
190
- deps.push(d);
191
- }
192
- return deps.length === t.depends_on.length ? t : { ...t, depends_on: deps };
193
- });
194
- return { tasks: normalized, duplicatesCollapsed };
195
- }
196
-
197
- /** Data-quality facts computed from parsed data WITHOUT judging them — the
198
- * verifier owns pass/fail; indexers only surface facts deterministically.
199
- * - selfDeps: tasks whose depends_on includes their own id (note: such a task
200
- * is also a 1-node cycle, so the CLI cycle gate normally exits 2 first;
201
- * the field keeps the shape uniform for lib-level consumers).
202
- * - emptyOwnership: missing or empty file_ownership (parallel workers would
203
- * have no disjointness contract).
204
- * - blankAcceptance: missing or empty acceptance (no objective done-criteria).
205
- * @param {Array<any>} tasks
206
- * @returns {{selfDeps: number, emptyOwnership: number, blankAcceptance: number}} */
207
- export function computeQualityStats(tasks) {
208
- let selfDeps = 0;
209
- let emptyOwnership = 0;
210
- let blankAcceptance = 0;
211
- for (const t of tasks) {
212
- // Malformed entries are buildGraph's to report; counting them here would
213
- // throw on property access before any consumer sees a contract error.
214
- if (!isTaskMapping(t)) continue;
215
- if (Array.isArray(t.depends_on) && t.depends_on.includes(t.id)) selfDeps++;
216
- const own = t.file_ownership;
217
- if (!Array.isArray(own) || own.length === 0) emptyOwnership++;
218
- const acc = t.acceptance;
219
- if (!Array.isArray(acc) || acc.length === 0) blankAcceptance++;
220
- }
221
- return { selfDeps, emptyOwnership, blankAcceptance };
222
- }
@@ -1,32 +0,0 @@
1
- // plan-coverage.mjs — single shared implementation of plan<->tasks cross-checks.
2
- // WHY: validate.mjs --plan and verify-plan.mjs must apply IDENTICAL rules
3
- // (Metis: two implementations of one rule will drift).
4
-
5
- import { readdirSync, readFileSync, existsSync } from 'node:fs';
6
- import { join } from 'node:path';
7
-
8
- /** @returns {string[]} error findings (empty = coverage OK) */
9
- export function checkPlanCoverage(planDir, tasks) {
10
- const findings = [];
11
- if (!existsSync(planDir)) return [`plans dir not found: ${planDir}`];
12
- const files = readdirSync(planDir).filter((f) => f.endsWith('.md'));
13
- const planText = files.map((f) => readFileSync(join(planDir, f), 'utf8')).join('\n');
14
-
15
- const taskIds = new Set(tasks.map((t) => t.id));
16
- // Task ids referenced in plans: bare tokens that match an existing-or-not id pattern.
17
- const referenced = new Set();
18
- for (const t of taskIds) {
19
- const re = new RegExp(`\\b${escapeRe(t)}\\b`);
20
- if (re.test(planText)) referenced.add(t);
21
- }
22
- for (const t of tasks) {
23
- if (!referenced.has(t.id)) findings.push(`task '${t.id}' is not mentioned in any plan file under ${planDir}`);
24
- }
25
- // Inverse: plan mentions ids like TASK-xx not present in tasks.yaml.
26
- for (const m of planText.matchAll(/\b[A-Z][A-Z0-9]+-[0-9]+\b/g)) {
27
- if (!taskIds.has(m[0])) findings.push(`plan references unknown task id '${m[0]}' (not in tasks.yaml)`);
28
- }
29
- return findings;
30
- }
31
-
32
- function escapeRe(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
@@ -1,65 +0,0 @@
1
- // scope.mjs — the single entry gate for plan-graph.mjs and doc-index.mjs.
2
- //
3
- // WHY THIS EXISTS: these indexers read whatever tree they are pointed at. An
4
- // agent following injected instructions in a README, a prompt, or a hostile
5
- // tasks.yaml could otherwise aim them at arbitrary project source, dotfiles,
6
- // or home-directory content and have the tooling enumerate it. Pinning every
7
- // input to `.archgen/<slug>/` guarantees both tools only ever read archgen's
8
- // own artifact space — nothing else, no escape hatch.
9
- //
10
- // resolveSlugInput(rawInput) -> { kind: 'dir'|'tasks', root: <absolute slug dir>, slug: <dirname> }
11
- // Rules:
12
- // - fs.realpathSync the input first: symlinks are resolved to their target,
13
- // so `ln -s ~/secrets .archgen/x` style escapes are defeated (the real
14
- // path outside .archgen fails the segment check).
15
- // - The REAL path must contain a path segment exactly `.archgen` (segment
16
- // equality — `.archgenx/` does not count) such that the target is either
17
- // (a) a directory immediately under that `.archgen` segment (the slug
18
- // dir), or (b) a file named `tasks.yaml` directly inside such a slug dir.
19
- // - Everything else throws ScopeError. Missing/unreadable inputs surface as
20
- // the underlying realpath/stat error so callers keep their own messages.
21
- import { realpathSync, statSync } from 'node:fs';
22
- import { basename, dirname, sep } from 'node:path';
23
-
24
- export class ScopeError extends Error {
25
- constructor(input) {
26
- super(`'${input}' is outside .archgen scope — plan-graph/doc-index index only .archgen/<slug>/ plans`);
27
- this.name = 'ScopeError';
28
- }
29
- }
30
-
31
- /** Split an absolute real path into segments, separator-aware on all platforms
32
- * (win32 accepts both `\` and `/`; posix treats `\` as a normal filename char). */
33
- function segmentsOf(abs) {
34
- return process.platform === 'win32' ? abs.split(/[\\/]+/).filter(Boolean) : abs.split(sep).filter(Boolean);
35
- }
36
-
37
- export function resolveSlugInput(rawInput) {
38
- let real;
39
- try {
40
- real = realpathSync(rawInput); // follows symlinks: judge the TARGET, not the link
41
- } catch (e) {
42
- e.input = rawInput;
43
- throw e; // missing/unreadable: caller renders its own "cannot read" message
44
- }
45
- const st = statSync(real);
46
- const parts = segmentsOf(real);
47
-
48
- // Scan EVERY `.archgen` segment; accept when any of them makes the target an
49
- // immediate slug dir (a) or its direct tasks.yaml (b). Last-wins is fine in
50
- // practice but any-match is the honest reading of the rule for nested oddities.
51
- for (let i = 0; i < parts.length; i++) {
52
- if (parts[i] !== '.archgen') continue;
53
- const depthFromArchgen = parts.length - 1 - i; // segments after this `.archgen`
54
- if (st.isDirectory() && depthFromArchgen === 1) {
55
- // (a) `<...>/.archgen/<slug>` — the slug dir itself
56
- return { kind: 'dir', root: real, slug: parts[i + 1] };
57
- }
58
- if (st.isFile() && basename(real) === 'tasks.yaml' && depthFromArchgen === 2) {
59
- // (b) `<...>/.archgen/<slug>/tasks.yaml` — direct tasks.yaml mode
60
- const root = dirname(real);
61
- return { kind: 'tasks', root, slug: basename(root) };
62
- }
63
- }
64
- throw new ScopeError(rawInput);
65
- }