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.
Files changed (37) hide show
  1. package/README.md +39 -45
  2. package/bin/archgen.mjs +30 -6
  3. package/lib/block.js +55 -9
  4. package/lib/bootstrap.js +103 -0
  5. package/lib/doctor.js +67 -5
  6. package/lib/init.js +43 -4
  7. package/lib/install.js +40 -4
  8. package/lib/mcp-server.js +159 -0
  9. package/lib/migrations/001-stamp-provenance.mjs +3 -3
  10. package/lib/spawn-npm.js +54 -0
  11. package/lib/store.js +174 -9
  12. package/lib/uninstall-project.js +16 -3
  13. package/lib/version-stamp.js +3 -1
  14. package/lib/version.js +11 -3
  15. package/package.json +5 -4
  16. package/vendor/skills/archgen/SKILL.md +370 -315
  17. package/vendor/skills/archgen/archgen.config.json +1 -1
  18. package/vendor/skills/archgen/references/code-standards.md +2 -2
  19. package/vendor/skills/archgen/assets/mcp-template.json +0 -42
  20. package/vendor/skills/archgen/assets/skill-registry.json +0 -24
  21. package/vendor/skills/archgen/references/artifact-templates.md +0 -349
  22. package/vendor/skills/archgen/references/interview.md +0 -230
  23. package/vendor/skills/archgen/references/mcp-and-skills.md +0 -117
  24. package/vendor/skills/archgen/references/orchestration.md +0 -169
  25. package/vendor/skills/archgen/references/platforms.md +0 -83
  26. package/vendor/skills/archgen/scripts/doc-index.mjs +0 -457
  27. package/vendor/skills/archgen/scripts/impact.mjs +0 -77
  28. package/vendor/skills/archgen/scripts/lib/graph.mjs +0 -207
  29. package/vendor/skills/archgen/scripts/lib/plan-coverage.mjs +0 -32
  30. package/vendor/skills/archgen/scripts/lib/scope.mjs +0 -65
  31. package/vendor/skills/archgen/scripts/lib/yaml.mjs +0 -492
  32. package/vendor/skills/archgen/scripts/next-tasks.mjs +0 -55
  33. package/vendor/skills/archgen/scripts/plan-graph.mjs +0 -329
  34. package/vendor/skills/archgen/scripts/set-status.mjs +0 -60
  35. package/vendor/skills/archgen/scripts/update-agents.mjs +0 -204
  36. package/vendor/skills/archgen/scripts/validate.mjs +0 -87
  37. package/vendor/skills/archgen/scripts/verify-plan.mjs +0 -70
@@ -1,207 +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
- /** Build adjacency (prerequisite → dependents) + reverse maps from tasks.
6
- * Normative semantics: depends_on lists PREREQUISITE ids; a task is ready iff
7
- * every prerequisite is done; wave 1 = empty depends_on. */
8
- export function buildGraph(tasks) {
9
- /** @type {Map<string, string[]>} prereq -> [dependent ids] */
10
- const dependents = new Map();
11
- /** @type {Map<string, string[]>} id -> prerequisite ids */
12
- const prerequisites = new Map();
13
- const byId = new Map();
14
- for (const t of tasks) {
15
- if (byId.has(t.id)) throw new GraphError(`duplicate task id: ${t.id}`);
16
- byId.set(t.id, t);
17
- prerequisites.set(t.id, Array.isArray(t.depends_on) ? t.depends_on : []);
18
- }
19
- for (const t of tasks) {
20
- for (const dep of prerequisites.get(t.id)) {
21
- if (!byId.has(dep)) throw new GraphError(`dangling depends_on reference: '${t.id}' depends on unknown task '${dep}'`);
22
- if (!dependents.has(dep)) dependents.set(dep, []);
23
- dependents.get(dep).push(t.id);
24
- }
25
- }
26
- return { byId, prerequisites, dependents };
27
- }
28
-
29
- export class GraphError extends Error {}
30
-
31
- /** Return an array of ids forming a cycle (empty if acyclic). Iterative DFS. */
32
- export function findCycle(prerequisites) {
33
- const WHITE = 0, GRAY = 1, BLACK = 2;
34
- const color = new Map();
35
- const parent = new Map();
36
- for (const id of prerequisites.keys()) color.set(id, WHITE);
37
-
38
- for (const start of prerequisites.keys()) {
39
- if (color.get(start) !== WHITE) continue;
40
- const stack = [[start, 0]];
41
- color.set(start, GRAY);
42
- while (stack.length) {
43
- const [id, pi] = stack[stack.length - 1];
44
- const deps = prerequisites.get(id);
45
- if (pi < deps.length) {
46
- stack[stack.length - 1] = [id, pi + 1];
47
- const d = deps[pi];
48
- if (!color.has(d)) continue; // dangling handled elsewhere
49
- if (color.get(d) === GRAY) {
50
- // Found a back-edge: reconstruct the cycle path d ← id ← … ← d
51
- const cycle = [d];
52
- let cur = id;
53
- while (cur !== d) { cycle.push(cur); cur = parent.get(cur); }
54
- cycle.push(d);
55
- return cycle.reverse();
56
- }
57
- if (color.get(d) === WHITE) { color.set(d, GRAY); parent.set(d, id); stack.push([d, 0]); }
58
- } else {
59
- color.set(id, BLACK);
60
- stack.pop();
61
- }
62
- }
63
- }
64
- return [];
65
- }
66
-
67
- /** Topological waves over NOT-DONE tasks.
68
- * @param {Map<string,any>} byId
69
- * @param {Map<string,string[]>} prerequisites
70
- * @returns {{waves: Array<Array<any>>, blockedByFailure: string[]}}
71
- * Wave membership: every prerequisite is done. Tasks whose TRANSITIVE
72
- * prerequisites include any failed task are excluded entirely and reported in
73
- * blockedByFailure (no silent cascades — orchestrator re-plans around them). */
74
- export function computeWaves(byId, prerequisites) {
75
- const done = new Set();
76
- const failed = new Set();
77
- for (const [id, t] of byId) {
78
- if (t.status === 'done') done.add(id);
79
- if (t.status === 'failed') failed.add(id);
80
- }
81
- // Propagate failure transitively.
82
- const blockedByFailure = new Set();
83
- let changed = true;
84
- while (changed) {
85
- changed = false;
86
- for (const [id, deps] of prerequisites) {
87
- if (blockedByFailure.has(id) || failed.has(id)) continue;
88
- if (deps.some((d) => failed.has(d) || blockedByFailure.has(d))) {
89
- blockedByFailure.add(id); changed = true;
90
- }
91
- }
92
- }
93
-
94
- const remaining = [...byId.keys()].filter((id) =>
95
- !done.has(id) && !failed.has(id) && !blockedByFailure.has(id));
96
- const waves = [];
97
- const satisfied = new Set(done);
98
- let frontier = remaining.filter((id) => prerequisites.get(id).every((d) => satisfied.has(d)));
99
- const queued = new Set();
100
-
101
- while (frontier.length) {
102
- frontier.sort(); // deterministic output for identical inputs
103
- waves.push(frontier.map((id) => byId.get(id)));
104
- for (const id of frontier) { satisfied.add(id); queued.add(id); }
105
- const next = [];
106
- for (const id of remaining) {
107
- if (queued.has(id) || satisfied.has(id)) continue;
108
- if (prerequisites.get(id).every((d) => satisfied.has(d))) next.push(id);
109
- }
110
- frontier = next;
111
- }
112
- return { waves, blockedByFailure: [...blockedByFailure].sort() };
113
- }
114
-
115
- /** First ownership conflict inside a single wave, or null.
116
- * Two rules, both conservative:
117
- * 1. Literal equality — two tasks must never claim the SAME glob string.
118
- * 2. Glob intersection — when a '**' is involved, patterns whose non-glob
119
- * prefix segments overlap compatibly are flagged (e.g. "src/**" vs a
120
- * TypeScript-scoped variant of it). When segment comparison is uncertain
121
- * (wildcards), the pair IS flagged: false positives merely force a rename;
122
- * false negatives let parallel workers corrupt each other's files.
123
- */
124
- export function findOwnershipConflict(wave) {
125
- const seen = new Map();
126
- const claimed = [];
127
- for (const t of wave) {
128
- for (const g of t.file_ownership ?? []) {
129
- if (seen.has(g)) return { glob: g, a: seen.get(g), b: t.id };
130
- for (const prev of claimed) {
131
- if (globsMayOverlap(prev.glob, g)) return { glob: g, a: prev.id, b: t.id };
132
- }
133
- seen.set(g, t.id);
134
- claimed.push({ glob: g, id: t.id });
135
- }
136
- }
137
- return null;
138
- }
139
-
140
- /** Conservative may-intersect test for two ownership globs. */
141
- function globsMayOverlap(a, b) {
142
- if (a === b) return true;
143
- // Deepen the check only when a recursive '**' is involved; plain literal
144
- // globs keep the cheap equality rule above.
145
- if (!a.includes('**') && !b.includes('**')) return false;
146
- const as = a.split('/');
147
- const bs = b.split('/');
148
- const n = Math.min(as.length, bs.length);
149
- for (let i = 0; i < n; i++) {
150
- const x = as[i];
151
- const y = bs[i];
152
- if (x === y) continue;
153
- if (x === '**' || y === '**') return true; // '**' swallows any divergence below it
154
- if (x.includes('*') || y.includes('*') || x.includes('?') || y.includes('?')) return true; // uncertain -> flag
155
- return false; // both segments literal and different: paths diverge here
156
- }
157
- return true; // one pattern is a segment-prefix of the other
158
- }
159
-
160
- // --- additive (hardening pass): dedup normalization + quality surfacing -----
161
-
162
- /** Collapse redundant depends_on entries BEFORE graph construction so no
163
- * downstream consumer can ever see the same edge twice (idempotency: identical
164
- * input ⇒ byte-identical output with zero duplicated entities).
165
- * First occurrence wins; order otherwise preserved. Tasks without an array
166
- * depends_on pass through untouched.
167
- * @param {Array<any>} tasks
168
- * @returns {{tasks: Array<any>, duplicatesCollapsed: number}} */
169
- export function dedupeDependencies(tasks) {
170
- let duplicatesCollapsed = 0;
171
- const normalized = tasks.map((t) => {
172
- if (!Array.isArray(t.depends_on)) return t;
173
- const seen = new Set();
174
- const deps = [];
175
- for (const d of t.depends_on) {
176
- if (seen.has(d)) { duplicatesCollapsed++; continue; }
177
- seen.add(d);
178
- deps.push(d);
179
- }
180
- return deps.length === t.depends_on.length ? t : { ...t, depends_on: deps };
181
- });
182
- return { tasks: normalized, duplicatesCollapsed };
183
- }
184
-
185
- /** Data-quality facts computed from parsed data WITHOUT judging them — the
186
- * verifier owns pass/fail; indexers only surface facts deterministically.
187
- * - selfDeps: tasks whose depends_on includes their own id (note: such a task
188
- * is also a 1-node cycle, so the CLI cycle gate normally exits 2 first;
189
- * the field keeps the shape uniform for lib-level consumers).
190
- * - emptyOwnership: missing or empty file_ownership (parallel workers would
191
- * have no disjointness contract).
192
- * - blankAcceptance: missing or empty acceptance (no objective done-criteria).
193
- * @param {Array<any>} tasks
194
- * @returns {{selfDeps: number, emptyOwnership: number, blankAcceptance: number}} */
195
- export function computeQualityStats(tasks) {
196
- let selfDeps = 0;
197
- let emptyOwnership = 0;
198
- let blankAcceptance = 0;
199
- for (const t of tasks) {
200
- if (Array.isArray(t.depends_on) && t.depends_on.includes(t.id)) selfDeps++;
201
- const own = t.file_ownership;
202
- if (!Array.isArray(own) || own.length === 0) emptyOwnership++;
203
- const acc = t.acceptance;
204
- if (!Array.isArray(acc) || acc.length === 0) blankAcceptance++;
205
- }
206
- return { selfDeps, emptyOwnership, blankAcceptance };
207
- }
@@ -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
- }