agent-docs-bootstrap 0.4.0 → 0.4.2

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/CHANGELOG.md CHANGED
@@ -4,6 +4,20 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), versions follo
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.4.2] - 2026-10-27
8
+
9
+ ### Fixed
10
+ - Content hashes left out every dot file, so a user's own hidden file in a kit folder, such as `.claude/skills/docs-check/.notes.md`, did not count as a change and was deleted by an update or `--uninstall`. Only files the OS adds are left out now: `.DS_Store`, `._*`, `Thumbs.db`, `desktop.ini`. A folder without them hashes as in 0.4.0.
11
+ - `LEGACY` lacked what 0.4.0 and 0.4.1 installed for `docs-check`, so installs stuck on 0.4.0 would stop recovering once `docs-check` changed. The hash is added, and `scripts/check.mjs` now installs every release tag and fails when `LEGACY` misses anything one of them installed; CI fetches the tags for it.
12
+
13
+ ## [0.4.1] - 2026-10-27
14
+
15
+ ### Fixed
16
+ - A `.DS_Store` or other OS file inside an installed folder counted as a change: the first run skipped the path and dropped its marker line, and later runs called it "not installed by this kit", so updates stopped for the whole team once the marker was committed. Dot files, `Thumbs.db` and `desktop.ini` are now left out of content hashes; a folder without them hashes exactly as in 0.4.0.
17
+ - A path that exists without a marker line but holds content a kit version wrote, byte for byte, is recognised, updated and listed again, so installs stuck on 0.4.0 recover with a normal run.
18
+ - A shipped path the user changed keeps its marker line and old hash, so every run reports the same thing about it instead of "changed" first and "not installed by this kit" afterwards.
19
+ - The installer exits non-zero and lists the paths it could not install when a shipped path is skipped, so an agent or CI notices an incomplete install. A changed path the kit no longer ships is only a warning.
20
+
7
21
  ## [0.4.0] - 2026-10-27
8
22
 
9
23
  ### Changed
package/README.md CHANGED
@@ -17,7 +17,7 @@ You need Node.js 18+ and a git repository.
17
17
  1. From your project root, install the skills and subagents:
18
18
 
19
19
  ```bash
20
- npx agent-docs-bootstrap@0.4.0 --target claude
20
+ npx agent-docs-bootstrap@0.4.2 --target claude
21
21
  ```
22
22
 
23
23
  Replace `claude` with `codex`, `opencode`, a list such as `claude,codex`, or `all`.
@@ -103,19 +103,19 @@ All options run the same installer from your project root. Pin the version: the
103
103
  **From npm** (recommended):
104
104
 
105
105
  ```bash
106
- npx agent-docs-bootstrap@0.4.0 --target claude
106
+ npx agent-docs-bootstrap@0.4.2 --target claude
107
107
  ```
108
108
 
109
109
  **From a GitHub release tag**, when the npm registry is blocked or unavailable:
110
110
 
111
111
  ```bash
112
- npx github:TraneDev/agent-docs-bootstrap#v0.4.0 --target claude
112
+ npx github:TraneDev/agent-docs-bootstrap#v0.4.2 --target claude
113
113
  ```
114
114
 
115
115
  **From a clone**, to read or change the kit first:
116
116
 
117
117
  ```bash
118
- git clone --branch v0.4.0 https://github.com/TraneDev/agent-docs-bootstrap.git ~/agent-docs-bootstrap
118
+ git clone --branch v0.4.2 https://github.com/TraneDev/agent-docs-bootstrap.git ~/agent-docs-bootstrap
119
119
  node ~/agent-docs-bootstrap/install.mjs --target claude
120
120
  ```
121
121
 
@@ -134,7 +134,8 @@ Two skills are installed, `docs-bootstrap` and `docs-check`. The guides by other
134
134
  `.agent-docs-bootstrap` lists every installed path with a hash of what was written. The installer replaces or removes a path only if it is listed there and its content still matches, so it never touches files it did not create or that you changed:
135
135
 
136
136
  - a path that exists but is not listed is skipped with a warning;
137
- - a listed path whose content you changed, for example a skill you replaced with your own version, is left in place as yours, with a warning, and drops out of the marker;
137
+ - a listed path whose content you changed is left in place, with a warning, and the installer exits non-zero; a changed path the kit no longer ships, for example a skill you replaced with your own version, becomes yours and drops out of the marker;
138
+ - files the OS adds to the kit's folders (`.DS_Store`, `._*`, `Thumbs.db`, `desktop.ini`) do not count as a change; a file of your own there, hidden or not, does, so it is never deleted;
138
139
  - a marker line not shaped like a path the kit installs (for example `src` or `../x`, added by a bad merge) is ignored with a warning.
139
140
 
140
141
  ### Other agents
package/install.mjs CHANGED
@@ -11,9 +11,10 @@
11
11
  // codex skills → .agents/skills/ agents → .codex/agents/*.toml
12
12
  // opencode skills → .agents/skills/ agents → .opencode/agents/*.md commands → .opencode/commands/*.md
13
13
  // Installed paths are listed in .agent-docs-bootstrap with a hash of what was written. A path is replaced or removed
14
- // only if it is listed, has the shape of a path the kit writes (<layout folder>/<kit name>), and its content still
15
- // matches that hash; a changed path is left in place as the user's. On an update, listed paths this version no
16
- // longer ships are removed under the same rule.
14
+ // only if its line has the shape of a path the kit writes (<layout folder>/<kit name>) and its content still matches
15
+ // that hash or something a version of the kit wrote. OS files such as .DS_Store do not count; a user's own file,
16
+ // hidden or not, does. A changed path is left in place and the run exits non-zero. On an update, listed paths this
17
+ // version no longer ships are removed under the same rule, or left to the user if changed.
17
18
  // No network access, no changes to harness settings, no admin rights.
18
19
 
19
20
  import fs from 'node:fs';
@@ -64,7 +65,9 @@ for (const l of read(MARKER).split('\n').map(l => l.trim()).filter(l => l && !l.
64
65
  if (isOwnedPath(r)) old.set(r, h);
65
66
  else warn(`ignored '${r}' in .agent-docs-bootstrap: not a path this kit installs`);
66
67
  }
67
- // Hashes of every path versions 0.1.0–0.3.0 installed (all targets, no model flags). Fixed: never edit.
68
+ // Hashes of every path each released version installed (all targets, no model flags), so an install whose marker
69
+ // lost its lines is still recognised after the kit changes. Every release adds the outgoing version's hashes;
70
+ // nothing is ever removed. check.mjs installs each release tag and fails, listing the missing ones.
68
71
  const LEGACY = new Set([
69
72
  '00ca80d7077c0e4b', '122ae594f7eb38bd', '185ec7d02d9cf70b', '1f5a0fe496f38dd6', '3798ad63dd954bac',
70
73
  '4c36074d70c5a88c', '562d1e8f3a090ffd', '698cadefe39cb46f', '6acf8481d4dd6a1a', '72bcff6d0e5476ba',
@@ -72,9 +75,22 @@ const LEGACY = new Set([
72
75
  '900cf27ab683e5d1', '96288a3e60126264', '9e0e5c30da22c059', 'b45162299d9aa78b', 'c5d47d8811aa61ee',
73
76
  'c87450584ed42c8b', 'cf2f8a40c91f3c45', 'dac09810ceb351f7', 'dc418221c7363caa', 'f741ba5711364c55',
74
77
  'fceff4c3b3159c80', 'fd975138dcda6b38', 'fe412d3c3c312f7a',
78
+ // 0.4.0, 0.4.1
79
+ '7f6ef4c3f161abfa',
75
80
  ]);
81
+
82
+ const skills = skillDirs();
83
+ const agents = AGENTS.map(n => parseAgent(path.join(KIT, 'agents', `${n}.md`)));
84
+ const models = { 'docs-scout': a['scout-model'], 'docs-reader': a['reader-model'] };
85
+ const COMMANDS = [['docs-bootstrap', "Build the project's reference documentation"], ['docs-check', "Optional: test the project's documentation with a fresh reader"]];
86
+ const commandBody = (name, what) => `---\ndescription: ${JSON.stringify(`${what} (${name})`)}\n---\nLoad the \`${name}\` skill and follow it. Arguments from the developer, if any: $ARGUMENTS\n`;
87
+ // What this version writes when no model is pinned. Content that matches it or LEGACY byte for byte is the kit's,
88
+ // even where the marker lost its line (0.4.0 counted a .DS_Store as a change and dropped the line).
89
+ const CURRENT = new Set([...skills.map(d => contentHash(d)),
90
+ ...TARGETS.flatMap(t => agents.map(ag => textHash(render(t, ag)))), ...COMMANDS.map(([n, w]) => textHash(commandBody(n, w)))]);
91
+ const isKitContent = (h) => LEGACY.has(h) || CURRENT.has(h);
76
92
  // The path is the kit's and nobody changed it since it was installed.
77
- const isUnchanged = (r) => { const h = contentHash(abs(r)); return old.get(r) ? h === old.get(r) : LEGACY.has(h); };
93
+ const isUnchanged = (r) => { const h = contentHash(abs(r)); return h === old.get(r) || isKitContent(h); };
78
94
  const changedNote = (r) => `${r} changed since it was installed, so it is left in place as yours`;
79
95
  if (a.uninstall) { uninstall(); process.exit(0); }
80
96
 
@@ -90,11 +106,18 @@ if (!GLOBAL) {
90
106
 
91
107
  const installed = new Map();
92
108
  const released = new Set();
109
+ const shipped = new Set();
110
+ const skipped = [];
111
+ // A shipped path the user changed keeps its marker line and old hash, so every run says the same thing about it.
93
112
  function writeOwned(r, fn) {
94
- if (installed.has(r) || released.has(r)) return false;
113
+ if (shipped.has(r)) return false;
114
+ shipped.add(r);
95
115
  const dst = abs(r);
96
- if (exists(dst) && !old.has(r)) { warn(`skipped ${r}: exists and was not installed by this kit`); return false; }
97
- if (exists(dst) && !isUnchanged(r)) { warn(`skipped ${changedNote(r)}; delete it and run again to get this version`); released.add(r); return false; }
116
+ if (exists(dst)) {
117
+ const why = !old.has(r) && !isKitContent(contentHash(dst)) ? 'exists and was not installed by this kit'
118
+ : old.has(r) && !isUnchanged(r) ? 'changed since it was installed' : '';
119
+ if (why) { warn(`skipped ${r}: ${why}`); skipped.push(r); return false; }
120
+ }
98
121
  fs.rmSync(dst, { recursive: true, force: true });
99
122
  fs.mkdirSync(path.dirname(dst), { recursive: true });
100
123
  fn(dst);
@@ -102,10 +125,6 @@ function writeOwned(r, fn) {
102
125
  return true;
103
126
  }
104
127
 
105
- const skills = skillDirs();
106
- const agents = AGENTS.map(n => parseAgent(path.join(KIT, 'agents', `${n}.md`)));
107
- const models = { 'docs-scout': a['scout-model'], 'docs-reader': a['reader-model'] };
108
-
109
128
  for (const t of targets) {
110
129
  const L = LAYOUT[t];
111
130
  console.log(`\n${t}`);
@@ -118,29 +137,36 @@ for (const t of targets) {
118
137
  const r = `${L.agents}/${ag.name}.${ext}`;
119
138
  if (writeOwned(r, dst => fs.writeFileSync(dst, render(t, ag, models[ag.name])))) log(`agent ${r}`);
120
139
  }
121
- if (L.commands) for (const [name, what] of [['docs-bootstrap', "Build the project's reference documentation"], ['docs-check', "Optional: test the project's documentation with a fresh reader"]]) {
140
+ if (L.commands) for (const [name, what] of COMMANDS) {
122
141
  const r = `${L.commands}/${name}.md`;
123
- const body = `---\ndescription: ${JSON.stringify(`${what} (${name})`)}\n---\nLoad the \`${name}\` skill and follow it. Arguments from the developer, if any: $ARGUMENTS\n`;
124
- if (writeOwned(r, dst => fs.writeFileSync(dst, body))) log(`command /${name}`);
142
+ if (writeOwned(r, dst => fs.writeFileSync(dst, commandBody(name, what)))) log(`command /${name}`);
125
143
  }
126
144
  }
127
145
 
128
146
  // Paths an earlier version installed into these targets' folders and this version no longer ships.
129
147
  const ownDirs = new Set(targets.flatMap(t => Object.values(LAYOUT[t])));
130
148
  for (const r of old.keys()) {
131
- if (installed.has(r) || released.has(r) || !ownDirs.has(path.dirname(r)) || !exists(abs(r))) continue;
149
+ if (shipped.has(r) || !ownDirs.has(path.dirname(r)) || !exists(abs(r))) continue;
132
150
  if (isUnchanged(r)) { fs.rmSync(abs(r), { recursive: true, force: true }); log(`removed ${r} (no longer shipped)`); }
133
151
  else { warn(`not removed: ${changedNote(r)}`); released.add(r); }
134
152
  }
135
153
 
136
- // Keep entries of earlier installs (e.g. another target) that are still on disk; a changed path is no longer listed.
154
+ // Keep entries of earlier installs (e.g. another target) and skipped shipped paths that are still on disk; a changed
155
+ // path this version no longer ships is the user's and is no longer listed.
137
156
  const keep = [...old].filter(([r]) => !installed.has(r) && !released.has(r) && exists(abs(r)));
138
157
  fs.writeFileSync(MARKER, [`version=${VERSION}`, ...[...keep, ...installed].map(([r, h]) => h ? `${r} ${h}` : r)].join('\n') + '\n');
139
158
 
140
- console.log(`\nDone: agent-docs-bootstrap ${VERSION}, ${targets.join(', ')}${GLOBAL ? ' (global)' : ''}.`);
141
- if (!GLOBAL) log(`commit the installed folders and .agent-docs-bootstrap so the whole team gets the same version`);
142
- log('restart the agent session so it picks up the new skills and subagents');
143
- log('start with the docs-bootstrap skill (Claude Code /docs-bootstrap, Codex $docs-bootstrap, OpenCode /docs-bootstrap)');
159
+ if (skipped.length) {
160
+ console.error(`\nIncomplete: agent-docs-bootstrap ${VERSION} could not install ${skipped.length} path(s):`);
161
+ for (const r of skipped) console.error(` ${r}`);
162
+ console.error('They hold content this kit did not write. To get this version, move or delete them, then run the installer again.');
163
+ process.exitCode = 1;
164
+ } else {
165
+ console.log(`\nDone: agent-docs-bootstrap ${VERSION}, ${targets.join(', ')}${GLOBAL ? ' (global)' : ''}.`);
166
+ if (!GLOBAL) log(`commit the installed folders and .agent-docs-bootstrap so the whole team gets the same version`);
167
+ log('restart the agent session so it picks up the new skills and subagents');
168
+ log('start with the docs-bootstrap skill (Claude Code /docs-bootstrap, Codex $docs-bootstrap, OpenCode /docs-bootstrap)');
169
+ }
144
170
 
145
171
  // ---------- rendering
146
172
  function render(target, ag, model) {
@@ -220,15 +246,22 @@ function usage(code, msg) {
220
246
  process.exit(code);
221
247
  }
222
248
  // Content hash of an installed file or folder: relative names and text with line endings normalised, so a
223
- // checkout with CRLF line endings still matches.
249
+ // checkout with CRLF line endings still matches. Files the OS adds (.DS_Store, ._*, Thumbs.db, desktop.ini) are
250
+ // left out; the kit ships none (check.mjs makes sure). The format must not change: markers and LEGACY depend on it.
224
251
  function contentHash(p) {
225
252
  const h = createHash('sha256');
226
253
  const files = fs.lstatSync(p).isDirectory() ? listFiles(p).sort() : [''];
227
254
  for (const f of files) h.update(`${f}\0${read(path.join(p, f)).replace(/\r\n/g, '\n')}\0`);
228
255
  return h.digest('hex').slice(0, 16);
229
256
  }
257
+ // Only files the OS adds on its own. A user's own hidden file, such as .notes.md, is content and counts as a change.
258
+ function isOsFile(name) { return /^(\.DS_Store|Thumbs\.db|desktop\.ini)$/i.test(name) || name.startsWith('._'); }
259
+ // The same hash for text about to be written as a file.
260
+ function textHash(text) {
261
+ return createHash('sha256').update(`\0${text.replace(/\r\n/g, '\n')}\0`).digest('hex').slice(0, 16);
262
+ }
230
263
  function listFiles(dir, base = dir) {
231
- return fs.readdirSync(dir, { withFileTypes: true }).flatMap(e => {
264
+ return fs.readdirSync(dir, { withFileTypes: true }).filter(e => !isOsFile(e.name)).flatMap(e => {
232
265
  const p = path.join(dir, e.name);
233
266
  return e.isDirectory() ? listFiles(p, base) : [path.relative(base, p).split(path.sep).join('/')];
234
267
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-docs-bootstrap",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Agent skills that build reference documentation for a codebase (glossary, ADRs, architecture, how-tos, AGENTS.md) through interviews with the team, with an optional check by a fresh agent. Works with any agent that runs skills; the installer sets up Claude Code, Codex and OpenCode.",
5
5
  "license": "MIT",
6
6
  "repository": { "type": "git", "url": "git+https://github.com/TraneDev/agent-docs-bootstrap.git" },