@sabaiway/agent-workflow-kit 1.15.1 → 1.15.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/SKILL.md +1 -1
  3. package/capability.json +1 -1
  4. package/package.json +5 -2
  5. package/bin/install.test.mjs +0 -347
  6. package/tools/delegation.test.mjs +0 -116
  7. package/tools/detect-backends.test.mjs +0 -444
  8. package/tools/engine-source.test.mjs +0 -280
  9. package/tools/family-registry.test.mjs +0 -279
  10. package/tools/fs-safe.test.mjs +0 -346
  11. package/tools/hide-footprint.integration.test.mjs +0 -168
  12. package/tools/hide-footprint.test.mjs +0 -463
  13. package/tools/inject-methodology.test.mjs +0 -689
  14. package/tools/known-footprint.test.mjs +0 -271
  15. package/tools/manifest/fixtures/bad-available/SKILL.md +0 -7
  16. package/tools/manifest/fixtures/bad-available/capability.json +0 -10
  17. package/tools/manifest/fixtures/detect-array/SKILL.md +0 -7
  18. package/tools/manifest/fixtures/detect-array/capability.json +0 -10
  19. package/tools/manifest/fixtures/malformed-json/capability.json +0 -1
  20. package/tools/manifest/fixtures/metadata-version/SKILL.md +0 -10
  21. package/tools/manifest/fixtures/metadata-version/capability.json +0 -9
  22. package/tools/manifest/fixtures/missing-key/SKILL.md +0 -7
  23. package/tools/manifest/fixtures/missing-key/capability.json +0 -8
  24. package/tools/manifest/fixtures/missing-source/SKILL.md +0 -7
  25. package/tools/manifest/fixtures/missing-source/capability.json +0 -11
  26. package/tools/manifest/fixtures/nested-version-decoy/SKILL.md +0 -10
  27. package/tools/manifest/fixtures/nested-version-decoy/capability.json +0 -9
  28. package/tools/manifest/fixtures/null-root/capability.json +0 -1
  29. package/tools/manifest/fixtures/provides-roles-mismatch/SKILL.md +0 -7
  30. package/tools/manifest/fixtures/provides-roles-mismatch/bin/run.sh +0 -2
  31. package/tools/manifest/fixtures/provides-roles-mismatch/capability.json +0 -11
  32. package/tools/manifest/fixtures/stub/capability.json +0 -10
  33. package/tools/manifest/fixtures/traversal-source/SKILL.md +0 -7
  34. package/tools/manifest/fixtures/traversal-source/capability.json +0 -11
  35. package/tools/manifest/fixtures/unknown-schema/capability.json +0 -9
  36. package/tools/manifest/fixtures/valid/SKILL.md +0 -10
  37. package/tools/manifest/fixtures/valid/bin/run.sh +0 -3
  38. package/tools/manifest/fixtures/valid/capability.json +0 -18
  39. package/tools/manifest/fixtures/version-mismatch/SKILL.md +0 -7
  40. package/tools/manifest/fixtures/version-mismatch/capability.json +0 -9
  41. package/tools/manifest/fixtures/win-absolute-source/SKILL.md +0 -7
  42. package/tools/manifest/fixtures/win-absolute-source/capability.json +0 -11
  43. package/tools/manifest/validate.test.mjs +0 -73
  44. package/tools/procedures.test.mjs +0 -303
  45. package/tools/recipes.test.mjs +0 -538
  46. package/tools/release-scan.test.mjs +0 -41
  47. package/tools/setup-backends.test.mjs +0 -500
  48. package/tools/uninstall.integration.test.mjs +0 -144
  49. package/tools/uninstall.test.mjs +0 -401
  50. package/tools/velocity-profile.test.mjs +0 -496
@@ -1,463 +0,0 @@
1
- import { describe, it } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { join, dirname, basename, resolve } from 'node:path';
4
- import {
5
- hideFootprint,
6
- inferVisibility,
7
- migrateFromGlobal,
8
- buildBlock,
9
- START_MARKER,
10
- END_MARKER,
11
- } from './hide-footprint.mjs';
12
- import { FOOTPRINT_STOP } from './known-footprint.mjs';
13
-
14
- const DIR = '/repo';
15
- const EXCLUDE = join(DIR, '.git/info/exclude');
16
- const ENOENT = (p) => Object.assign(new Error(`ENOENT: ${p}`), { code: 'ENOENT' });
17
-
18
- // ── in-memory world (absolute path → node) ───────────────────────────────────────
19
-
20
- const makeWorld = (seed = {}) => {
21
- const nodes = new Map(); // abspath → { kind:'file'|'dir', content? }
22
- const world = {
23
- file: (rel, content = '') => { nodes.set(join(DIR, rel), { kind: 'file', content }); return world; },
24
- dir: (rel) => { nodes.set(join(DIR, rel.replace(/\/$/, '')), { kind: 'dir' }); return world; },
25
- abs: (p, content = '') => { nodes.set(p, { kind: 'file', content }); return world; },
26
- read: (rel) => nodes.get(join(DIR, rel))?.content,
27
- nodes,
28
- };
29
- if (seed.exclude !== undefined) world.file('.git/info/exclude', seed.exclude);
30
- return world;
31
- };
32
-
33
- const fsDeps = (world) => ({
34
- readFile: (p) => { const n = world.nodes.get(p); if (!n || n.kind !== 'file') throw ENOENT(p); return n.content; },
35
- writeFile: (p, c) => { world.nodes.set(p, { kind: 'file', content: c }); },
36
- stat: (p) => { const n = world.nodes.get(p); if (!n) throw ENOENT(p); return { isFile: () => n.kind === 'file' }; },
37
- readdir: (p) => {
38
- const n = world.nodes.get(p);
39
- if (!n || n.kind !== 'dir') throw ENOENT(p);
40
- const names = [];
41
- for (const k of world.nodes.keys()) if (dirname(k) === p) names.push(basename(k));
42
- return names;
43
- },
44
- });
45
-
46
- // ── mock git (asserts cwd + repo-relative probes) ────────────────────────────────
47
-
48
- const gitDeps = (cfg, world) => (args, opts) => {
49
- assert.equal(opts.cwd, DIR, 'every git call runs with cwd = the project dir');
50
- const [cmd, ...rest] = args;
51
- if (cmd === 'rev-parse') return { status: 0, stdout: '.git/info/exclude\n', stderr: '' };
52
- if (cmd === 'config') {
53
- if (cfg.global == null) return { status: 1, stdout: '', stderr: '' }; // unset
54
- return { status: 0, stdout: `${cfg.global}\n`, stderr: '' };
55
- }
56
- const probe = rest[rest.length - 1];
57
- assert.ok(!probe.startsWith('/'), `probe is repo-relative, never the anchored "/" form: ${probe}`);
58
- if (cmd === 'ls-files') {
59
- if (cfg.lsFilesFails) return { status: 128, stdout: '', stderr: 'fatal: boom' };
60
- const hits = (cfg.tracked ?? []).filter((t) => (probe.endsWith('/') ? t.startsWith(probe) : t === probe));
61
- return { status: 0, stdout: hits.map((h) => `${h}\0`).join(''), stderr: '' };
62
- }
63
- if (cmd === 'check-ignore') {
64
- if (cfg.checkIgnoreFails) return { status: 128, stdout: '', stderr: 'fatal: bad config' };
65
- // Real precedence: a tracked .gitignore > .git/info/exclude > global core.excludesFile.
66
- const stat = (cfg.ignored ?? {})[probe];
67
- const fmt = (s) => ({ status: 0, stdout: `${s.source}:${s.line ?? 1}:${s.pattern ?? probe}\t${probe}\n`, stderr: '' });
68
- if (stat && basename(stat.source) === '.gitignore') return fmt(stat); // gitignore wins
69
- const content = world.nodes.get(EXCLUDE)?.content ?? '';
70
- const lines = content.split('\n').map((l) => l.replace(/\r$/, '')); // git is EOL-agnostic
71
- if (lines.includes(`/${probe}`)) return { status: 0, stdout: `.git/info/exclude:1:/${probe}\t${probe}\n`, stderr: '' };
72
- if (stat) return fmt(stat); // global (lower than the local exclude)
73
- return { status: 1, stdout: '', stderr: '' };
74
- }
75
- return { status: 128, stdout: '', stderr: 'unknown' };
76
- };
77
-
78
- const mk = (cfg = {}, seed = {}) => {
79
- const world = makeWorld(seed);
80
- if (seed.world) seed.world(world);
81
- return { world, deps: { git: gitDeps(cfg, world), ...fsDeps(world), home: '/home/u', log: () => {}, errlog: () => {} } };
82
- };
83
-
84
- // ── buildBlock ────────────────────────────────────────────────────────────────────
85
-
86
- describe('buildBlock', () => {
87
- it('sorts + dedupes', () => {
88
- assert.deepEqual(buildBlock(['/b', '/a', '/b', '/c']), ['/a', '/b', '/c']);
89
- });
90
- });
91
-
92
- // ── idempotency + EOL + splice ────────────────────────────────────────────────────
93
-
94
- describe('hide flow — idempotency & splice', () => {
95
- it('apply twice → byte-identical (zero diff)', () => {
96
- const { world, deps } = mk();
97
- const r1 = hideFootprint({ dir: DIR }, deps);
98
- assert.equal(r1.action, 'created');
99
- const after1 = world.read('.git/info/exclude');
100
- const r2 = hideFootprint({ dir: DIR }, deps);
101
- assert.equal(r2.action, 'noop');
102
- assert.equal(world.read('.git/info/exclude'), after1, 'second run leaves the file byte-for-byte');
103
- });
104
-
105
- it('one managed block, markers present, KIT_OWN hidden', () => {
106
- const { world, deps } = mk();
107
- const r = hideFootprint({ dir: DIR }, deps);
108
- const content = world.read('.git/info/exclude');
109
- assert.equal(content.split(START_MARKER).length - 1, 1, 'exactly one start marker');
110
- assert.equal(content.split(END_MARKER).length - 1, 1, 'exactly one end marker');
111
- assert.ok(r.wrote.includes('/AGENTS.md') && r.wrote.includes('/docs/ai/'));
112
- assert.ok(content.endsWith('\n'));
113
- });
114
-
115
- it('preserves outside boilerplate; appends the fence', () => {
116
- const boiler = '# git ls-files --others --exclude-from=.git/info/exclude\n# *~\n';
117
- const { world, deps } = mk({}, { exclude: boiler });
118
- hideFootprint({ dir: DIR }, deps);
119
- const content = world.read('.git/info/exclude');
120
- assert.ok(content.startsWith('# git ls-files --others'), 'boilerplate kept at the top');
121
- assert.ok(content.includes('# *~'), 'comment kept');
122
- assert.ok(content.indexOf(START_MARKER) > content.indexOf('# *~'), 'fence appended after boilerplate');
123
- });
124
-
125
- it('CRLF preserved when the file uses CRLF', () => {
126
- const { world, deps } = mk({}, { exclude: '# boiler\r\n' });
127
- hideFootprint({ dir: DIR }, deps);
128
- const content = world.read('.git/info/exclude');
129
- assert.ok(content.includes('\r\n'), 'CRLF EOL preserved');
130
- assert.ok(!/[^\r]\n/.test(content), 'no lone LF introduced');
131
- });
132
-
133
- it('empty/missing exclude → LF default, still idempotent', () => {
134
- const { world, deps } = mk(); // no exclude file at all
135
- hideFootprint({ dir: DIR }, deps);
136
- const content = world.read('.git/info/exclude');
137
- assert.ok(content.includes('\n') && !content.includes('\r'));
138
- const before = content;
139
- hideFootprint({ dir: DIR }, deps);
140
- assert.equal(world.read('.git/info/exclude'), before);
141
- });
142
-
143
- it('malformed markers (single / reversed / duplicate) → STOP, file byte-for-byte unchanged', () => {
144
- const cases = [
145
- `${START_MARKER}\n/AGENTS.md\n`, // single: start, no end
146
- `${END_MARKER}\n/x\n${START_MARKER}\n`, // reversed: end before start
147
- `${START_MARKER}\n/x\n${START_MARKER}\n/y\n${END_MARKER}\n`, // duplicate start
148
- ];
149
- for (const bad of cases) {
150
- const { world, deps } = mk({}, { exclude: bad });
151
- assert.throws(() => hideFootprint({ dir: DIR }, deps), (e) => e.code === FOOTPRINT_STOP, `STOP on: ${JSON.stringify(bad)}`);
152
- assert.equal(world.read('.git/info/exclude'), bad, 'left byte-for-byte');
153
- }
154
- });
155
- });
156
-
157
- // ── classify: tracked → ASK; order; drop; risk ───────────────────────────────────
158
-
159
- describe('classify', () => {
160
- it('a tracked KIT_OWN path → ASK (excluded by default), with rm guidance', () => {
161
- const { deps } = mk({ tracked: ['.claude/settings.json'] });
162
- const r = hideFootprint({ dir: DIR }, deps);
163
- assert.ok(!r.wrote.includes('/.claude/settings.json'), 'tracked path not written');
164
- assert.ok(r.asks.some((a) => a.path === '/.claude/settings.json'), 'surfaced as ASK');
165
- });
166
-
167
- it('--include opts a tracked ASK path into needsUntrack (written line + rm guidance, NOT hidden)', () => {
168
- const { world, deps } = mk({ tracked: ['.claude/settings.json'] });
169
- const r = hideFootprint({ dir: DIR, include: ['/.claude/settings.json'] }, deps);
170
- assert.ok(r.wrote.includes('/.claude/settings.json'), 'now written into the block');
171
- assert.ok(r.needsUntrack.some((n) => n.path === '/.claude/settings.json' && n.command.includes('git rm --cached')));
172
- assert.ok(!r.verify.some((v) => v.path === '/.claude/settings.json' && v.hidden), 'tracked → never counted hidden');
173
- });
174
-
175
- it('--include rejects a path that is not one of this run’s asks', () => {
176
- const { deps } = mk();
177
- assert.throws(() => hideFootprint({ dir: DIR, include: ['/src/'] }, deps), (e) => e.code === FOOTPRINT_STOP);
178
- });
179
-
180
- it('--include rejects traversal / globs', () => {
181
- const { deps } = mk({ tracked: ['.claude/settings.json'] });
182
- assert.throws(() => hideFootprint({ dir: DIR, include: ['../etc'] }, deps), (e) => e.code === FOOTPRINT_STOP);
183
- assert.throws(() => hideFootprint({ dir: DIR, include: ['/.github/copilot-*'] }, deps), (e) => e.code === FOOTPRINT_STOP);
184
- });
185
-
186
- it('untracked + covered by a tracked .gitignore → DROPPED; tracked + covered → ASK (tracked checked first)', () => {
187
- const { deps } = mk({
188
- tracked: ['.gitignore', 'AGENTS.md'],
189
- ignored: { 'AGENTS.md': { source: '.gitignore' }, 'docs/ai/': { source: '.gitignore' } },
190
- });
191
- const r = hideFootprint({ dir: DIR }, deps);
192
- assert.ok(r.dropped.includes('/docs/ai/'), 'untracked + gitignore-covered → dropped as redundant');
193
- assert.ok(r.asks.some((a) => a.path === '/AGENTS.md'), 'tracked wins over the gitignore-drop → ASK');
194
- assert.ok(!r.dropped.includes('/AGENTS.md'));
195
- });
196
-
197
- it('ls-files failure → fail-closed STOP (UNKNOWN never counts as safe-to-hide)', () => {
198
- const { deps } = mk({ lsFilesFails: true });
199
- assert.throws(() => hideFootprint({ dir: DIR }, deps), (e) => e.code === FOOTPRINT_STOP);
200
- });
201
-
202
- it('check-ignore exit 128 → fail-closed STOP (the symmetric guard to ls-files)', () => {
203
- const { deps } = mk({ checkIgnoreFails: true });
204
- assert.throws(() => hideFootprint({ dir: DIR }, deps), (e) => e.code === FOOTPRINT_STOP);
205
- });
206
- });
207
-
208
- // ── present external footprint + glob ────────────────────────────────────────────
209
-
210
- describe('external footprint', () => {
211
- it('a present low-risk external dir → HIDE; an absent external → not emitted', () => {
212
- const { deps } = mk({}, { world: (w) => w.dir('.continue') });
213
- const r = hideFootprint({ dir: DIR }, deps);
214
- assert.ok(r.wrote.includes('/.continue/'), 'present external dir hidden');
215
- assert.ok(!r.wrote.includes('/.cursorrules'), 'absent external not emitted');
216
- });
217
-
218
- it('a present high-risk external file (generic name) → ASK, not hidden by default', () => {
219
- const { deps } = mk({}, { world: (w) => w.file('GEMINI.md') });
220
- const r = hideFootprint({ dir: DIR }, deps);
221
- assert.ok(!r.wrote.includes('/GEMINI.md'), 'present-high-risk excluded by default');
222
- assert.ok(r.asks.some((a) => a.path === '/GEMINI.md'));
223
- });
224
-
225
- it('glob /.github/copilot-* expands to each present file; a tracked sibling does not pull them into ASK', () => {
226
- const { deps } = mk(
227
- { tracked: ['.github/workflows/ci.yml'] },
228
- { world: (w) => w.dir('.github').file('.github/copilot-instructions.md').file('.github/copilot-setup.yml').dir('.github/workflows') },
229
- );
230
- const r = hideFootprint({ dir: DIR }, deps);
231
- // copilot-* is falsePositiveRisk:true → present → ASK each (not hidden by default). The ASK reason
232
- // must be the RISK reason, NOT the tracked reason — proving each file was probed on its OWN path
233
- // and the tracked sibling did not leak into their classification.
234
- const a1 = r.asks.find((a) => a.path === '/.github/copilot-instructions.md');
235
- const a2 = r.asks.find((a) => a.path === '/.github/copilot-setup.yml');
236
- assert.ok(a1 && /generic|confirm before hiding/.test(a1.reason) && !/tracked/.test(a1.reason));
237
- assert.ok(a2 && /generic|confirm before hiding/.test(a2.reason));
238
- assert.ok(!r.wrote.some((p) => p.startsWith('/.github/copilot')));
239
- });
240
-
241
- it('a glob-expanded file consented via --include is RETAINED on re-run (zero diff, idempotent)', () => {
242
- const seed = { world: (w) => w.dir('.github').file('.github/copilot-instructions.md') };
243
- const { world, deps } = mk({}, seed);
244
- const r1 = hideFootprint({ dir: DIR, include: ['/.github/copilot-instructions.md'] }, deps);
245
- assert.ok(r1.wrote.includes('/.github/copilot-instructions.md'), 'consented glob child written');
246
- const c1 = world.read('.git/info/exclude');
247
- const r2 = hideFootprint({ dir: DIR }, deps); // no --include — consent must survive
248
- assert.ok(r2.wrote.includes('/.github/copilot-instructions.md'), 'glob-child consent survives re-run');
249
- assert.ok(!r2.asks.some((a) => a.path === '/.github/copilot-instructions.md'), 'not re-asked');
250
- assert.equal(r2.action, 'noop');
251
- assert.equal(world.read('.git/info/exclude'), c1, 'zero diff (no silent un-hide of the copilot file)');
252
- });
253
- });
254
-
255
- // ── re-run consent (D4) ──────────────────────────────────────────────────────────
256
-
257
- describe('re-run preserves prior consent', () => {
258
- it('a consented present-high-risk path stays on re-run without --include (zero diff, not re-asked)', () => {
259
- const { world, deps } = mk({}, { world: (w) => w.file('GEMINI.md') });
260
- const r1 = hideFootprint({ dir: DIR, include: ['/GEMINI.md'] }, deps); // opt in once
261
- assert.ok(r1.wrote.includes('/GEMINI.md'));
262
- const c1 = world.read('.git/info/exclude');
263
- const r2 = hideFootprint({ dir: DIR }, deps); // no --include this time
264
- assert.ok(r2.wrote.includes('/GEMINI.md'), 'prior in-block consent retained');
265
- assert.ok(!r2.asks.some((a) => a.path === '/GEMINI.md'), 'not re-asked');
266
- assert.equal(r2.action, 'noop');
267
- assert.equal(world.read('.git/info/exclude'), c1, 'zero diff');
268
- });
269
-
270
- it('a previously-consented high-risk path that DISAPPEARED is dropped (consent does not survive)', () => {
271
- const { world, deps } = mk({}, { world: (w) => w.file('GEMINI.md') });
272
- hideFootprint({ dir: DIR, include: ['/GEMINI.md'] }, deps); // in the block now
273
- world.nodes.delete(join(DIR, 'GEMINI.md')); // GEMINI.md disappears from disk
274
- const r = hideFootprint({ dir: DIR }, deps);
275
- assert.ok(!r.wrote.includes('/GEMINI.md'), 'disappeared high-risk path dropped');
276
- assert.ok(!world.read('.git/info/exclude').includes('/GEMINI.md'));
277
- });
278
- });
279
-
280
- // ── absorb (D8) — verbatim present-machine exclude ───────────────────────────────
281
-
282
- describe('absorb pre-existing recognized local lines', () => {
283
- const presentMachine = [
284
- '# git ls-files --others --exclude-from=.git/info/exclude',
285
- "# Lines that start with '#' are comments.",
286
- '',
287
- '# standalone local-dev agent skills — kept out of THIS repo (project-local; visibility is a project setting)',
288
- '.claude/skills/',
289
- '',
290
- ].join('\n');
291
-
292
- it('folds the AD-013 comment + bare .claude/skills/ (dir ABSENT) into the fence, no orphan comment, apply-twice zero diff', () => {
293
- const { world, deps } = mk({}, { exclude: presentMachine });
294
- const r1 = hideFootprint({ dir: DIR }, deps);
295
- const c1 = world.read('.git/info/exclude');
296
- assert.ok(r1.wrote.includes('/.claude/skills/'), 'folded into the fence (preserved despite absent dir)');
297
- assert.ok(!c1.includes('# standalone local-dev agent skills'), 'orphan comment removed');
298
- assert.equal((c1.match(/\.claude\/skills\//g) ?? []).length, 1, 'exactly one .claude/skills/ rule, inside the fence');
299
- assert.ok(c1.startsWith('# git ls-files --others'), 'git boilerplate preserved');
300
- const r2 = hideFootprint({ dir: DIR }, deps);
301
- assert.equal(r2.action, 'noop');
302
- assert.equal(world.read('.git/info/exclude'), c1, 'apply-twice = zero diff');
303
- });
304
-
305
- it('a loose pre-existing HIGH-RISK line whose file is PRESENT is folded as consent (no report mismatch, one block)', () => {
306
- const { world, deps } = mk({}, { exclude: '# boiler\n/.cursorrules\n', world: (w) => w.file('.cursorrules') });
307
- const r = hideFootprint({ dir: DIR }, deps);
308
- const content = world.read('.git/info/exclude');
309
- assert.ok(r.wrote.includes('/.cursorrules'), 'folded into the managed block as prior consent');
310
- assert.ok(!r.asks.some((a) => a.path === '/.cursorrules'), 'not reported as un-consented ASK while it is in fact hidden');
311
- assert.equal((content.match(/\.cursorrules/g) ?? []).length, 1, 'exactly one .cursorrules rule, inside the fence');
312
- assert.ok(content.indexOf('/.cursorrules') > content.indexOf(START_MARKER), 'inside the fence, not loose');
313
- const v = r.verify.find((x) => x.path === '/.cursorrules');
314
- assert.ok(v && v.hidden === true, 'verified hidden');
315
- });
316
-
317
- it('a loose HIGH-RISK line whose file is ABSENT is left untouched (a pre-emptive hide is never silently removed)', () => {
318
- const { world, deps } = mk({}, { exclude: '# boiler\n/.cursorrules\n' }); // .cursorrules absent
319
- const r = hideFootprint({ dir: DIR }, deps);
320
- assert.ok(!r.wrote.includes('/.cursorrules'), 'absent high-risk path not folded');
321
- assert.ok(world.read('.git/info/exclude').includes('/.cursorrules'), 'the user’s loose line is preserved as-is');
322
- });
323
- });
324
-
325
- // ── verify (D5) ──────────────────────────────────────────────────────────────────
326
-
327
- describe('verify', () => {
328
- it('a tracked path written via --include reports NOT hidden', () => {
329
- const { deps } = mk({ tracked: ['.claude/settings.json'] });
330
- const r = hideFootprint({ dir: DIR, include: ['/.claude/settings.json'] }, deps);
331
- const v = r.verify.find((x) => x.path === '/.claude/settings.json');
332
- assert.ok(v && v.tracked === true && v.hidden === false);
333
- });
334
-
335
- it('an untracked auto-HIDE path verifies hidden by our exclude source', () => {
336
- const { deps } = mk();
337
- const r = hideFootprint({ dir: DIR }, deps);
338
- const v = r.verify.find((x) => x.path === '/AGENTS.md');
339
- assert.ok(v && v.hidden === true && /info\/exclude/.test(v.source));
340
- });
341
- });
342
-
343
- // ── migrateFromGlobal (D6/D7) ────────────────────────────────────────────────────
344
-
345
- describe('migrateFromGlobal', () => {
346
- const GLOBAL = '/home/u/.gitignore_global';
347
- const kitOld = [
348
- '',
349
- '# agent-workflow-kit hidden mode (machine-local; remove these lines to un-hide)',
350
- '/AGENTS.md', '/CLAUDE.md', '/docs/ai/',
351
- '/scripts/_expect-shim.mjs', '/scripts/install-git-hooks.mjs',
352
- '',
353
- ].join('\n');
354
-
355
- it('default KEEPS + reports the legacy global block; --remove-global removes it + returns a backup', () => {
356
- const { world, deps } = mk({ global: GLOBAL }, { world: (w) => w.abs(GLOBAL, kitOld) });
357
- const kept = migrateFromGlobal(deps, DIR, { home: '/home/u', removeGlobal: false, dryRun: false });
358
- assert.equal(kept.action, 'kept');
359
- assert.ok(kept.removedLines.includes('/AGENTS.md'));
360
- assert.equal(world.nodes.get(GLOBAL).content, kitOld, 'kept = file untouched');
361
-
362
- const { world: w2, deps: d2 } = mk({ global: GLOBAL }, { world: (w) => w.abs(GLOBAL, kitOld) });
363
- const removed = migrateFromGlobal(d2, DIR, { home: '/home/u', removeGlobal: true, dryRun: false });
364
- assert.equal(removed.action, 'removed');
365
- assert.ok(removed.backup.includes('/AGENTS.md'));
366
- assert.ok(!w2.nodes.get(GLOBAL).content.includes('/AGENTS.md'), 'global block removed');
367
- });
368
-
369
- it('preserves a user line interleaved with the legacy block', () => {
370
- const withUser = `/AGENTS.md\n/CLAUDE.md\n\n/my/own/rule\n`;
371
- const { world, deps } = mk({ global: GLOBAL }, { world: (w) => w.abs(GLOBAL, withUser) });
372
- const r = migrateFromGlobal(deps, DIR, { home: '/home/u', removeGlobal: true, dryRun: false });
373
- assert.equal(r.action, 'removed');
374
- assert.ok(world.nodes.get(GLOBAL).content.includes('/my/own/rule'), 'user line preserved');
375
- assert.ok(!world.nodes.get(GLOBAL).content.includes('/AGENTS.md'));
376
- });
377
-
378
- it('core.excludesFile unset (exit 1) → no-op, never a STOP', () => {
379
- const { deps } = mk({ global: null });
380
- const r = migrateFromGlobal(deps, DIR, { home: '/home/u', removeGlobal: true, dryRun: false });
381
- assert.equal(r.found, false);
382
- assert.equal(r.action, 'none');
383
- });
384
-
385
- it('a memory-old global path-set (no header) is recognized + removed', () => {
386
- const memOld = `/AGENTS.md\n/docs/ai/\n/docs/ai/.memory-version\n/docs/plans/\n`;
387
- const { world, deps } = mk({ global: GLOBAL }, { world: (w) => w.abs(GLOBAL, memOld) });
388
- const r = migrateFromGlobal(deps, DIR, { home: '/home/u', removeGlobal: true, dryRun: false });
389
- assert.equal(r.action, 'removed');
390
- assert.equal(world.nodes.get(GLOBAL).content, '', 'whole memory-old block recognized + removed');
391
- });
392
-
393
- it('the AD-013 standalone form as a GLOBAL block is recognized + removed (the third D7 fixture)', () => {
394
- const ad013 = '# standalone local-dev agent skills — kept out of THIS repo (project-local; visibility is a project setting)\n.claude/skills/\n';
395
- const { world, deps } = mk({ global: GLOBAL }, { world: (w) => w.abs(GLOBAL, ad013) });
396
- const r = migrateFromGlobal(deps, DIR, { home: '/home/u', removeGlobal: true, dryRun: false });
397
- assert.equal(r.action, 'removed');
398
- assert.ok(r.backup.includes('.claude/skills/'));
399
- assert.equal(world.nodes.get(GLOBAL).content, '', 'AD-013 comment + bare line removed');
400
- });
401
- });
402
-
403
- // ── --unhide (D12) ───────────────────────────────────────────────────────────────
404
-
405
- describe('--unhide', () => {
406
- const GLOBAL = '/home/u/.gitignore_global';
407
- it('deletes the fence; default reports the residual global block; --remove-global removes it', () => {
408
- const exclude = `# boiler\n${START_MARKER}\n/AGENTS.md\n/docs/ai/\n${END_MARKER}\n`;
409
- const kitOld = '# agent-workflow-kit hidden mode (machine-local; remove these lines to un-hide)\n/AGENTS.md\n/docs/ai/\n';
410
- const { world, deps } = mk({ global: GLOBAL }, { exclude, world: (w) => w.abs(GLOBAL, kitOld) });
411
- const r = hideFootprint({ dir: DIR, unhide: true }, deps);
412
- assert.equal(r.action, 'unhidden');
413
- const content = world.read('.git/info/exclude');
414
- assert.ok(!content.includes(START_MARKER), 'fence removed');
415
- assert.ok(content.includes('# boiler'), 'user boilerplate kept');
416
- assert.equal(r.global.action, 'kept', 'residual global reported, not removed by default');
417
- assert.equal(world.nodes.get(GLOBAL).content, kitOld);
418
-
419
- const { world: w2, deps: d2 } = mk({ global: GLOBAL }, { exclude, world: (w) => w.abs(GLOBAL, kitOld) });
420
- const r2 = hideFootprint({ dir: DIR, unhide: true, removeGlobal: true }, d2);
421
- assert.equal(r2.global.action, 'removed');
422
- assert.ok(!w2.nodes.get(GLOBAL).content.includes('/AGENTS.md'));
423
- });
424
- });
425
-
426
- // ── visibility inference (D16) ───────────────────────────────────────────────────
427
-
428
- describe('inferVisibility + --reconcile', () => {
429
- it('tracked anchor → visible; --reconcile writes zero bytes', () => {
430
- const { world, deps } = mk({ tracked: ['AGENTS.md'] }, { world: (w) => w.file('AGENTS.md') });
431
- assert.equal(inferVisibility(deps, DIR).visibility, 'visible');
432
- const r = hideFootprint({ dir: DIR, reconcile: true }, deps);
433
- assert.equal(r.action, 'noop');
434
- assert.equal(r.visibility, 'visible');
435
- assert.equal(world.read('.git/info/exclude'), undefined, 'no exclude file written');
436
- });
437
-
438
- it('untracked + ignored anchor → hidden; --reconcile runs the hide', () => {
439
- const { deps } = mk(
440
- { ignored: { 'AGENTS.md': { source: '/home/u/.gitignore_global' } } },
441
- { world: (w) => w.file('AGENTS.md') },
442
- );
443
- assert.equal(inferVisibility(deps, DIR).visibility, 'hidden');
444
- const r = hideFootprint({ dir: DIR, reconcile: true }, deps);
445
- assert.equal(r.visibility, 'hidden');
446
- assert.ok(r.wrote.includes('/AGENTS.md'));
447
- });
448
-
449
- it('untracked + not ignored anchor → ambiguous; --reconcile surfaces it and writes nothing', () => {
450
- const { world, deps } = mk({}, { world: (w) => w.file('AGENTS.md') });
451
- assert.equal(inferVisibility(deps, DIR).visibility, 'ambiguous');
452
- const r = hideFootprint({ dir: DIR, reconcile: true }, deps);
453
- assert.equal(r.ambiguous, true);
454
- assert.equal(r.action, 'noop');
455
- assert.equal(world.read('.git/info/exclude'), undefined);
456
- });
457
-
458
- it('a committed-but-DELETED AGENTS.md still infers VISIBLE (tracked-ness, not disk presence)', () => {
459
- // AGENTS.md tracked but absent from the worktree; docs/ai/ present, untracked, not ignored.
460
- const { deps } = mk({ tracked: ['AGENTS.md'] }, { world: (w) => w.dir('docs/ai') });
461
- assert.equal(inferVisibility(deps, DIR).visibility, 'visible');
462
- });
463
- });