archgen-skill 0.0.1 → 0.0.3

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.
@@ -34,7 +34,8 @@ Do NOT load for plain bug fixes or one-line edits that need no planning.
34
34
  1. **Everything you generate goes inside `.archgen/<project-slug>/`** at the
35
35
  target repo root. NEVER write generated artifacts anywhere else. The only
36
36
  files you may touch outside `.archgen/` are source-code edits performed by
37
- worker sub-agents for approved tasks.
37
+ worker sub-agents for approved tasks and the `AGENTS.md` features registry,
38
+ maintained exclusively via `scripts/update-agents.mjs`.
38
39
  2. **Two gates before execution**: (a) the VERIFIER sub-agent must return
39
40
  APPROVE on plan+tasks, then (b) the USER must approve. Never skip either.
40
41
  3. **Right-size every generation.** Classify scope BEFORE creating files:
@@ -98,9 +99,13 @@ Pick ONE mode from the user's intent and follow its section below:
98
99
  Use templates from references/artifact-templates.md. Every task needs:
99
100
  id, title, depends_on (prerequisite ids), file_ownership (disjoint globs!),
100
101
  acceptance criteria, and status left unset (resolver derives readiness).
101
- 4. **Self-check:** run
102
+ If `<slug>` already exists under `.archgen/`, pick `<slug>-2`, `<slug>-3`, …
103
+ — NEVER overwrite or merge into an existing feature folder.
104
+ 4. **Self-check + registry:** run
102
105
  `node <skill>/scripts/validate.mjs .archgen/<slug>/tasks.yaml`
103
- Fix until exit 0.
106
+ Fix until exit 0. Then refresh the AGENTS.md features registry:
107
+ `node <skill>/scripts/update-agents.mjs <repo-root> --slug <slug> --status planned`.
108
+ Re-run it after ANY later artifact update so AGENTS.md never goes stale.
104
109
  5. **VERIFIER GATE.** Dispatch a verifier sub-agent (brief in
105
110
  references/orchestration.md §verifier). It must also run
106
111
  `node <skill>/scripts/verify-plan.mjs .archgen/<slug>/tasks.yaml --plan .archgen/<slug>/plans`
@@ -145,6 +150,8 @@ Run from the target repo root where `.archgen/<slug>/` exists.
145
150
  `set-status.mjs <id> done` on success (or `failed` on failure) and commit.
146
151
  4. After the wave settles: re-run next-tasks. Failed tasks are NOT retried
147
152
  automatically — surface them, propose a fix task, [wait for user].
153
+ Refresh the registry so AGENTS.md tracks the new statuses:
154
+ `node <skill>/scripts/update-agents.mjs <repo-root>`.
148
155
  5. Repeat until no actionable tasks remain.
149
156
  6. Final report: tasks done/failed/blocked, commits produced, suggested
150
157
  follow-ups. Update `.archgen/<slug>/plans/*.md` checkboxes if present.
@@ -197,6 +204,8 @@ When a task domain matches an entry in `assets/skill-registry.json`
197
204
  ## Things you must NOT do
198
205
 
199
206
  - Do NOT write artifacts outside `.archgen/<slug>/`.
207
+ - Do NOT reuse or overwrite an existing `.archgen/<slug>/` folder — suffix
208
+ `-2`, `-3`, … instead (multi-feature repos must stay isolated).
200
209
  - Do NOT skip the verifier gate or the user gate — ever, even for SMALL scopes.
201
210
  - Do NOT implement features yourself; dispatch workers.
202
211
  - Do NOT put two same-wave tasks on overlapping file_ownership globs.
@@ -225,6 +234,8 @@ When a task domain matches an entry in `assets/skill-registry.json`
225
234
  scripts/next-tasks.mjs <tasks.yaml> # waves JSON
226
235
  scripts/validate.mjs <tasks.yaml> [--plan <dir>] # exit 0/1
227
236
  scripts/set-status.mjs <tasks.yaml> <id> <status> [--force]
237
+ scripts/update-agents.mjs <projectRoot> [--slug <s>] [--status <s>] [--prune]
238
+ # AGENTS.md features registry
228
239
  scripts/impact.mjs <tasks.yaml> <id-or-artifactPath> # ripple JSON
229
240
  scripts/verify-plan.mjs <tasks.yaml> --plan <dir> # APPROVE|ISSUES
230
241
  ```
@@ -23,6 +23,23 @@ confirmed. The orchestrator loads ONLY the row matching Step-0 detection.
23
23
  Universal fallback: `.agents/skills/` project + `~/.agents/skills/` user works
24
24
  across most harnesses. Primary distribution: `npx skills add <repo>` (75+ agents).
25
25
 
26
+ ## archgen installed layout
27
+
28
+ `install.sh` / `npx archgen-skill install` maintain ONE canonical store plus
29
+ thin bridges — the skill is never copied twice:
30
+
31
+ | Path | Role |
32
+ |---|---|
33
+ | `.agents/skills/archgen/` | Canonical store — `SKILL.md`, `scripts/`, `references/`, `assets/` |
34
+ | `.claude/skills/archgen` | Optional symlink → canonical store, for Claude Code discovery |
35
+ | `CLAUDE.md` | One-line `@AGENTS.md` bridge only — no content of its own |
36
+ | `AGENTS.md` | The hub every harness reads; hosts the features registry between `<!-- archgen:features:start -->` and `<!-- archgen:features:end -->` |
37
+
38
+ The registry table (`| Feature | Status | Updated |`, one row per
39
+ `.archgen/<slug>/`) is machine-maintained by `scripts/update-agents.mjs` after
40
+ every generate/plan/wave run — never hand-edit it; edit `.archgen/` state and
41
+ re-run the script instead.
42
+
26
43
  ## MCP configuration
27
44
 
28
45
  Four different top-level keys exist — NEVER assume `mcpServers`:
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env node
2
+ // update-agents.mjs — keeps the AGENTS.md features registry in sync with .archgen/.
3
+ // Usage: node update-agents.mjs <projectRoot> [--slug <s>] [--status <s>] [--prune]
4
+ // Scans .archgen/*/tasks.yaml and rewrites the table between the
5
+ // <!-- archgen:features:start --> / <!-- archgen:features:end --> markers in
6
+ // <projectRoot>/AGENTS.md, preserving every byte outside the block (BOM and
7
+ // CRLF line endings included). A missing AGENTS.md gets a minimal scaffold; an
8
+ // existing file without markers gets them appended. Rows whose feature folder
9
+ // disappeared are pruned by default (--prune is accepted as the explicit
10
+ // no-op spelling of the same behavior). Writes atomically via temp + rename.
11
+ // Exit codes: 0 ok · 2 no .archgen/ dir · 4 usage/IO/marker errors.
12
+ import { existsSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from 'node:fs';
13
+ import { join } from 'node:path';
14
+ import { parseYaml } from './lib/yaml.mjs';
15
+
16
+ const START_MARKER = '<!-- archgen:features:start -->';
17
+ const END_MARKER = '<!-- archgen:features:end -->';
18
+ const HEADER = '| Feature | Status | Updated |';
19
+ const SEPARATOR = '| --- | --- | --- |';
20
+ const DISPLAY_STATUSES = ['planned', 'in progress', 'done', 'blocked', 'unknown'];
21
+ // Task-level statuses set-status.mjs may write; anything else in tasks.yaml
22
+ // means the file is not a contract we can read -> row falls back to unknown.
23
+ const TASK_STATUSES = new Set(['pending', 'ready', 'running', 'blocked', 'done', 'failed']);
24
+
25
+ // ---- arguments ------------------------------------------------------------
26
+ const [projectRoot, ...flags] = process.argv.slice(2);
27
+ let slug = null;
28
+ let status = null;
29
+ let pruneFlag = false; // pruning is always on; the flag just makes it explicit
30
+ for (let i = 0; i < flags.length; i++) {
31
+ const arg = flags[i];
32
+ if (arg === '--slug' || arg === '--status') {
33
+ const value = flags[++i];
34
+ if (value === undefined) { console.error(`${arg} requires a value`); process.exit(4); }
35
+ if (arg === '--slug') slug = value;
36
+ else status = value;
37
+ } else if (arg === '--prune') {
38
+ pruneFlag = true;
39
+ } else {
40
+ console.error(`unknown argument '${arg}'`);
41
+ process.exit(4);
42
+ }
43
+ }
44
+ if (!projectRoot) {
45
+ console.error('usage: update-agents.mjs <projectRoot> [--slug <s>] [--status <s>] [--prune]');
46
+ process.exit(4);
47
+ }
48
+ if ((slug === null) !== (status === null)) {
49
+ console.error('--slug and --status must be passed together');
50
+ process.exit(4);
51
+ }
52
+ if (status !== null && !DISPLAY_STATUSES.includes(status)) {
53
+ console.error(`invalid status '${status}' (allowed: ${DISPLAY_STATUSES.join('|')})`);
54
+ process.exit(4);
55
+ }
56
+ if (slug !== null && !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(slug)) {
57
+ console.error(`invalid slug '${slug}' (start alphanumeric; then letters, digits, '.', '_', '-')`);
58
+ process.exit(4);
59
+ }
60
+
61
+ // ---- scan .archgen/*/tasks.yaml -------------------------------------------
62
+ const archgenDir = join(projectRoot, '.archgen');
63
+ if (!existsSync(archgenDir)) {
64
+ console.error(`no .archgen/ directory under ${projectRoot} — nothing to register (generate a plan first)`);
65
+ process.exit(2);
66
+ }
67
+
68
+ /** Date -> YYYY-MM-DD (local time, zero-padded). */
69
+ function fmtDate(d) {
70
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
71
+ const dd = String(d.getDate()).padStart(2, '0');
72
+ return `${d.getFullYear()}-${mm}-${dd}`;
73
+ }
74
+
75
+ /**
76
+ * Derive the one-word registry status from a parsed tasks.yaml `tasks`
77
+ * sequence. Precedence: unreadable -> unknown; any blocked -> blocked; all
78
+ * done -> done; all pending/ready -> planned; everything else (any running,
79
+ * or a done/pending/failed mix) -> in progress.
80
+ */
81
+ function deriveStatus(tasks) {
82
+ if (!Array.isArray(tasks)) return 'unknown';
83
+ const statuses = [];
84
+ for (const t of tasks) {
85
+ if (t === null || typeof t !== 'object' || Array.isArray(t)) return 'unknown';
86
+ const s = String(t.status ?? 'pending').trim().toLowerCase(); // SKILL.md leaves status unset until wave resolution
87
+ if (!TASK_STATUSES.has(s)) return 'unknown';
88
+ statuses.push(s);
89
+ }
90
+ if (statuses.length === 0) return 'planned'; // scaffolded folder, no entries yet
91
+ if (statuses.includes('blocked')) return 'blocked';
92
+ if (statuses.every((s) => s === 'done')) return 'done';
93
+ if (statuses.every((s) => s === 'pending' || s === 'ready')) return 'planned';
94
+ return 'in progress';
95
+ }
96
+
97
+ /** @type {Map<string, {feature: string, status: string, updated: string}>} */
98
+ const rows = new Map();
99
+ for (const ent of readdirSync(archgenDir, { withFileTypes: true })) {
100
+ if (!ent.isDirectory()) continue;
101
+ const tasksPath = join(archgenDir, ent.name, 'tasks.yaml');
102
+ if (!existsSync(tasksPath)) continue; // the scan target is exactly */tasks.yaml
103
+ let derived = 'unknown';
104
+ try {
105
+ const { data } = parseYaml(readFileSync(tasksPath, 'utf8'), { filename: tasksPath });
106
+ derived = deriveStatus(data?.tasks);
107
+ } catch {
108
+ derived = 'unknown'; // unparseable YAML is data about the feature, not a crash
109
+ }
110
+ rows.set(ent.name, { feature: ent.name, status: derived, updated: fmtDate(statSync(tasksPath).mtime) });
111
+ }
112
+
113
+ // ---- upsert override (--slug/--status, run right after generating) --------
114
+ if (slug !== null) {
115
+ const existing = rows.get(slug);
116
+ if (existing) existing.status = status;
117
+ else rows.set(slug, { feature: slug, status, updated: fmtDate(new Date()) });
118
+ }
119
+
120
+ const ordered = [...rows.values()].sort((a, b) => (a.feature < b.feature ? -1 : a.feature > b.feature ? 1 : 0));
121
+
122
+ // ---- render -----------------------------------------------------------------
123
+ function renderTable(eol) {
124
+ const lines = [HEADER, SEPARATOR, ...ordered.map((r) => `| ${r.feature} | ${r.status} | ${r.updated} |`)];
125
+ return lines.join(eol) + eol;
126
+ }
127
+
128
+ /** Parse the CURRENT registry rows out of an existing AGENTS.md (or null). */
129
+ function oldRowsFrom(raw) {
130
+ const si = raw.indexOf(START_MARKER);
131
+ if (si === -1) return null;
132
+ const ei = raw.indexOf(END_MARKER, si + START_MARKER.length);
133
+ if (ei === -1) return null;
134
+ const old = new Map();
135
+ for (const line of raw.slice(si + START_MARKER.length, ei).split(/\r?\n/)) {
136
+ const t = line.trim();
137
+ if (!t.startsWith('|') || t === HEADER || t === SEPARATOR) continue;
138
+ const cells = t.split('|').slice(1, -1).map((c) => c.trim());
139
+ if (!cells[0]) continue;
140
+ old.set(cells[0], { status: cells[1] ?? '', updated: cells[2] ?? '' });
141
+ }
142
+ return old;
143
+ }
144
+
145
+ const agentsPath = join(projectRoot, 'AGENTS.md');
146
+ let raw = null;
147
+ try { raw = readFileSync(agentsPath, 'utf8'); } catch { /* missing -> scaffold below */ }
148
+ const oldRows = raw === null ? new Map() : (oldRowsFrom(raw) ?? new Map());
149
+
150
+ let out;
151
+ let banner;
152
+ if (raw === null) {
153
+ // Missing AGENTS.md: minimal scaffold — H1, pointer paragraph, markers+table.
154
+ banner = `created ${agentsPath}`;
155
+ out = '# Agent Guide\n'
156
+ + '\n'
157
+ + 'This project uses the archgen skill at `.agents/skills/archgen/SKILL.md` for architecture generation and autonomous task execution.\n'
158
+ + '\n'
159
+ + START_MARKER + '\n'
160
+ + renderTable('\n')
161
+ + END_MARKER + '\n';
162
+ } else if (!raw.includes(START_MARKER)) {
163
+ // Existing user file: append newline + markers + table; prior bytes untouched.
164
+ banner = `appended features registry to ${agentsPath}`;
165
+ const eol = raw.includes('\r\n') ? '\r\n' : '\n';
166
+ out = raw + eol + START_MARKER + eol + renderTable(eol) + END_MARKER + '\n';
167
+ } else {
168
+ // Markers present: replace ONLY the span between them; every outside byte
169
+ // (BOM, prose, trailing sections, CRLF endings) survives verbatim.
170
+ const si = raw.indexOf(START_MARKER);
171
+ const ei = raw.indexOf(END_MARKER, si + START_MARKER.length);
172
+ if (ei === -1) {
173
+ console.error(`${agentsPath}: found ${START_MARKER} but no ${END_MARKER} — repair the markers by hand`);
174
+ process.exit(4);
175
+ }
176
+ banner = `updated features registry in ${agentsPath}`;
177
+ const eol = raw.includes('\r\n') ? '\r\n' : '\n';
178
+ out = raw.slice(0, si + START_MARKER.length) + eol + renderTable(eol) + raw.slice(ei);
179
+ }
180
+
181
+ // ---- human summary ----------------------------------------------------------
182
+ const notes = [];
183
+ for (const feature of oldRows.keys()) {
184
+ if (!rows.has(feature)) notes.push(`pruned: ${feature} (missing .archgen/${feature}/tasks.yaml)`);
185
+ }
186
+ let unchanged = 0;
187
+ for (const r of ordered) {
188
+ const prev = oldRows.get(r.feature);
189
+ if (!prev) notes.push(`added: ${r.feature} (${r.status})`);
190
+ else if (prev.status !== r.status) notes.push(`updated: ${r.feature} (${prev.status || '?'} -> ${r.status})`);
191
+ else if (prev.updated !== r.updated) notes.push(`updated: ${r.feature} (${r.updated})`);
192
+ else unchanged++;
193
+ }
194
+
195
+ const tmp = `${agentsPath}.tmp-${process.pid}-${Date.now()}`;
196
+ writeFileSync(tmp, out);
197
+ renameSync(tmp, agentsPath); // atomic on POSIX: readers observe old-or-new, never partial
198
+
199
+ console.log(banner);
200
+ for (const n of notes) console.log(n);
201
+ if (unchanged) console.log(`unchanged: ${unchanged}`);
202
+ console.log(`registry: ${ordered.length} feature${ordered.length === 1 ? '' : 's'}`);