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.
- package/README.md +156 -0
- package/bin/archgen.mjs +124 -13
- package/lib/block.js +109 -5
- package/lib/doctor.js +186 -0
- package/lib/init.js +129 -27
- package/lib/store.js +146 -0
- package/lib/uninstall-project.js +92 -0
- package/lib/version.js +23 -0
- package/package.json +5 -3
- package/vendor/skills/archgen/SKILL.md +14 -3
- package/vendor/skills/archgen/references/platforms.md +17 -0
- package/vendor/skills/archgen/scripts/update-agents.mjs +202 -0
|
@@ -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
|
-
|
|
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'}`);
|