@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,346 +0,0 @@
1
- import { describe, it, beforeEach, afterEach } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import {
4
- mkdtempSync, rmSync, mkdirSync, writeFileSync, symlinkSync, readlinkSync, readFileSync, existsSync, lstatSync,
5
- } from 'node:fs';
6
- import { tmpdir } from 'node:os';
7
- import { join } from 'node:path';
8
- import {
9
- assertContainedRealPath,
10
- copyTreeRefresh,
11
- linkManaged,
12
- removeTreeManaged,
13
- unlinkManaged,
14
- MANAGED_LINK_CONFLICT,
15
- } from './fs-safe.mjs';
16
-
17
- // All three primitives are SYNC and operate on real tmp dirs here (the symlink behaviours are
18
- // fiddly enough that real fs is the honest test). The out-of-root case needs no fs at all.
19
- let dir;
20
- beforeEach(() => {
21
- dir = mkdtempSync(join(tmpdir(), 'awf-fs-safe-'));
22
- });
23
- afterEach(() => {
24
- rmSync(dir, { recursive: true, force: true });
25
- });
26
-
27
- // ── assertContainedRealPath ───────────────────────────────────────────────────
28
-
29
- describe('assertContainedRealPath', () => {
30
- it('rejects a dest outside the root (no fs needed — the relative check fires first)', () => {
31
- assert.throws(() => assertContainedRealPath('/root', '/root/../etc/passwd'), /outside/);
32
- assert.throws(() => assertContainedRealPath('/root', '/etc/passwd'), /outside/);
33
- });
34
-
35
- it('Issue-004: accepts a contained child literally named "..foo", still rejects a true ".." segment', () => {
36
- // rel "..foo" is a real child, NOT a "../" escape — the old `rel.startsWith('..')` wrongly rejected it.
37
- assert.doesNotThrow(() => assertContainedRealPath(dir, join(dir, '..foo')));
38
- assert.throws(() => assertContainedRealPath(dir, join(dir, '..', 'escape')), /outside/);
39
- });
40
-
41
- it('rejects writing INTO a symlinked root', () => {
42
- const real = join(dir, 'real');
43
- const root = join(dir, 'root');
44
- mkdirSync(real);
45
- symlinkSync(real, root);
46
- assert.throws(() => assertContainedRealPath(root, join(root, 'x')), /symlink/i);
47
- });
48
-
49
- it('rejects writing THROUGH a symlinked intermediate component', () => {
50
- const root = join(dir, 'root');
51
- const elsewhere = join(dir, 'elsewhere');
52
- mkdirSync(root);
53
- mkdirSync(elsewhere);
54
- symlinkSync(elsewhere, join(root, 'sub'));
55
- assert.throws(() => assertContainedRealPath(root, join(root, 'sub', 'file')), /symlink/i);
56
- });
57
-
58
- it('rejects a symlinked leaf dest', () => {
59
- const root = join(dir, 'root');
60
- mkdirSync(root);
61
- symlinkSync(join(dir, 'target'), join(root, 'leaf'));
62
- assert.throws(() => assertContainedRealPath(root, join(root, 'leaf')), /symlink/i);
63
- });
64
-
65
- it('accepts a clean dest within the root', () => {
66
- const root = join(dir, 'root');
67
- mkdirSync(root);
68
- assert.doesNotThrow(() => assertContainedRealPath(root, join(root, 'a', 'b', 'c')));
69
- });
70
-
71
- it('lstat is injectable (closes the install.mjs injectability gap)', () => {
72
- let seen = 0;
73
- const lstat = (p) => { seen += 1; return { isSymbolicLink: () => false }; };
74
- assert.doesNotThrow(() => assertContainedRealPath('/root', '/root/a/b', { lstat }));
75
- assert.ok(seen > 0, 'the injected lstat was used');
76
- });
77
- });
78
-
79
- // ── copyTreeRefresh ───────────────────────────────────────────────────────────
80
-
81
- describe('copyTreeRefresh', () => {
82
- it('overwrites an existing regular file (refresh)', () => {
83
- const root = join(dir, 'dest');
84
- mkdirSync(root);
85
- const src = join(dir, 'src.txt');
86
- const dest = join(root, 'f.txt');
87
- writeFileSync(src, 'new');
88
- writeFileSync(dest, 'old');
89
- copyTreeRefresh(src, dest, root);
90
- assert.equal(readFileSync(dest, 'utf8'), 'new');
91
- });
92
-
93
- it('copies a nested directory tree', () => {
94
- const src = join(dir, 'src');
95
- const root = join(dir, 'dest');
96
- mkdirSync(join(src, 'a'), { recursive: true });
97
- writeFileSync(join(src, 'top.txt'), 'T');
98
- writeFileSync(join(src, 'a', 'deep.txt'), 'D');
99
- mkdirSync(root);
100
- copyTreeRefresh(src, join(root, 'src'), root);
101
- assert.equal(readFileSync(join(root, 'src', 'top.txt'), 'utf8'), 'T');
102
- assert.equal(readFileSync(join(root, 'src', 'a', 'deep.txt'), 'utf8'), 'D');
103
- });
104
-
105
- it('skips a symlink whose dest already exists (additive — never replace)', () => {
106
- const root = join(dir, 'dest');
107
- mkdirSync(root);
108
- const linkSrc = join(dir, 'link');
109
- symlinkSync(join(dir, 'whatever'), linkSrc); // src IS a symlink
110
- const dest = join(root, 'f');
111
- writeFileSync(dest, 'keep');
112
- copyTreeRefresh(linkSrc, dest, root);
113
- assert.equal(readFileSync(dest, 'utf8'), 'keep'); // untouched
114
- assert.equal(lstatSync(dest).isSymbolicLink(), false);
115
- });
116
-
117
- it('STOPs on a symlinked dest component (never writes through it)', () => {
118
- const root = join(dir, 'root');
119
- const elsewhere = join(dir, 'elsewhere');
120
- mkdirSync(root);
121
- mkdirSync(elsewhere);
122
- symlinkSync(elsewhere, join(root, 'sub'));
123
- const src = join(dir, 's.txt');
124
- writeFileSync(src, 'x');
125
- assert.throws(() => copyTreeRefresh(src, join(root, 'sub', 'f.txt'), root), /symlink/i);
126
- assert.equal(existsSync(join(elsewhere, 'f.txt')), false); // no leak
127
- });
128
- });
129
-
130
- // ── linkManaged ───────────────────────────────────────────────────────────────
131
-
132
- describe('linkManaged', () => {
133
- const makeSrc = () => {
134
- const src = join(dir, 'src.sh');
135
- writeFileSync(src, '#!/bin/sh\n');
136
- return src;
137
- };
138
-
139
- it('creates a symlink when the dest is absent', () => {
140
- const src = makeSrc();
141
- const root = join(dir, 'bin');
142
- mkdirSync(root);
143
- const dest = join(root, 'cmd');
144
- const result = linkManaged(src, dest, root);
145
- assert.equal(result, 'linked');
146
- assert.equal(lstatSync(dest).isSymbolicLink(), true);
147
- assert.equal(readlinkSync(dest), src);
148
- });
149
-
150
- it('creates the parent bindir if absent (mkdir -p)', () => {
151
- const src = makeSrc();
152
- const root = join(dir, 'base');
153
- mkdirSync(root);
154
- const dest = join(root, 'newbin', 'cmd');
155
- linkManaged(src, dest, root);
156
- assert.equal(readlinkSync(dest), src);
157
- });
158
-
159
- it('is idempotent — a second call is a no-op', () => {
160
- const src = makeSrc();
161
- const root = join(dir, 'bin');
162
- mkdirSync(root);
163
- const dest = join(root, 'cmd');
164
- linkManaged(src, dest, root);
165
- const again = linkManaged(src, dest, root);
166
- assert.equal(again, 'noop');
167
- assert.equal(readlinkSync(dest), src);
168
- });
169
-
170
- it('STOPs on a non-symlink dest (typed ManagedLinkConflict)', () => {
171
- const src = makeSrc();
172
- const root = join(dir, 'bin');
173
- mkdirSync(root);
174
- const dest = join(root, 'cmd');
175
- writeFileSync(dest, 'someone-elses-file');
176
- assert.throws(() => linkManaged(src, dest, root), (err) => err.code === MANAGED_LINK_CONFLICT);
177
- assert.equal(readFileSync(dest, 'utf8'), 'someone-elses-file'); // untouched
178
- });
179
-
180
- it('STOPs on a foreign symlink (points elsewhere)', () => {
181
- const src = makeSrc();
182
- const root = join(dir, 'bin');
183
- mkdirSync(root);
184
- const dest = join(root, 'cmd');
185
- const foreign = join(dir, 'foreign.sh');
186
- writeFileSync(foreign, '#!/bin/sh\n');
187
- symlinkSync(foreign, dest);
188
- assert.throws(() => linkManaged(src, dest, root), (err) => err.code === MANAGED_LINK_CONFLICT);
189
- assert.equal(readlinkSync(dest), foreign); // untouched
190
- });
191
-
192
- it('refuses a symlinked source (never links through a symlink)', () => {
193
- const realSrc = makeSrc();
194
- const linkSrc = join(dir, 'link.sh');
195
- symlinkSync(realSrc, linkSrc);
196
- const root = join(dir, 'bin');
197
- mkdirSync(root);
198
- assert.throws(() => linkManaged(linkSrc, join(root, 'cmd'), root), /symlink/i);
199
- });
200
-
201
- it('refuses a non-regular-file source (e.g. a directory)', () => {
202
- const srcDir = join(dir, 'src-dir');
203
- mkdirSync(srcDir);
204
- const root = join(dir, 'bin');
205
- mkdirSync(root);
206
- assert.throws(() => linkManaged(srcDir, join(root, 'cmd'), root), /regular file/i);
207
- });
208
- });
209
-
210
- // ── removeTreeManaged ───────────────────────────────────────────────────────────
211
-
212
- describe('removeTreeManaged', () => {
213
- it('recursively removes a managed dir tree', () => {
214
- const root = join(dir, 'skills');
215
- const skill = join(root, 'agent-workflow-kit');
216
- mkdirSync(join(skill, 'tools'), { recursive: true });
217
- writeFileSync(join(skill, 'SKILL.md'), 'x');
218
- writeFileSync(join(skill, 'tools', 'a.mjs'), 'y');
219
- const result = removeTreeManaged(skill, root);
220
- assert.equal(result, 'removed');
221
- assert.equal(existsSync(skill), false);
222
- assert.equal(existsSync(root), true); // only the target went, not the parent
223
- });
224
-
225
- it('is a no-op when the target is already absent', () => {
226
- const root = join(dir, 'skills');
227
- mkdirSync(root);
228
- assert.equal(removeTreeManaged(join(root, 'gone'), root), 'noop');
229
- });
230
-
231
- it('STOPs on a symlinked target (never follows + deletes through it)', () => {
232
- const root = join(dir, 'skills');
233
- const real = join(dir, 'real-skill');
234
- mkdirSync(root);
235
- mkdirSync(real);
236
- writeFileSync(join(real, 'keep.txt'), 'keep');
237
- symlinkSync(real, join(root, 'agent-workflow-kit')); // the skill dir is a symlink
238
- assert.throws(() => removeTreeManaged(join(root, 'agent-workflow-kit'), root), /symlink/i);
239
- assert.equal(existsSync(join(real, 'keep.txt')), true); // the target it pointed at is untouched
240
- });
241
-
242
- it('removes a symlink ENTRY inside the tree without touching what it points at', () => {
243
- const root = join(dir, 'skills');
244
- const skill = join(root, 'agent-workflow-kit');
245
- const outside = join(dir, 'outside');
246
- mkdirSync(skill, { recursive: true });
247
- mkdirSync(outside);
248
- writeFileSync(join(outside, 'precious.txt'), 'precious');
249
- symlinkSync(outside, join(skill, 'link-to-outside')); // an internal symlink
250
- removeTreeManaged(skill, root);
251
- assert.equal(existsSync(skill), false);
252
- assert.equal(existsSync(join(outside, 'precious.txt')), true); // never recursed through the link
253
- });
254
-
255
- it('refuses a target outside the root', () => {
256
- const root = join(dir, 'skills');
257
- mkdirSync(root);
258
- assert.throws(() => removeTreeManaged(join(dir, 'elsewhere'), root), /outside/);
259
- });
260
-
261
- it('rm is injectable (no real deletion when injected)', () => {
262
- const root = join(dir, 'skills');
263
- const skill = join(root, 'agent-workflow-kit');
264
- mkdirSync(skill, { recursive: true });
265
- let removed = null;
266
- const result = removeTreeManaged(skill, root, { rm: (p) => { removed = p; } });
267
- assert.equal(result, 'removed');
268
- assert.equal(removed, skill);
269
- assert.equal(existsSync(skill), true); // the injected rm did nothing
270
- });
271
- });
272
-
273
- // ── unlinkManaged ───────────────────────────────────────────────────────────────
274
-
275
- describe('unlinkManaged', () => {
276
- const makeSrc = () => {
277
- const src = join(dir, 'src.sh');
278
- writeFileSync(src, '#!/bin/sh\n');
279
- return src;
280
- };
281
-
282
- it('unlinks a symlink that points at our source', () => {
283
- const src = makeSrc();
284
- const root = join(dir, 'bin');
285
- mkdirSync(root);
286
- const dest = join(root, 'cmd');
287
- symlinkSync(src, dest);
288
- const result = unlinkManaged(dest, src, root);
289
- assert.equal(result, 'unlinked');
290
- assert.equal(existsSync(dest), false);
291
- assert.equal(existsSync(src), true); // the source it pointed at is untouched
292
- });
293
-
294
- it('is a no-op when the dest is absent', () => {
295
- const src = makeSrc();
296
- const root = join(dir, 'bin');
297
- mkdirSync(root);
298
- assert.equal(unlinkManaged(join(root, 'cmd'), src, root), 'noop');
299
- });
300
-
301
- it('STOPs on a non-symlink dest (typed ManagedLinkConflict)', () => {
302
- const src = makeSrc();
303
- const root = join(dir, 'bin');
304
- mkdirSync(root);
305
- const dest = join(root, 'cmd');
306
- writeFileSync(dest, 'someone-elses-file');
307
- assert.throws(() => unlinkManaged(dest, src, root), (err) => err.code === MANAGED_LINK_CONFLICT);
308
- assert.equal(readFileSync(dest, 'utf8'), 'someone-elses-file'); // untouched
309
- });
310
-
311
- it('STOPs on a foreign symlink (points elsewhere)', () => {
312
- const src = makeSrc();
313
- const root = join(dir, 'bin');
314
- mkdirSync(root);
315
- const dest = join(root, 'cmd');
316
- const foreign = join(dir, 'foreign.sh');
317
- writeFileSync(foreign, '#!/bin/sh\n');
318
- symlinkSync(foreign, dest);
319
- assert.throws(() => unlinkManaged(dest, src, root), (err) => err.code === MANAGED_LINK_CONFLICT);
320
- assert.equal(readlinkSync(dest), foreign); // untouched
321
- });
322
-
323
- it('removes a dangling symlink that still textually points at our source', () => {
324
- const src = join(dir, 'src.sh'); // never created → the link is dangling
325
- const root = join(dir, 'bin');
326
- mkdirSync(root);
327
- const dest = join(root, 'cmd');
328
- symlinkSync(src, dest);
329
- assert.equal(unlinkManaged(dest, src, root), 'unlinked');
330
- assert.equal(lstatSync(root).isDirectory(), true);
331
- assert.equal(existsSync(dest), false);
332
- });
333
-
334
- it('unlink is injectable', () => {
335
- const src = makeSrc();
336
- const root = join(dir, 'bin');
337
- mkdirSync(root);
338
- const dest = join(root, 'cmd');
339
- symlinkSync(src, dest);
340
- let unlinked = null;
341
- const result = unlinkManaged(dest, src, root, { unlink: (p) => { unlinked = p; } });
342
- assert.equal(result, 'unlinked');
343
- assert.equal(unlinked, dest);
344
- assert.equal(existsSync(dest), true); // the injected unlink did nothing
345
- });
346
- });
@@ -1,168 +0,0 @@
1
- // Integration acceptance for hide-footprint against a REAL `git` — what injected-git mocks cannot
2
- // prove: real `git rev-parse --git-path info/exclude` (incl. a linked worktree), real precedence
3
- // (tracked .gitignore > .git/info/exclude > global core.excludesFile), and the delegated-memory
4
- // hand-off (fresh + stale). Every test ISOLATES the git environment (per-test HOME + GIT_CONFIG_GLOBAL
5
- // + GIT_CONFIG_NOSYSTEM) so the host's real ~/.gitignore_global — which already hides /AGENTS.md,
6
- // /docs/ai/ — cannot make a real check-ignore pass/fail for host-state reasons (Review fold R2 codex#8).
7
-
8
- import { describe, it, before, beforeEach, afterEach } from 'node:test';
9
- import assert from 'node:assert/strict';
10
- import { mkdtempSync, rmSync, writeFileSync, mkdirSync, readFileSync, existsSync } from 'node:fs';
11
- import { tmpdir } from 'node:os';
12
- import { join } from 'node:path';
13
- import { execFileSync } from 'node:child_process';
14
- import { hideFootprint, excludePath, START_MARKER } from './hide-footprint.mjs';
15
-
16
- let gitOk = true;
17
- before(() => {
18
- try { execFileSync('git', ['--version'], { encoding: 'utf8' }); } catch { gitOk = false; }
19
- });
20
-
21
- const made = [];
22
- const mkdtemp = (tag) => { const d = mkdtempSync(join(tmpdir(), tag)); made.push(d); return d; };
23
- afterEach(() => { while (made.length) { try { rmSync(made.pop(), { recursive: true, force: true }); } catch { /* best effort */ } } });
24
-
25
- // An isolated repo: empty global config (no host excludesFile), committer identity in env.
26
- const setup = () => {
27
- const home = mkdtemp('aw-home-');
28
- const dir = mkdtemp('aw-repo-');
29
- const gcfg = join(home, '.gitconfig');
30
- writeFileSync(gcfg, '');
31
- const env = {
32
- ...process.env, HOME: home, GIT_CONFIG_GLOBAL: gcfg, GIT_CONFIG_NOSYSTEM: '1',
33
- GIT_AUTHOR_NAME: 'T', GIT_AUTHOR_EMAIL: 't@e', GIT_COMMITTER_NAME: 'T', GIT_COMMITTER_EMAIL: 't@e',
34
- };
35
- const git = (args, cwd = dir) => execFileSync('git', args, { cwd, env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
36
- git(['init', '-q', '-b', 'main']);
37
- return { home, dir, gcfg, env, git, deps: { env, home } };
38
- };
39
- const readExclude = (dir) => { const p = join(dir, '.git/info/exclude'); return existsSync(p) ? readFileSync(p, 'utf8') : ''; };
40
- const count = (s, sub) => s.split(sub).length - 1;
41
- const checkIgnoreSource = (git, dir, probe) => {
42
- try { return git(['check-ignore', '-v', '--', probe], dir).split('\t')[0]; } catch { return null; }
43
- };
44
-
45
- describe('hide-footprint integration (real git)', { skip: !gitOk }, () => {
46
- let env;
47
- beforeEach(() => { env = setup(); });
48
-
49
- it('resolves info/exclude, writes ONE managed block, and real check-ignore reports it', () => {
50
- const { dir, git, deps } = env;
51
- writeFileSync(join(dir, 'AGENTS.md'), '# entry\n');
52
- const ep = excludePath(deps, dir);
53
- assert.ok(ep.endsWith('.git/info/exclude'));
54
- const r = hideFootprint({ dir }, deps);
55
- assert.equal(r.action, 'created');
56
- const content = readExclude(dir);
57
- assert.equal(count(content, START_MARKER), 1, 'exactly one managed block');
58
- assert.match(checkIgnoreSource(git, dir, 'AGENTS.md') ?? '', /info\/exclude/);
59
- // Idempotent: a second run changes nothing.
60
- const r2 = hideFootprint({ dir }, deps);
61
- assert.equal(r2.action, 'noop');
62
- assert.equal(readExclude(dir), content);
63
- });
64
-
65
- it('works inside a linked git worktree', () => {
66
- const { dir, git, deps, env: e } = env;
67
- git(['commit', '-q', '--allow-empty', '-m', 'init']);
68
- const wt = mkdtemp('aw-wt-');
69
- rmSync(wt, { recursive: true, force: true }); // git worktree add wants a non-existent path
70
- git(['worktree', 'add', '-q', wt]);
71
- writeFileSync(join(wt, 'AGENTS.md'), '# entry\n');
72
- const r = hideFootprint({ dir: wt }, deps);
73
- assert.equal(r.visibility, 'hidden');
74
- assert.equal(count(r.wrote.join('\n'), '/AGENTS.md'), 1);
75
- assert.match(checkIgnoreSource((a, c) => execFileSync('git', a, { cwd: c, env: e, encoding: 'utf8' }), wt, 'AGENTS.md') ?? '', /exclude/);
76
- });
77
-
78
- it('precedence: a path in a TRACKED .gitignore is dropped (redundant), reported by .gitignore', () => {
79
- const { dir, git, deps } = env;
80
- writeFileSync(join(dir, '.gitignore'), '/docs/ai/\n');
81
- git(['add', '.gitignore']);
82
- const r = hideFootprint({ dir }, deps);
83
- assert.ok(r.dropped.includes('/docs/ai/'), 'covered by tracked .gitignore → dropped');
84
- assert.ok(!r.wrote.includes('/docs/ai/'));
85
- assert.match(checkIgnoreSource(git, dir, 'docs/ai/') ?? '', /\.gitignore/);
86
- });
87
-
88
- it('precedence: a path in BOTH global excludes and the local block is reported by the LOCAL block', () => {
89
- const { dir, git, deps } = env;
90
- const globalExcludes = join(env.home, 'gitignore_global');
91
- writeFileSync(globalExcludes, '/AGENTS.md\n');
92
- git(['config', 'core.excludesFile', globalExcludes]);
93
- writeFileSync(join(dir, 'AGENTS.md'), '# entry\n');
94
- const r = hideFootprint({ dir }, deps); // default keeps the global block
95
- assert.ok(r.wrote.includes('/AGENTS.md'));
96
- assert.match(checkIgnoreSource(git, dir, 'AGENTS.md') ?? '', /info\/exclude/, 'local exclude wins precedence');
97
- assert.equal(r.global.action, 'kept');
98
- });
99
-
100
- it('a machine-global core.excludesFile NAMED .gitignore is not mistaken for a project .gitignore (no spurious STOP)', () => {
101
- const { dir, git, deps } = env;
102
- const globalGitignore = join(env.home, '.gitignore'); // basename collides with a project .gitignore
103
- writeFileSync(globalGitignore, '/AGENTS.md\n');
104
- git(['config', 'core.excludesFile', globalGitignore]);
105
- writeFileSync(join(dir, 'AGENTS.md'), '# entry\n');
106
- const r = hideFootprint({ dir }, deps); // must not STOP probing an outside-repo path
107
- assert.ok(r.wrote.includes('/AGENTS.md'), 'AGENTS.md hidden project-local, not dropped as a "tracked .gitignore"');
108
- assert.match(checkIgnoreSource(git, dir, 'AGENTS.md') ?? '', /info\/exclude/);
109
- });
110
-
111
- it('delegated-hidden, FRESH memory: bare project-local lines are absorbed into ONE canonical fence', () => {
112
- const { dir, deps } = env;
113
- // What a 1.1.0 memory writes project-local (bare, no fence):
114
- writeFileSync(join(dir, '.git/info/exclude'), '/AGENTS.md\n/CLAUDE.md\n/docs/ai/\n');
115
- const r = hideFootprint({ dir }, deps);
116
- const content = readExclude(dir);
117
- assert.equal(count(content, START_MARKER), 1, 'one fence');
118
- assert.equal(count(content, '/AGENTS.md\n'), 1, 'no duplicate AGENTS.md rule');
119
- // the bare lines are now INSIDE the fence, not loose above it.
120
- assert.ok(content.indexOf('/AGENTS.md') > content.indexOf(START_MARKER));
121
- assert.ok(r.wrote.includes('/AGENTS.md') && r.wrote.includes('/docs/ai/'));
122
- });
123
-
124
- it('delegated-hidden, STALE memory: a memory-old GLOBAL block is migrated away with --remove-global', () => {
125
- const { dir, git, deps } = env;
126
- const globalExcludes = join(env.home, 'gitignore_global');
127
- writeFileSync(globalExcludes, '# agent-workflow-kit hidden mode (machine-local; remove these lines to un-hide)\n/AGENTS.md\n/docs/ai/\n/docs/ai/.memory-version\n');
128
- git(['config', 'core.excludesFile', globalExcludes]);
129
- writeFileSync(join(dir, 'AGENTS.md'), '# entry\n');
130
- const r = hideFootprint({ dir, removeGlobal: true }, deps);
131
- assert.equal(r.global.action, 'removed');
132
- assert.ok(!readFileSync(globalExcludes, 'utf8').includes('/AGENTS.md'), 'global block removed');
133
- assert.match(checkIgnoreSource(git, dir, 'AGENTS.md') ?? '', /info\/exclude/, 'now hidden project-local');
134
- });
135
-
136
- describe('visibility inference (D16)', () => {
137
- it('tracked anchor → VISIBLE → --reconcile writes zero bytes', () => {
138
- const { dir, git, deps } = env;
139
- writeFileSync(join(dir, 'AGENTS.md'), '# entry\n');
140
- git(['add', 'AGENTS.md']);
141
- const before = readExclude(dir);
142
- const r = hideFootprint({ dir, reconcile: true }, deps);
143
- assert.equal(r.visibility, 'visible');
144
- assert.equal(readExclude(dir), before, 'no bytes written in visible mode');
145
- });
146
-
147
- it('untracked + ignored anchor → HIDDEN → --reconcile runs the hide', () => {
148
- const { dir, git, deps } = env;
149
- const globalExcludes = join(env.home, 'gitignore_global');
150
- writeFileSync(globalExcludes, '/AGENTS.md\n');
151
- git(['config', 'core.excludesFile', globalExcludes]);
152
- writeFileSync(join(dir, 'AGENTS.md'), '# entry\n');
153
- const r = hideFootprint({ dir, reconcile: true }, deps);
154
- assert.equal(r.visibility, 'hidden');
155
- assert.ok(r.wrote.includes('/AGENTS.md'));
156
- });
157
-
158
- it('untracked + not ignored anchor → AMBIGUOUS → --reconcile surfaces it, writes nothing', () => {
159
- const { dir, deps } = env;
160
- writeFileSync(join(dir, 'AGENTS.md'), '# entry\n');
161
- const before = readExclude(dir);
162
- const r = hideFootprint({ dir, reconcile: true }, deps);
163
- assert.equal(r.ambiguous, true);
164
- assert.equal(r.visibility, 'ambiguous');
165
- assert.equal(readExclude(dir), before, 'ambiguous → no write, agent ASKs');
166
- });
167
- });
168
- });