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 ADDED
@@ -0,0 +1,156 @@
1
+ <div align="center">
2
+
3
+ # archgen-skill
4
+
5
+ **Conversational architecture generation & autonomous task execution for coding agents.**
6
+
7
+ [![CI](https://github.com/akashmaurya99/archgen/actions/workflows/ci.yml/badge.svg)](https://github.com/akashmaurya99/archgen/actions/workflows/ci.yml)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/akashmaurya99/archgen/blob/main/LICENSE)
9
+ [![Node](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org)
10
+
11
+ Describe what you want to build. archgen interviews you (or surveys your existing
12
+ codebase), produces an architecture contract plus a dependency-ordered task graph,
13
+ verifies the plan, then executes it wave-by-wave through sub-agents.
14
+
15
+ Works as an agent skill for **Claude Code · OpenCode · Cursor · Codex · Gemini CLI ·
16
+ Antigravity** — and any [agentskills.io](https://agentskills.io)-compatible harness.
17
+
18
+ </div>
19
+
20
+ ---
21
+
22
+ ## Install
23
+
24
+ **Prepare a project** (recommended) — installs ONE canonical skill copy plus
25
+ harness bridges so every agent auto-discovers it:
26
+
27
+ ```bash
28
+ npx archgen-skill init
29
+ ```
30
+
31
+ What `init` writes (nothing is ever duplicated):
32
+
33
+ | Path | What |
34
+ | --- | --- |
35
+ | `.agents/skills/archgen/` | **The only real copy** — the agentskills.io standard location read natively by OpenCode, Cursor, Codex, Gemini CLI, Copilot and Antigravity |
36
+ | `.claude/skills/archgen → ../../.agents/skills/archgen` | Relative symlink for Claude Code (the one harness needing it). Skipped gracefully where symlinks need privileges — Claude still works via the bridge below |
37
+ | `AGENTS.md` | Managed hub block: skill pointer + quick triggers + a features registry that stays current as you add features |
38
+ | `CLAUDE.md` | One-line bridge: `@AGENTS.md` (existing files keep all their content; if it already imports AGENTS.md we write nothing) |
39
+
40
+ Existing projects with user content are safe: managed blocks upsert between
41
+ markers, modified skill copies are backed up to `.archgen/.backup/<timestamp>/`
42
+ before refresh, and legacy dual-copy layouts are migrated to symlinks
43
+ automatically. Verify anytime with `archgen-skill doctor`.
44
+
45
+ **Or install globally** into every detected harness:
46
+
47
+ ```bash
48
+ npx archgen-skill install # symlinks; --copy for real copies
49
+ ```
50
+
51
+ Prefer shell? Clone the repo and run `./install.sh` (same behavior, plus
52
+ `--init`, `--project`, `--uninstall`). Requires Node.js ≥ 18.
53
+ The skill itself has zero npm dependencies.
54
+
55
+ ## Quick start
56
+
57
+ After `npx archgen-skill init`, open your project in any supported agent and talk naturally:
58
+
59
+ | You say | What runs |
60
+ | --- | --- |
61
+ | *"generate architecture for a booking platform"* | Interview → artifacts → both gates |
62
+ | *"add rate-limiting to my API server"* | Survey existing code first, then plan + gates |
63
+ | *"start work"* | Autonomous wave execution until done or blocked |
64
+ | *"roll back the auth changes"* | Impact analysis → reverse-order revert plan → approval |
65
+
66
+ Everything archgen produces lives in one reviewable `.archgen/<slug>/` folder —
67
+ architecture contract, docs, ADRs, plans, and a `tasks.yaml` task graph — versioned by git.
68
+
69
+ ## Commands
70
+
71
+ | Command | What it does |
72
+ | --- | --- |
73
+ | `npx archgen-skill init [dir]` | Install the single canonical skill store at `.agents/skills/archgen`, add the Claude symlink adapter, write the AGENTS.md hub + CLAUDE.md `@AGENTS.md` bridge |
74
+ | `npx archgen-skill install [--copy]` | Install into global harness skill dirs (symlinks by default, real copies with `--copy`) |
75
+ | `npx archgen-skill uninstall` | Remove globally-installed copies (manifest-recorded, safe removal) |
76
+ | `npx archgen-skill uninstall --project [dir]` | Remove project install: strips managed blocks, our symlink and the store **only if unmodified** — feature folders in `.archgen/` and all user content are preserved |
77
+ | `npx archgen-skill doctor [dir] [--check]` | Verify + auto-repair an installation: store integrity, version stamp, Claude link, blocks present exactly once, manifest resolution (`--check` reports without fixing) |
78
+ | `npx archgen-skill --help` | Help |
79
+
80
+ ## Why archgen
81
+
82
+ AI coding agents fail at scale for predictable reasons: plans with hidden cycles,
83
+ workers editing the same files, tasks without objective acceptance criteria, and no
84
+ human checkpoint before execution. archgen makes each of these a *structural*
85
+ guarantee rather than a hope:
86
+
87
+ | Guarantee | Mechanism |
88
+ | --- | --- |
89
+ | Plans are valid before work starts | Verifier gate — catches cycles, dangling refs, ownership overlaps, plan↔task coverage gaps |
90
+ | You approve before code is written | Explicit user gate after verifier approval |
91
+ | Parallel workers never collide | Disjoint `file_ownership` globs enforced per wave |
92
+ | Task order is always correct | Topological waves from `depends_on`; chains stay sequential |
93
+ | Failures never cascade silently | Failed tasks exclude downstream into `blockedByFailure`, surfaced to you |
94
+ | Every artifact is reviewable | Everything in one `.archgen/<slug>/` folder, versioned by git |
95
+
96
+ ```text
97
+ interview (greenfield) · survey (brownfield)
98
+ │
99
+ ▼
100
+ .archgen/<slug>/ ── architecture.yaml · docs · ADRs · plans · tasks.yaml
101
+ │
102
+ ▼
103
+ VERIFIER GATE ──issues──► fix & re-verify
104
+ │ APPROVE
105
+ ▼
106
+ USER GATE ────reject───► revise
107
+ │ approve
108
+ ▼
109
+ WAVES ── topological order · one sub-agent per task · disjoint ownership
110
+ │
111
+ ▼
112
+ final report: done / failed / blocked · commits · follow-ups
113
+ ```
114
+
115
+ ## Supported harnesses
116
+
117
+ | Harness | Global install | Project-local (`init`) |
118
+ | --- | --- | --- |
119
+ | Claude Code | ✅ | ✅ |
120
+ | OpenCode | ✅ | ✅ |
121
+ | Cursor | ✅ | ✅ |
122
+ | Codex | ✅ | ✅ |
123
+ | Gemini CLI | ✅ | ✅ |
124
+ | Antigravity | ✅ | ✅ |
125
+ | Any agentskills.io harness | ✅ | ✅ |
126
+
127
+ ## VS Code extension (optional)
128
+
129
+ A read-only visual layer that renders `.archgen/` as a live task DAG (running tasks
130
+ pulse, edges animate), your code-dependency graph, and rendered docs — with a ▶
131
+ button that launches your agent on any task. Install the `.vsix` from
132
+ [GitHub Releases](https://github.com/akashmaurya99/archgen/releases). It is strictly
133
+ a viewer: uninstalling it loses nothing.
134
+
135
+ ## Documentation
136
+
137
+ | Doc | Contents |
138
+ | --- | --- |
139
+ | [Architecture walkthrough](https://github.com/akashmaurya99/archgen/blob/main/docs/architecture.md) | Dual-mode design, gates, wave mechanics, platform detection |
140
+ | [The skill contract](https://github.com/akashmaurya99/archgen/blob/main/skill/SKILL.md) | The full skill specification agents execute |
141
+ | [Changelog](https://github.com/akashmaurya99/archgen/blob/main/CHANGELOG.md) | Release history |
142
+ | [Contributing](https://github.com/akashmaurya99/archgen/blob/main/CONTRIBUTING.md) | Development setup and PR guide |
143
+
144
+ ## Contributing
145
+
146
+ See [CONTRIBUTING.md](https://github.com/akashmaurya99/archgen/blob/main/CONTRIBUTING.md).
147
+ Skill suite and CLI tests run with zero dependencies:
148
+
149
+ ```sh
150
+ node --test skill/scripts/test/*.test.mjs # skill suite
151
+ cd packages/cli && npm test # CLI suite
152
+ ```
153
+
154
+ ## License
155
+
156
+ [MIT](https://github.com/akashmaurya99/archgen/blob/main/LICENSE) © ArchGen contributors
package/bin/archgen.mjs CHANGED
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  // archgen — npm CLI for the archgen skill.
3
3
  //
4
- // npx archgen-skill init Install skill into THIS project + write
5
- // AGENTS.md / CLAUDE.md context pointers
4
+ // npx archgen-skill init [dir] ONE canonical skill store + claude symlink
5
+ // adapter + AGENTS.md / CLAUDE.md context pointers
6
+ // npx archgen-skill doctor [dir] Verify/repair a project install (--check = report only)
6
7
  // npx archgen-skill install Install skill into global harness dirs
7
8
  // (Claude Code, OpenCode, Cursor, agentskills)
8
9
  // npx archgen-skill uninstall Remove globally-installed copies (manifest-based)
10
+ // --project [dir] Remove archgen from a specific project instead
9
11
  //
10
12
  // Zero dependencies. Cross-platform (Windows included — no bash required).
11
13
 
@@ -13,23 +15,53 @@ import { dirname, resolve } from 'node:path';
13
15
  import { fileURLToPath } from 'node:url';
14
16
  import { installGlobal, uninstallGlobal } from '../lib/install.js';
15
17
  import { initProject } from '../lib/init.js';
18
+ import { doctorProject } from '../lib/doctor.js';
19
+ import { uninstallProject } from '../lib/uninstall-project.js';
20
+ import { compareSemver, fetchLatestVersion } from '../lib/version.js';
21
+ import { spawnSync } from 'node:child_process';
22
+ import { readFileSync } from 'node:fs';
16
23
 
17
24
  const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
18
25
 
26
+ function cliVersion() {
27
+ return JSON.parse(readFileSync(resolve(PACKAGE_ROOT, 'package.json'), 'utf8')).version;
28
+ }
29
+
30
+ function runInitAndDoctor(dir) {
31
+ const r = initProject(dir, PACKAGE_ROOT);
32
+ console.log(' = store refreshed at ' + r.storePath);
33
+ doctorProject(dir, PACKAGE_ROOT, {});
34
+ }
35
+
19
36
  const HELP = `archgen — architecture generation & autonomous task execution for coding agents
20
37
 
21
38
  Usage:
22
- npx archgen-skill init [dir] Prepare a project: copy the skill into
23
- .agents/skills/archgen + .claude/skills/archgen
24
- and generate AGENTS.md / CLAUDE.md pointer blocks
25
- so every agent harness auto-discovers it.
39
+ npx archgen-skill init [dir] Prepare a project: create the canonical skill
40
+ store at .agents/skills/archgen, symlink
41
+ .claude/skills/archgen -> ../../.agents/skills/archgen,
42
+ and generate AGENTS.md / CLAUDE.md pointer blocks.
43
+ [--force] proceed automatically; divergent copies are
44
+ backed up to .archgen/.backup/ (never destroyed)
45
+ npx archgen-skill doctor [dir] Health-check a project install: store integrity,
46
+ version stamp, claude link, managed blocks,
47
+ manifest. Repairs safe issues in place.
48
+ [--check] report only - change nothing
26
49
  npx archgen-skill install Install into global harness skill dirs
27
50
  [--copy] real copies instead of symlinks
28
51
  [--uninstall] remove manifest-recorded entries
29
- npx archgen-skill uninstall Alias for install --uninstall
52
+ npx archgen-skill uninstall Remove globally-installed copies (manifest-based)
53
+ --project [dir] remove archgen from a project: claude link,
54
+ managed blocks, canonical store (kept when
55
+ customized) and manifest; .archgen feature
56
+ folders and backups are preserved
57
+ npx archgen-skill update [dir] Check npm for a newer archgen-skill, upgrade
58
+ the global install when outdated, then
59
+ re-init this project + doctor against the
60
+ new version. No-op when already latest.
61
+ npx archgen-skill --version Print the running CLI version
30
62
  npx archgen-skill --help This help
31
63
 
32
- Docs: https://github.com/akash/archgen`;
64
+ Docs: https://github.com/akashmaurya99/archgen`;
33
65
 
34
66
  const [, , cmd, ...rest] = process.argv;
35
67
 
@@ -40,19 +72,40 @@ function flag(name) {
40
72
  return true;
41
73
  }
42
74
 
75
+ function positionalDir() {
76
+ return rest[0] ? resolve(rest[0]) : process.cwd();
77
+ }
78
+
43
79
  try {
44
80
  switch (cmd) {
45
81
  case 'init': {
46
- const dir = rest[0] ? resolve(rest[0]) : process.cwd();
47
- const r = initProject(dir, PACKAGE_ROOT);
48
- console.log('archgen: skill installed into project');
49
- for (const p of r.skillCopies) console.log(' + ' + p);
82
+ flag('--force');
83
+ const r = initProject(positionalDir(), PACKAGE_ROOT);
84
+ console.log('archgen: project ready (single-store layout)');
85
+ console.log(' + store ' + r.storePath);
86
+ if (r.claudeLink === 'created') console.log(' + link .claude/skills/archgen -> ../../.agents/skills/archgen');
87
+ else if (r.claudeLink === 'existing') console.log(' = link .claude/skills/archgen (already points at the store)');
88
+ else if (r.claudeLink === 'migrated') console.log(' ~ link .claude/skills/archgen migrated from copy to symlink');
89
+ else if (r.claudeLink === 'kept-divergent') console.log(' ! kept .claude/skills/archgen (divergent - left untouched)');
90
+ else console.log(' ! skip .claude/skills/archgen (symlink unavailable)');
91
+ for (const b of r.backups) console.log(' ~ backup ' + b + ' (previous store moved aside)');
50
92
  for (const f of r.contextFiles) {
51
93
  console.log(` ${r.createdContextFiles.includes(f) ? 'created' : 'updated'} ${f} (archgen pointer block)`);
52
94
  }
95
+ for (const w of r.warnings) console.log(' note: ' + w);
53
96
  console.log('\nNext: open this project in your agent and say "generate architecture" or "add feature X".');
54
97
  break;
55
98
  }
99
+ case 'doctor': {
100
+ const check = flag('--check');
101
+ const r = doctorProject(positionalDir(), PACKAGE_ROOT, { check });
102
+ console.log('archgen doctor — ' + r.root + (check ? ' (check mode: no changes made)' : ''));
103
+ for (const c of r.checks) console.log(` [${c.status.toLowerCase().padEnd(9)}] ${c.msg}`);
104
+ const t = r.tally;
105
+ console.log(`Summary: ${t.OK} ok, ${t.FIXED} fixed, ${t['WOULD-FIX']} would-fix, ${t.WARN} warning(s), ${t.FAIL} failure(s)`);
106
+ if (r.failures > 0) process.exitCode = 1;
107
+ break;
108
+ }
56
109
  case 'install': {
57
110
  const copy = flag('--copy');
58
111
  if (flag('--uninstall')) { uninstall(); break; }
@@ -64,7 +117,65 @@ try {
64
117
  break;
65
118
  }
66
119
  case 'uninstall': {
67
- uninstall();
120
+ if (flag('--project')) {
121
+ const r = uninstallProject(positionalDir(), PACKAGE_ROOT);
122
+ console.log('archgen: project uninstall — ' + r.root);
123
+ if (r.linkRemoved) console.log(' - .claude/skills/archgen (link)');
124
+ if (r.strippedBlocks.length) console.log(' - managed blocks stripped from ' + r.strippedBlocks.join(', '));
125
+ if (r.storeRemoved) console.log(' - .agents/skills/archgen (unmodified store)');
126
+ if (r.storeKept) console.log(' = .agents/skills/archgen KEPT (customized)');
127
+ if (r.manifestRemoved) console.log(' - .archgen/.install-manifest.json');
128
+ for (const w of r.warnings) console.log(' note: ' + w);
129
+ console.log(' .archgen feature folders and backups are preserved.');
130
+ } else {
131
+ uninstall();
132
+ }
133
+ break;
134
+ }
135
+ case 'update': {
136
+ const dir = positionalDir();
137
+ const current = cliVersion();
138
+ console.log('archgen update — running v' + current + ', checking npm registry…');
139
+ const latest = fetchLatestVersion('archgen-skill');
140
+ if (!latest) {
141
+ console.log(' ! could not reach the npm registry (offline?). Nothing changed.');
142
+ break;
143
+ }
144
+ const cmp = compareSemver(latest, current);
145
+ if (cmp === 0) {
146
+ console.log(' = already on the latest published version (' + latest + ').');
147
+ console.log(' Refreshing this project against it anyway…');
148
+ runInitAndDoctor(dir);
149
+ break;
150
+ }
151
+ if (cmp < 0) {
152
+ console.log(' ↑ running v' + current + ' is newer than npm (' + latest + ') — not published yet.');
153
+ console.log(' Refreshing this project against the running version…');
154
+ runInitAndDoctor(dir);
155
+ break;
156
+ }
157
+ console.log(' ↑ newer version available: ' + latest);
158
+ console.log(' upgrading global install: npm install -g archgen-skill@' + latest);
159
+ const up = spawnSync('npm', ['install', '-g', 'archgen-skill@' + latest], { stdio: 'inherit' });
160
+ if (up.status !== 0) {
161
+ console.error(' ! global upgrade failed — run it manually, then re-run: archgen-skill update');
162
+ process.exitCode = 1;
163
+ break;
164
+ }
165
+ // The running process is the OLD code; re-exec the freshly installed binary.
166
+ const bin = spawnSync('command', ['-v', 'archgen-skill'], { encoding: 'utf8', shell: true });
167
+ const freshBin = (bin.stdout || '').trim();
168
+ if (!freshBin) {
169
+ console.log('\nUpgraded to v' + latest + '. Now finish per project with:\n npx archgen-skill@latest init');
170
+ break;
171
+ }
172
+ console.log('\nre-running init + doctor with the new binary (' + latest + ')…');
173
+ spawnSync(freshBin, ['init', dir], { stdio: 'inherit' });
174
+ spawnSync(freshBin, ['doctor', dir], { stdio: 'inherit' });
175
+ break;
176
+ }
177
+ case '--version': case '-v': {
178
+ console.log(cliVersion());
68
179
  break;
69
180
  }
70
181
  case '--help': case '-h': case undefined: {
package/lib/block.js CHANGED
@@ -4,15 +4,24 @@
4
4
  // archgen writes lives between START/END markers; re-runs replace the block
5
5
  // in place, uninstalls strip it, and anything the user wrote outside the
6
6
  // markers is untouched.
7
+ //
8
+ // AGENTS.md carries the full pointer block with an embedded features registry
9
+ // (between FEATURES_START/FEATURES_END). CLAUDE.md carries a one-line
10
+ // `@AGENTS.md` bridge inside the same managed markers.
11
+
12
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
7
13
 
8
14
  export const START = '<!-- archgen:start (managed block - do not edit between markers) -->';
9
15
  export const END = '<!-- archgen:end -->';
16
+ export const FEATURES_START = '<!-- archgen:features:start -->';
17
+ export const FEATURES_END = '<!-- archgen:features:end -->';
10
18
 
11
19
  /**
12
20
  * Render the archgen pointer block for a project.
13
21
  * @param {string} skillRelPath relative path to the skill dir from project root
22
+ * @param {string} eol line ending used to join the block lines
14
23
  */
15
- export function renderBlock(skillRelPath = '.agents/skills/archgen') {
24
+ export function renderBlock(skillRelPath = '.agents/skills/archgen', eol = '\n') {
16
25
  return [
17
26
  START,
18
27
  '# ArchGen - Architecture Generation & Autonomous Task Execution',
@@ -32,16 +41,24 @@ export function renderBlock(skillRelPath = '.agents/skills/archgen') {
32
41
  '- Generated artifacts live ONLY under `.archgen/<slug>/`',
33
42
  '- Never hand-edit task statuses - use `scripts/set-status.mjs` (comment-safe)',
34
43
  '- Two gates before any execution: verifier approval, then human approval',
44
+ '',
45
+ '## Features registry',
46
+ '',
47
+ FEATURES_START,
48
+ '| Feature | Status | Updated |',
49
+ '| --- | --- | --- |',
50
+ FEATURES_END,
35
51
  END,
36
- ].join('\n');
52
+ ].join(eol);
37
53
  }
38
54
 
39
55
  /**
40
56
  * Insert or replace the managed block inside existing file content.
41
57
  * @param {string} existing file contents ('' when creating new)
42
58
  * @param {string} block rendered block
59
+ * @param {string} eol line ending used when appending to empty/missing files
43
60
  */
44
- export function upsertBlock(existing, block) {
61
+ export function upsertBlock(existing, block, eol = '\n') {
45
62
  const trimmed = existing ?? '';
46
63
  const startIdx = trimmed.indexOf(START);
47
64
  const endIdx = trimmed.indexOf(END);
@@ -51,8 +68,9 @@ export function upsertBlock(existing, block) {
51
68
  if (startIdx !== -1 || endIdx !== -1) {
52
69
  throw new Error('found only one archgen marker - fix or remove it manually');
53
70
  }
54
- if (trimmed.trim() === '') return block + '\n';
55
- return trimmed.replace(/\n*$/, '\n\n') + block + '\n';
71
+ if (trimmed.trim() === '') return block + eol;
72
+ const core = trimmed.replace(/(?:\r?\n)+$/, '');
73
+ return core + eol + eol + block + eol;
56
74
  }
57
75
 
58
76
  /**
@@ -69,3 +87,89 @@ export function stripBlock(existing) {
69
87
  out = out.replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
70
88
  return { content: out, hadBlock: true };
71
89
  }
90
+
91
+ function splitBom(raw) {
92
+ const bom = raw.charCodeAt(0) === 0xfeff;
93
+ return { bom, body: bom ? raw.slice(1) : raw };
94
+ }
95
+
96
+ /**
97
+ * BOM/EOL-aware upsert of a managed block directly into a file.
98
+ * Preserves a leading UTF-8 BOM and the file's CRLF/LF convention.
99
+ * @param {string} absPath target file (created when missing)
100
+ * @param {string[]} blockLines lines between the managed markers
101
+ * @returns {boolean} whether the file existed before the call
102
+ */
103
+ export function upsertManagedFile(absPath, blockLines) {
104
+ const existed = existsSync(absPath);
105
+ const raw = existed ? readFileSync(absPath, 'utf8') : '';
106
+ const { bom, body } = splitBom(raw);
107
+ const eol = body.includes('\r\n') ? '\r\n' : '\n';
108
+ const next = upsertBlock(body, blockLines.join(eol), eol);
109
+ writeFileSync(absPath, (bom ? '\uFEFF' : '') + next);
110
+ return existed;
111
+ }
112
+
113
+ /**
114
+ * True when the file already contains a trimmed `@AGENTS.md` line anywhere
115
+ * (BOM- and CRLF-tolerant).
116
+ */
117
+ export function importsAgents(absPath) {
118
+ let raw = '';
119
+ try { raw = readFileSync(absPath, 'utf8'); } catch { return false; }
120
+ const { body } = splitBom(raw);
121
+ return body.split(/\r?\n/).some((l) => l.trim() === '@AGENTS.md');
122
+ }
123
+
124
+ /** Strip the managed block from a file on disk, preserving BOM. */
125
+ export function stripManagedFile(absPath) {
126
+ let raw = '';
127
+ try { raw = readFileSync(absPath, 'utf8'); } catch { return { hadBlock: false }; }
128
+ const { bom, body } = splitBom(raw);
129
+ const { content, hadBlock } = stripBlock(body);
130
+ if (hadBlock) writeFileSync(absPath, (bom ? '\uFEFF' : '') + content);
131
+ return { hadBlock };
132
+ }
133
+
134
+ function renderRow(row) {
135
+ const cells = Array.isArray(row)
136
+ ? row.map(String)
137
+ : [row.feature ?? row.name ?? '', row.status ?? '', row.updated ?? ''].map(String);
138
+ return '| ' + cells.join(' | ') + ' |';
139
+ }
140
+
141
+ /**
142
+ * Replace the content between the archgen:features markers in a file with the
143
+ * registry table (header rows plus `rows`). When the markers are missing but
144
+ * the managed block exists, the registry section is inserted before its END
145
+ * marker. Never touches anything outside the markers.
146
+ * @param {string} fileAbsPath absolute path to AGENTS.md-style file
147
+ * @param {Array<string[]|{feature:string,status:string,updated:string}>} rows
148
+ * @returns {string} the new file content
149
+ */
150
+ export function upsertFeaturesRegistry(fileAbsPath, rows = []) {
151
+ const raw = readFileSync(fileAbsPath, 'utf8');
152
+ const table = [
153
+ '| Feature | Status | Updated |',
154
+ '| --- | --- | --- |',
155
+ ...rows.map(renderRow),
156
+ ].join('\n');
157
+ const s = raw.indexOf(FEATURES_START);
158
+ const e = raw.indexOf(FEATURES_END);
159
+ let next;
160
+ if (s !== -1 && e !== -1 && e > s) {
161
+ next = raw.slice(0, s + FEATURES_START.length) + '\n' + table + '\n' + raw.slice(e);
162
+ } else if (s === -1 && e === -1) {
163
+ const bStart = raw.indexOf(START);
164
+ const bEnd = raw.indexOf(END);
165
+ if (bStart === -1 || bEnd === -1 || bEnd < bStart) {
166
+ throw new Error('no archgen managed block - cannot place features registry');
167
+ }
168
+ const section = '\n## Features registry\n\n' + FEATURES_START + '\n' + table + '\n' + FEATURES_END + '\n';
169
+ next = raw.slice(0, bEnd) + section + raw.slice(bEnd);
170
+ } else {
171
+ throw new Error('found only one archgen:features marker - fix or remove it manually');
172
+ }
173
+ if (next !== raw) writeFileSync(fileAbsPath, next);
174
+ return next;
175
+ }