agentic-workflow-manager 9.22.0 → 9.22.1

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 (53) hide show
  1. package/dist/src/commands/change/index.js +2 -2
  2. package/dist/src/commands/init.js +8 -4
  3. package/dist/src/commands/registry/add.js +9 -3
  4. package/dist/src/commands/sync.js +11 -5
  5. package/dist/src/commands/update.js +23 -12
  6. package/dist/src/core/changes/derive.js +6 -3
  7. package/dist/src/core/changes/git.js +1 -2
  8. package/dist/src/core/changes/worktree.js +15 -3
  9. package/dist/src/core/init/steps.js +3 -8
  10. package/dist/src/core/registries.js +20 -15
  11. package/dist/src/core/registry-git-errors.js +107 -0
  12. package/dist/src/core/registry-git.js +93 -0
  13. package/dist/src/core/registry-sync-report.js +74 -0
  14. package/dist/src/core/versioning.js +3 -6
  15. package/dist/src/index.js +5 -8
  16. package/dist/tests/commands/change/new.test.js +103 -57
  17. package/dist/tests/commands/change/validate.test.js +164 -108
  18. package/dist/tests/commands/changes/list.test.js +41 -22
  19. package/dist/tests/commands/job/gate-reconcile.test.js +13 -2
  20. package/dist/tests/commands/registry-sync-honesty.test.js +175 -0
  21. package/dist/tests/commands/sensors/exec.test.js +3 -1
  22. package/dist/tests/commands/update.test.js +8 -8
  23. package/dist/tests/commands/watch/supervisor-loop.test.js +21 -5
  24. package/dist/tests/core/changes/helpers.js +15 -3
  25. package/dist/tests/core/changes/list-paging.test.js +131 -110
  26. package/dist/tests/core/changes/list.test.js +133 -83
  27. package/dist/tests/core/changes/receipt.test.js +18 -6
  28. package/dist/tests/core/diagnostics/provider-checks.test.js +3 -0
  29. package/dist/tests/core/init/steps-registry-sync.test.js +6 -8
  30. package/dist/tests/core/registry-git.test.js +153 -0
  31. package/dist/tests/helpers/commit-seeded-registry.js +18 -0
  32. package/dist/tests/integration/codex-provider-isolated.test.js +7 -2
  33. package/dist/tests/integration/copilot-init-isolated.test.js +7 -4
  34. package/dist/tests/structural/active-documentation.test.js +0 -41
  35. package/dist/tests/structural/bundle-skill-reference-contract.test.js +0 -12
  36. package/dist/tests/structural/cli-reference-matches-commander.test.js +94 -0
  37. package/dist/tests/structural/native-release-artifacts.test.js +116 -107
  38. package/dist/tests/structural/sensor-documentation-contract.test.js +0 -47
  39. package/dist/tests/structural/sensor-portability-contract.test.js +0 -7
  40. package/dist/tests/structural/simple-git-v4-pin.test.js +49 -0
  41. package/package.json +2 -2
  42. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  43. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  44. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  45. package/prebuilds/win32-x64/secure_fs.node +0 -0
  46. package/dist/tests/structural/cloud-admission-docs-contract.test.js +0 -54
  47. package/dist/tests/structural/documentation-phase-is-mechanized.test.js +0 -29
  48. package/dist/tests/structural/operator-environments-docs-contract.test.js +0 -70
  49. package/dist/tests/structural/process-model-description-contract.test.js +0 -34
  50. package/dist/tests/structural/project-declared-sensors-docs-contract.test.js +0 -104
  51. package/dist/tests/structural/r4-plan-currentness-docs.test.js +0 -37
  52. package/dist/tests/structural/sensor-execution-optimization-docs-contract.test.js +0 -102
  53. package/dist/tests/structural/sensor-slice-decision-contract.test.js +0 -63
@@ -15,14 +15,42 @@ const change_1 = require("../../../src/commands/change");
15
15
  const types_1 = require("../../../src/core/changes/types");
16
16
  let repo;
17
17
  let template;
18
- beforeAll(() => { template = (0, helpers_1.makeRepo)(); });
19
- afterAll(() => template.cleanup());
18
+ /** The template plus `adoptIn`, and the template plus `historyIn`: prebuilt once, copied per test with no git process. */
19
+ let adopted;
20
+ let historic;
21
+ beforeAll(() => {
22
+ template = (0, helpers_1.makeRepo)();
23
+ adopted = (0, helpers_1.cloneRepo)(template);
24
+ (0, list_fixtures_1.adoptIn)(adopted);
25
+ historic = (0, helpers_1.cloneRepo)(template);
26
+ (0, list_fixtures_1.historyIn)(historic);
27
+ });
28
+ afterAll(() => { adopted.cleanup(); historic.cleanup(); template.cleanup(); });
20
29
  beforeEach(() => { repo = (0, helpers_1.cloneRepo)(template); });
21
30
  afterEach(() => { jest.restoreAllMocks(); repo.cleanup(); });
22
31
  const list = (opts = {}) => (0, list_fixtures_1.listIn)(repo, opts);
23
32
  const listed = (opts = {}) => (0, list_fixtures_1.ok)(list(opts));
24
- const adopt = (id) => (0, list_fixtures_1.adoptIn)(repo, id);
25
- const history = () => (0, list_fixtures_1.historyIn)(repo);
33
+ const startFrom = (state) => { repo.cleanup(); repo = (0, helpers_1.cloneRepo)(state); };
34
+ /** The repository after `adoptIn(repo)` (a copy of the prebuilt one). */
35
+ const adopt = () => startFrom(adopted);
36
+ /** The repository after `historyIn(repo)` (a copy of the prebuilt one). */
37
+ const history = () => startFrom(historic);
38
+ /** The history of `historyIn`, listed once under a git spy for the tests that only read that listing (the state is never changed). */
39
+ let historyListing;
40
+ function listedHistory() {
41
+ if (historyListing === undefined) {
42
+ const spy = (0, helpers_1.spyOnGit)();
43
+ try {
44
+ historyListing = { result: (0, list_fixtures_1.ok)((0, list_fixtures_1.listIn)(historic)), calls: spy.calls };
45
+ }
46
+ finally {
47
+ spy.restore();
48
+ }
49
+ }
50
+ return historyListing;
51
+ }
52
+ /** The commits `repo.commit(files, message, date)` would make, in order, written by one `git fast-import` (importCommits). */
53
+ const commitMany = (planned) => (0, helpers_1.importCommits)(repo, planned.map(([files, message, date]) => ({ files, message: `${message}\n`, date })));
26
54
  describe('listChanges: input and base (R5.1, R3.1, R3.2, R3.5)', () => {
27
55
  test('resolves the local main by default and reports it', () => {
28
56
  adopt();
@@ -95,11 +123,7 @@ describe('listChanges: input and base (R5.1, R3.1, R3.2, R3.5)', () => {
95
123
  expect(r.changes.find((c) => c.subject === 'feat: first (#10)')?.classification).toBe('recorded');
96
124
  });
97
125
  test('a base that moves during the run cannot change the answer: every git call after the pin uses commit ids', () => {
98
- history();
99
- const spy = (0, helpers_1.spyOnGit)();
100
- listed();
101
- spy.restore();
102
- const afterPin = spy.calls.filter((a) => a.includes('log') || a.includes('diff') || a.includes('ls-tree') || a.includes('for-each-ref'));
126
+ const afterPin = listedHistory().calls.filter((a) => a.includes('log') || a.includes('diff') || a.includes('ls-tree') || a.includes('for-each-ref'));
103
127
  expect(afterPin.length).toBeGreaterThan(0);
104
128
  for (const call of afterPin)
105
129
  expect(call).not.toContain('main');
@@ -111,15 +135,16 @@ describe('listChanges: adoption point (R5.4)', () => {
111
135
  expect(listed()).toMatchObject({ adopted: false, changes: [], truncated: false, omitted: 0 });
112
136
  });
113
137
  test('R5.4 a docs/changes/README.md alone does not count: the repo has not adopted Change Records', () => {
114
- repo.commit({ 'docs/changes/README.md': 'hello' }, 'docs: readme', '2026-10-16T09:00:00Z');
115
- repo.commit({ 'a.ts': '1' }, 'feat: a', '2026-10-16T10:00:00Z');
138
+ commitMany([[{ 'docs/changes/README.md': 'hello' }, 'docs: readme', '2026-10-16T09:00:00Z'], [{ 'a.ts': '1' }, 'feat: a', '2026-10-16T10:00:00Z']]);
116
139
  expect(listed()).toMatchObject({ adopted: false, changes: [], truncated: false, omitted: 0 });
117
140
  });
118
141
  test('R5.4 a README first and a record later: the adoption is the commit that added the record', () => {
119
- repo.commit({ 'docs/changes/README.md': 'hello' }, 'docs: readme', '2026-10-16T09:00:00Z');
120
- repo.commit({ 'a.ts': '1' }, 'feat: before the first record', '2026-10-16T10:00:00Z');
121
- repo.commit({ [(0, list_fixtures_1.recordPath)(list_fixtures_1.FIRST)]: (0, list_fixtures_1.record)(list_fixtures_1.FIRST) }, 'feat: first record', '2026-10-16T11:00:00Z');
122
- repo.commit({ 'b.ts': '1' }, 'fix: after', '2026-10-16T12:00:00Z');
142
+ commitMany([
143
+ [{ 'docs/changes/README.md': 'hello' }, 'docs: readme', '2026-10-16T09:00:00Z'],
144
+ [{ 'a.ts': '1' }, 'feat: before the first record', '2026-10-16T10:00:00Z'],
145
+ [{ [(0, list_fixtures_1.recordPath)(list_fixtures_1.FIRST)]: (0, list_fixtures_1.record)(list_fixtures_1.FIRST) }, 'feat: first record', '2026-10-16T11:00:00Z'],
146
+ [{ 'b.ts': '1' }, 'fix: after', '2026-10-16T12:00:00Z'],
147
+ ]);
123
148
  const r = listed();
124
149
  expect(r.adopted).toBe(true);
125
150
  expect((0, list_fixtures_1.subjects)(r)).toEqual(['fix: after', 'feat: first record']);
@@ -130,8 +155,7 @@ describe('listChanges: adoption point (R5.4)', () => {
130
155
  ['a non-markdown file', 'docs/changes/2026-10-10-x.txt'],
131
156
  ['a record-shaped name outside docs/changes', 'docs/other/2026-10-10-x.md'],
132
157
  ])('R5.4 %s does not mark the adoption', (_label, file) => {
133
- repo.commit({ [file]: 'x' }, 'docs: not a record', '2026-10-16T09:00:00Z');
134
- repo.commit({ 'a.ts': '1' }, 'feat: a', '2026-10-16T10:00:00Z');
158
+ commitMany([[{ [file]: 'x' }, 'docs: not a record', '2026-10-16T09:00:00Z'], [{ 'a.ts': '1' }, 'feat: a', '2026-10-16T10:00:00Z']]);
135
159
  expect(listed()).toMatchObject({ adopted: false, changes: [] });
136
160
  });
137
161
  test('a record that was added and later deleted still marks the adoption', () => {
@@ -141,15 +165,17 @@ describe('listChanges: adoption point (R5.4)', () => {
141
165
  expect(listed().adopted).toBe(true);
142
166
  });
143
167
  test('commits older than the first one that added a file under docs/changes are omitted; the adopting commit itself is kept', () => {
144
- history();
145
- expect((0, list_fixtures_1.subjects)(listed())).not.toContain('feat: before adoption');
146
- expect((0, list_fixtures_1.subjects)(listed())).not.toContain('chore: init');
147
- expect((0, list_fixtures_1.subjects)(listed())).toContain('feat: first (#10)');
168
+ const { result } = listedHistory();
169
+ expect((0, list_fixtures_1.subjects)(result)).not.toContain('feat: before adoption');
170
+ expect((0, list_fixtures_1.subjects)(result)).not.toContain('chore: init');
171
+ expect((0, list_fixtures_1.subjects)(result)).toContain('feat: first (#10)');
148
172
  });
149
173
  test('modifying a file under docs/changes is not an adoption: only the commit that added the first file is', () => {
150
- repo.commit({ 'a.ts': '1' }, 'feat: before', '2026-10-16T09:00:00Z');
151
- repo.commit({ [(0, list_fixtures_1.recordPath)(list_fixtures_1.FIRST)]: (0, list_fixtures_1.record)(list_fixtures_1.FIRST) }, 'docs: first file', '2026-10-16T10:00:00Z');
152
- repo.commit({ [(0, list_fixtures_1.recordPath)(list_fixtures_1.FIRST)]: (0, list_fixtures_1.record)(list_fixtures_1.FIRST, { summary: 'Edit.' }) }, 'docs: edit', '2026-10-16T11:00:00Z');
174
+ commitMany([
175
+ [{ 'a.ts': '1' }, 'feat: before', '2026-10-16T09:00:00Z'],
176
+ [{ [(0, list_fixtures_1.recordPath)(list_fixtures_1.FIRST)]: (0, list_fixtures_1.record)(list_fixtures_1.FIRST) }, 'docs: first file', '2026-10-16T10:00:00Z'],
177
+ [{ [(0, list_fixtures_1.recordPath)(list_fixtures_1.FIRST)]: (0, list_fixtures_1.record)(list_fixtures_1.FIRST, { summary: 'Edit.' }) }, 'docs: edit', '2026-10-16T11:00:00Z'],
178
+ ]);
153
179
  expect((0, list_fixtures_1.subjects)(listed())).toEqual(['docs: edit', 'docs: first file']);
154
180
  });
155
181
  test('an adopted repo whose window holds no commit lists nothing', () => {
@@ -180,24 +206,20 @@ describe('listChanges: adoption point (R5.4)', () => {
180
206
  });
181
207
  describe('listChanges: classification (R5.3)', () => {
182
208
  test('recorded, missing, exempt and invalid-record', () => {
183
- history();
184
- expect((0, list_fixtures_1.byClass)(listed())).toEqual({ 'feat: first (#10)': 'recorded', 'fix: no record': 'missing', 'chore(release): v1.0.0 [skip ci]': 'exempt', 'feat: bad record': 'invalid-record' });
209
+ expect((0, list_fixtures_1.byClass)(listedHistory().result)).toEqual({ 'feat: first (#10)': 'recorded', 'fix: no record': 'missing', 'chore(release): v1.0.0 [skip ci]': 'exempt', 'feat: bad record': 'invalid-record' });
185
210
  });
186
211
  test('entries come newest first, in first-parent order', () => {
187
- history();
188
- expect((0, list_fixtures_1.subjects)(listed())).toEqual(['feat: bad record', 'chore(release): v1.0.0 [skip ci]', 'fix: no record', 'feat: first (#10)']);
212
+ expect((0, list_fixtures_1.subjects)(listedHistory().result)).toEqual(['feat: bad record', 'chore(release): v1.0.0 [skip ci]', 'fix: no record', 'feat: first (#10)']);
189
213
  });
190
214
  test('a recorded entry holds the path and the full frontmatter, nothing else', () => {
191
- history();
192
- const first = listed().changes.find((c) => c.subject === 'feat: first (#10)');
215
+ const first = listedHistory().result.changes.find((c) => c.subject === 'feat: first (#10)');
193
216
  expect(first?.records).toHaveLength(1);
194
217
  expect(first?.records[0]).toMatchObject({ path: (0, list_fixtures_1.recordPath)(list_fixtures_1.FIRST), schema: 'awm.change-record/v1', id: list_fixtures_1.FIRST, title: `T ${list_fixtures_1.FIRST}`, summary_short: 'Primer cambio.', type: 'feat', modules: ['cli'] });
195
218
  expect(first?.records[0]).not.toHaveProperty('diagnostics');
196
219
  expect(first?.records[0]).toHaveProperty('verification.overall', 'pass');
197
220
  });
198
221
  test('an invalid-record entry holds the path and its diagnostics, no frontmatter', () => {
199
- history();
200
- const bad = listed().changes.find((c) => c.subject === 'feat: bad record');
222
+ const bad = listedHistory().result.changes.find((c) => c.subject === 'feat: bad record');
201
223
  expect(bad?.records).toHaveLength(1);
202
224
  expect(bad?.records[0]).toMatchObject({ path: (0, list_fixtures_1.recordPath)('2026-10-17-bad') });
203
225
  expect(bad?.records[0]).not.toHaveProperty('schema');
@@ -258,8 +280,7 @@ describe('listChanges: classification (R5.3)', () => {
258
280
  expect(entry.records[0].diagnostics[0].message).toMatch(/not a regular file/);
259
281
  });
260
282
  test('exempt and missing entries have no records', () => {
261
- history();
262
- const r = listed();
283
+ const r = listedHistory().result;
263
284
  expect(r.changes.find((c) => c.classification === 'exempt')?.records).toEqual([]);
264
285
  expect(r.changes.find((c) => c.classification === 'missing')?.records).toEqual([]);
265
286
  });
@@ -275,8 +296,7 @@ describe('listChanges: classification (R5.3)', () => {
275
296
  });
276
297
  test('the opt-out must be a trailer of the final paragraph: a mention in the subject or the body does not exempt', () => {
277
298
  adopt();
278
- repo.commit({ 'a.ts': '1' }, 'feat: Change-Record: none', '2026-10-16T20:00:00Z');
279
- repo.commit({ 'b.ts': '1' }, 'feat: b\n\nChange-Record: none\n\nand more text', '2026-10-16T21:00:00Z');
299
+ commitMany([[{ 'a.ts': '1' }, 'feat: Change-Record: none', '2026-10-16T20:00:00Z'], [{ 'b.ts': '1' }, 'feat: b\n\nChange-Record: none\n\nand more text', '2026-10-16T21:00:00Z']]);
280
300
  expect((0, list_fixtures_1.byClass)(listed())).toEqual({ 'feat: Change-Record: none': 'missing', 'feat: b': 'missing' });
281
301
  });
282
302
  test('a commit adding two records, one of them invalid, is invalid-record and lists both', () => {
@@ -444,8 +464,7 @@ describe('listChanges: size and paths of what git prints (R5.7, R1.8)', () => {
444
464
  });
445
465
  test('a failing ls-tree makes the entry invalid-record with git-failed, and the others are still read', () => {
446
466
  adopt();
447
- repo.commit({ [(0, list_fixtures_1.recordPath)('2026-10-16-a')]: (0, list_fixtures_1.record)('2026-10-16-a') }, 'feat: a', '2026-10-16T10:00:00Z');
448
- repo.commit({ [(0, list_fixtures_1.recordPath)('2026-10-16-b')]: (0, list_fixtures_1.record)('2026-10-16-b') }, 'feat: b', '2026-10-16T11:00:00Z');
467
+ commitMany([[{ [(0, list_fixtures_1.recordPath)('2026-10-16-a')]: (0, list_fixtures_1.record)('2026-10-16-a') }, 'feat: a', '2026-10-16T10:00:00Z'], [{ [(0, list_fixtures_1.recordPath)('2026-10-16-b')]: (0, list_fixtures_1.record)('2026-10-16-b') }, 'feat: b', '2026-10-16T11:00:00Z']]);
449
468
  let fails = 1;
450
469
  const spy = (0, helpers_1.spyOnGit)((args) => { if (args.includes('ls-tree') && fails-- > 0)
451
470
  throw new Error('boom'); });
@@ -456,8 +475,7 @@ describe('listChanges: size and paths of what git prints (R5.7, R1.8)', () => {
456
475
  });
457
476
  test('a failing read of one blob makes that record git-failed only', () => {
458
477
  adopt();
459
- repo.commit({ [(0, list_fixtures_1.recordPath)('2026-10-16-a')]: (0, list_fixtures_1.record)('2026-10-16-a') }, 'feat: a', '2026-10-16T10:00:00Z');
460
- repo.commit({ [(0, list_fixtures_1.recordPath)('2026-10-16-b')]: (0, list_fixtures_1.record)('2026-10-16-b') }, 'feat: b', '2026-10-16T11:00:00Z');
478
+ commitMany([[{ [(0, list_fixtures_1.recordPath)('2026-10-16-a')]: (0, list_fixtures_1.record)('2026-10-16-a') }, 'feat: a', '2026-10-16T10:00:00Z'], [{ [(0, list_fixtures_1.recordPath)('2026-10-16-b')]: (0, list_fixtures_1.record)('2026-10-16-b') }, 'feat: b', '2026-10-16T11:00:00Z']]);
461
479
  let fails = 1;
462
480
  const spy = (0, helpers_1.spyOnGit)((args) => { if (args.includes('show') && fails-- > 0)
463
481
  throw new Error('boom'); });
@@ -510,8 +528,7 @@ describe('listChanges: derived fields (R5.5)', () => {
510
528
  });
511
529
  test('a tag on a later commit contains the earlier ones', () => {
512
530
  adopt();
513
- repo.commit({ 'a.ts': '1' }, 'fix: a', '2026-10-16T10:00:00Z');
514
- repo.commit({ 'b.ts': '1' }, 'fix: b', '2026-10-16T11:00:00Z');
531
+ commitMany([[{ 'a.ts': '1' }, 'fix: a', '2026-10-16T10:00:00Z'], [{ 'b.ts': '1' }, 'fix: b', '2026-10-16T11:00:00Z']]);
515
532
  repo.gitAt('2026-10-16T12:00:00Z', 'tag', '-a', 'v2.0.0', '-m', 'v2.0.0');
516
533
  expect(listed().changes.map((c) => c.release)).toEqual(['v2.0.0', 'v2.0.0']);
517
534
  });
@@ -558,26 +575,41 @@ describe('listChanges: derived fields (R5.5)', () => {
558
575
  });
559
576
  describe('R5.5 the tag lookup is bounded: only the 500 earliest-dated containing tags are examined per entry', () => {
560
577
  /** Lightweight tags pointing at `sha`, created with one fast-import process. */
561
- const lightTags = (sha, names) => {
562
- repo.gitInput(names.map((n) => `reset refs/tags/${n}\nfrom ${sha}\n`).join('\n'), 'fast-import', '--quiet');
578
+ const lightTags = (sha, names, into = repo) => {
579
+ into.gitInput(names.map((n) => `reset refs/tags/${n}\nfrom ${sha}\n`).join('\n'), 'fast-import', '--quiet');
563
580
  };
564
581
  const NAMES = Array.from({ length: 600 }, (_, i) => `t${String(i).padStart(4, '0')}`);
582
+ // One fix commit carrying the 600 lightweight tags, listed once under a git spy: the next two tests only read that listing.
583
+ let shared;
584
+ afterAll(() => shared?.state.cleanup());
585
+ function sharedTags() {
586
+ if (shared === undefined) {
587
+ const state = (0, helpers_1.cloneRepo)(adopted);
588
+ try {
589
+ state.commit({ 'a.ts': '1' }, 'fix: a', '2026-10-16T10:00:00Z');
590
+ lightTags(state.git('rev-parse', 'HEAD'), NAMES, state);
591
+ const spy = (0, helpers_1.spyOnGit)();
592
+ try {
593
+ shared = { state, result: (0, list_fixtures_1.ok)((0, list_fixtures_1.listIn)(state)), calls: spy.calls };
594
+ }
595
+ finally {
596
+ spy.restore();
597
+ }
598
+ }
599
+ catch (error) {
600
+ state.cleanup();
601
+ throw error;
602
+ }
603
+ }
604
+ return shared;
605
+ }
565
606
  test('the for-each-ref call asks for at most 500 tags', () => {
566
- adopt();
567
- repo.commit({ 'a.ts': '1' }, 'fix: a', '2026-10-16T10:00:00Z');
568
- lightTags(repo.git('rev-parse', 'HEAD'), NAMES);
569
- const spy = (0, helpers_1.spyOnGit)();
570
- listed();
571
- spy.restore();
572
- const lookups = spy.calls.filter((a) => a.includes('for-each-ref') && a.includes('refs/tags/')); // the tag lookup, not the base's
607
+ const lookups = sharedTags().calls.filter((a) => a.includes('for-each-ref') && a.includes('refs/tags/')); // the tag lookup, not the base's
573
608
  expect(lookups).toHaveLength(1);
574
609
  expect(lookups[0]).toContain('--count=500');
575
610
  });
576
611
  test('600 lightweight tags on the commit: the earliest by date then name is the release', () => {
577
- adopt();
578
- repo.commit({ 'a.ts': '1' }, 'fix: a', '2026-10-16T10:00:00Z');
579
- lightTags(repo.git('rev-parse', 'HEAD'), NAMES);
580
- expect(listed().changes[0].release).toBe('t0000');
612
+ expect(sharedTags().result.changes[0].release).toBe('t0000');
581
613
  });
582
614
  test('the earliest annotated tag among the first 500 still wins over 600 lightweight tags', () => {
583
615
  adopt();
@@ -711,8 +743,7 @@ describe('listChanges: shallow clones and the default base (R5.1)', () => {
711
743
  }
712
744
  test('a shallow clone fails with shallow-history and the remedy', () => {
713
745
  adopt();
714
- repo.commit({ 'a.ts': '1' }, 'fix: a', '2026-10-16T10:00:00Z');
715
- repo.commit({ 'b.ts': '1' }, 'fix: b', '2026-10-16T11:00:00Z');
746
+ commitMany([[{ 'a.ts': '1' }, 'fix: a', '2026-10-16T10:00:00Z'], [{ 'b.ts': '1' }, 'fix: b', '2026-10-16T11:00:00Z']]);
716
747
  const dest = clone(1);
717
748
  expect(repo.git('-C', dest, 'rev-parse', '--is-shallow-repository')).toBe('true');
718
749
  expect((0, list_1.listChanges)(dest, { since: list_fixtures_1.SINCE_48H })).toMatchObject({ ok: false, failure: { code: 'shallow-history', remedy: 'git fetch --unshallow' } });
@@ -751,46 +782,66 @@ describe('listChanges: shallow clones and the default base (R5.1)', () => {
751
782
  });
752
783
  });
753
784
  describe('listChanges: hostile text from git is escaped and clipped', () => {
785
+ // The subjects of the next six tests are commits of ONE history after the adoption point, listed once (each entry is found by its
786
+ // own commit date); that state is never changed. The commits are made as each test made its own: repo.commit, or git commit -m.
787
+ const SUBJECTS = {
788
+ hostile: 'feat: \u001b[31mred\u001b[0m \u202eevil\u202c \u2028 \u0085 \u007f end (#5)',
789
+ long: `feat: ${'x'.repeat(5000)}`,
790
+ atCap: `feat: ${'x'.repeat(list_1.MAX_SUBJECT - 6)}`,
791
+ surrogates: `feat: ${'x'.repeat(list_1.MAX_SUBJECT - 7)}\u{1F600}\u{1F600}`,
792
+ };
793
+ const ORDER = ['hostile', 'long', 'atCap', 'surrogates', 'controls', 'empty'];
794
+ const dateOf = (key) => new Date(Date.UTC(2026, 9, 16, 10, 0, 0) + ORDER.indexOf(key) * 60_000).toISOString();
795
+ let shared;
796
+ afterAll(() => shared?.state.cleanup());
797
+ function entry(key) {
798
+ if (shared === undefined) {
799
+ const state = (0, helpers_1.cloneRepo)(adopted);
800
+ try {
801
+ for (const k of ['hostile', 'long', 'atCap', 'surrogates'])
802
+ state.commit({ [`${k}.ts`]: '1' }, SUBJECTS[k], dateOf(k));
803
+ state.gitAt(dateOf('controls'), 'commit', '--allow-empty', '-q', '--allow-empty-message', '-m', '\u001b\u001b');
804
+ state.gitAt(dateOf('empty'), 'commit', '--allow-empty', '-q', '--allow-empty-message', '-m', '');
805
+ shared = { state, result: (0, list_fixtures_1.ok)((0, list_fixtures_1.listIn)(state)) };
806
+ }
807
+ catch (error) {
808
+ state.cleanup();
809
+ throw error;
810
+ }
811
+ }
812
+ expect(shared.result.changes).toHaveLength(ORDER.length);
813
+ const found = shared.result.changes.find((c) => c.merged_at === dateOf(key));
814
+ if (found === undefined)
815
+ throw new Error(`no entry for ${key}`);
816
+ return found;
817
+ }
754
818
  test('control characters, line separators and bidi marks in a subject are escaped', () => {
755
- adopt();
756
- repo.commit({ 'a.ts': '1' }, 'feat: \u001b[31mred\u001b[0m \u202eevil\u202c \u2028 \u0085 \u007f end (#5)', '2026-10-16T10:00:00Z');
757
- const entry = listed().changes[0];
758
- expect(entry.subject).not.toMatch(list_fixtures_1.UNSAFE);
759
- expect(entry.subject).toContain('\\u001b[31mred');
760
- expect(entry.subject).toContain('\\u202e');
761
- expect(entry.pr).toBe(5);
819
+ const e = entry('hostile');
820
+ expect(e.subject).not.toMatch(list_fixtures_1.UNSAFE);
821
+ expect(e.subject).toContain('\\u001b[31mred');
822
+ expect(e.subject).toContain('\\u202e');
823
+ expect(e.pr).toBe(5);
762
824
  });
763
825
  test('a subject is clipped to the subject cap with an ellipsis', () => {
764
- adopt();
765
- repo.commit({ 'a.ts': '1' }, `feat: ${'x'.repeat(5000)}`, '2026-10-16T10:00:00Z');
766
- const subject = listed().changes[0].subject;
826
+ const subject = entry('long').subject;
767
827
  expect(list_1.MAX_SUBJECT).toBe(1024);
768
828
  expect(subject.length).toBe(list_1.MAX_SUBJECT);
769
829
  expect(subject.endsWith('…')).toBe(true);
770
830
  });
771
831
  test('a subject exactly at the cap is not clipped', () => {
772
- adopt();
773
- const subject = `feat: ${'x'.repeat(list_1.MAX_SUBJECT - 6)}`;
774
- repo.commit({ 'a.ts': '1' }, subject, '2026-10-16T10:00:00Z');
775
- expect(listed().changes[0].subject).toBe(subject);
832
+ expect(entry('atCap').subject).toBe(SUBJECTS.atCap);
776
833
  });
777
834
  test('the cut never splits a surrogate pair', () => {
778
- adopt();
779
- repo.commit({ 'a.ts': '1' }, `feat: ${'x'.repeat(list_1.MAX_SUBJECT - 7)}\u{1F600}\u{1F600}`, '2026-10-16T10:00:00Z');
780
- const subject = listed().changes[0].subject;
835
+ const subject = entry('surrogates').subject;
781
836
  expect(Buffer.from(subject, 'utf8').toString('utf8')).toBe(subject);
782
837
  expect(subject.length).toBeLessThanOrEqual(list_1.MAX_SUBJECT);
783
838
  });
784
839
  test('a subject made only of controls is still one clean line', () => {
785
- adopt();
786
- repo.gitAt('2026-10-16T10:00:00Z', 'commit', '--allow-empty', '-q', '--allow-empty-message', '-m', '\u001b\u001b');
787
- const subject = listed().changes[0].subject;
840
+ const subject = entry('controls').subject;
788
841
  expect(subject).not.toMatch(list_fixtures_1.UNSAFE);
789
842
  });
790
843
  test('a commit with an empty message has an empty subject', () => {
791
- adopt();
792
- repo.gitAt('2026-10-16T10:00:00Z', 'commit', '--allow-empty', '-q', '--allow-empty-message', '-m', '');
793
- expect(listed().changes[0].subject).toBe('');
844
+ expect(entry('empty').subject).toBe('');
794
845
  });
795
846
  test('bidi marks inside a valid record are escaped in what list returns', () => {
796
847
  adopt();
@@ -815,8 +866,7 @@ describe('listChanges: hostile text from git is escaped and clipped', () => {
815
866
  expect(r.changes[0].sha).toMatch(/^[0-9a-f]{40}$/);
816
867
  });
817
868
  test('GIT_LITERAL_PATHSPECS and GIT_GLOB_PATHSPECS in the environment do not change the answer', () => {
818
- history();
819
- const plain = (0, list_fixtures_1.byClass)(listed());
820
- expect((0, helpers_1.withEnv)({ GIT_LITERAL_PATHSPECS: '1', GIT_GLOB_PATHSPECS: '1' }, () => (0, list_fixtures_1.byClass)(listed()))).toEqual(plain);
869
+ const plain = (0, list_fixtures_1.byClass)(listedHistory().result);
870
+ expect((0, helpers_1.withEnv)({ GIT_LITERAL_PATHSPECS: '1', GIT_GLOB_PATHSPECS: '1' }, () => (0, list_fixtures_1.byClass)((0, list_fixtures_1.ok)((0, list_fixtures_1.listIn)(historic))))).toEqual(plain);
821
871
  });
822
872
  });
@@ -75,15 +75,18 @@ let home;
75
75
  // Child git processes get an explicit identity and config isolation; the shared makeRepo already points HOME and AWM_HOME at a throwaway home.
76
76
  const gitEnv = () => ({ ...process.env, HOME: home, GIT_CONFIG_GLOBAL: path_1.default.join(home, 'gitconfig'), GIT_AUTHOR_NAME: 't', GIT_AUTHOR_EMAIL: 't@t', GIT_COMMITTER_NAME: 't', GIT_COMMITTER_EMAIL: 't@t', GIT_CONFIG_NOSYSTEM: '1' });
77
77
  const g = (...args) => repo.git(...args);
78
- beforeEach(() => {
79
- repo = (0, helpers_1.makeRepo)();
80
- root = repo.root;
81
- home = repo.home;
78
+ // One template repository for the whole file (it also holds HOME and AWM_HOME on a throwaway home); every test gets a copy (no git).
79
+ let template;
80
+ beforeAll(() => {
81
+ template = (0, helpers_1.makeRepo)();
82
82
  // makeRepo's own .gitignore ignores `.awm/`; these tests need a repository that does NOT (R2.2b), so the first commit is replaced.
83
- repo.commit({ '.gitignore': 'node_modules\n', 'a.txt': 'a' }, 'feat: a');
83
+ template.commit({ '.gitignore': 'node_modules\n', 'a.txt': 'a' }, 'feat: a');
84
84
  });
85
+ const useRepo = (from) => { repo = (0, helpers_1.cloneRepo)(from); root = repo.root; home = repo.home; };
86
+ beforeEach(() => useRepo(template));
85
87
  afterEach(() => repo.cleanup());
86
88
  afterAll(() => {
89
+ template.cleanup(); // restores HOME, AWM_HOME and the git config variables
87
90
  for (const k of ENV_KEYS)
88
91
  expect(process.env[k]).toBe(ORIGINAL[k]);
89
92
  expect(listRuns()).toEqual(RUNS_BEFORE);
@@ -776,9 +779,18 @@ describe('sensors run wiring (R2.2, R2.4)', () => {
776
779
  g('commit', '-q', '-m', `chore: manifest in ${path_1.default.basename(dir)}`);
777
780
  };
778
781
  const receiptNames = () => fs_1.default.readdirSync(receiptsDir()).sort();
782
+ // The template plus `commitManifest(root)`, built once: each test of this block starts from a copy of it.
783
+ let manifested;
784
+ afterAll(() => manifested?.cleanup());
779
785
  beforeEach(() => {
780
786
  process.exitCode = undefined;
781
- commitManifest(root);
787
+ // The shared state is its own copy, never the test's `repo`: a failed copy below must not let afterEach remove it.
788
+ if (manifested === undefined) {
789
+ commitManifest(root);
790
+ manifested = (0, helpers_1.cloneRepo)(repo);
791
+ }
792
+ repo.cleanup();
793
+ useRepo(manifested);
782
794
  run_1.findManifestDir.mockReset().mockImplementation((start) => {
783
795
  const project = (0, project_1.resolveSensorProject)(start);
784
796
  return project.state === 'configured' ? project.projectRoot : null;
@@ -29,6 +29,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
29
29
  const fs_1 = __importDefault(require("fs"));
30
30
  const os_1 = __importDefault(require("os"));
31
31
  const path_1 = __importDefault(require("path"));
32
+ const commit_seeded_registry_1 = require("../../helpers/commit-seeded-registry");
32
33
  describe('gatherProviderChecks — realistic fully-healthy single-provider fixtures', () => {
33
34
  let tmpHome;
34
35
  let tmpWork;
@@ -86,6 +87,8 @@ describe('gatherProviderChecks — realistic fully-healthy single-provider fixtu
86
87
  name: 'dev-core', description: '', version: '1.0.0', scope: 'baseline', visibility: 'public',
87
88
  dependsOn: [], skills: ['development-process'], workflows: [], agents,
88
89
  }));
90
+ // Local commit so real `awm init` skips network sync under SG-7 (#238).
91
+ (0, commit_seeded_registry_1.commitSeededRegistry)(root);
89
92
  fs_1.default.mkdirSync(path_1.default.join(tmpHome, '.awm'), { recursive: true });
90
93
  fs_1.default.writeFileSync(path_1.default.join(tmpHome, '.awm/registries.json'), JSON.stringify([{ name: 'baseline', remote: 'https://example.invalid/baseline.git' }], null, 2));
91
94
  }
@@ -6,10 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  // stepCache vs. syncRegistries()'s RESULT-shaped failures.
7
7
  //
8
8
  // `syncRegistries()` never throws on a per-registry failure — it returns
9
- // `{ action: 'error', error }` for that registry and keeps going. stepCache
10
- // used to `await` it and report a blanket 'applied', so a registry that never
11
- // landed on disk degraded silently into some later step's failure with its own
12
- // cause already gone.
9
+ // `{ action: 'error', error }` for that registry. SG-7: stepCache fails closed
10
+ // on ANY sync error (usable on-disk cache is not enough for init).
13
11
  //
14
12
  // Isolated in its own file (not steps.test.ts) because these assertions reach
15
13
  // `listRegistries()`, which resolves AWM_HOME at module require time — the env
@@ -83,12 +81,12 @@ describe('stepCache — registry sync error results', () => {
83
81
  expect(r.error).toContain('baseline');
84
82
  expect(r.error).toContain('could not clone');
85
83
  });
86
- it('stays applied but records the error when content is already on disk', async () => {
84
+ it('fails when sync errors even if content is already on disk (SG-7)', async () => {
87
85
  configureRegistries(['baseline'], ['baseline']);
88
86
  const r = await run([{ name: 'baseline', action: 'error', error: 'pull timed out' }]);
89
- expect(r.action).toBe('applied');
90
- expect(r.detail).toContain('baseline');
91
- expect(r.detail).toContain('pull timed out');
87
+ expect(r.action).toBe('failed');
88
+ expect(r.error).toContain('baseline');
89
+ expect(r.error).toContain('pull timed out');
92
90
  });
93
91
  it('fails on a secondary registry that never landed, even when the base one synced', async () => {
94
92
  configureRegistries(['baseline', 'documentation'], ['baseline']);
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const fs_1 = __importDefault(require("fs"));
7
+ const os_1 = __importDefault(require("os"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const registry_git_1 = require("../../src/core/registry-git");
10
+ const registry_git_errors_1 = require("../../src/core/registry-git-errors");
11
+ describe('registry-git factory (#238 S1)', () => {
12
+ it('buildRegistryGitEnv drops guarded GIT_* keys, keeps PATH, forces GIT_TERMINAL_PROMPT=0', () => {
13
+ const parent = {
14
+ PATH: '/usr/bin',
15
+ HOME: '/tmp/home',
16
+ GIT_ASKPASS: '/secret/askpass',
17
+ GIT_SSL_CAINFO: '/secret/ca.pem',
18
+ GIT_TERMINAL_PROMPT: '1',
19
+ EDITOR: 'vi',
20
+ VISUAL: 'vim',
21
+ PAGER: 'less',
22
+ OTHER: 'ok',
23
+ };
24
+ const env = (0, registry_git_1.buildRegistryGitEnv)(parent);
25
+ expect(env.PATH).toBe('/usr/bin');
26
+ expect(env.HOME).toBe('/tmp/home');
27
+ expect(env.OTHER).toBe('ok');
28
+ expect(env.GIT_ASKPASS).toBeUndefined();
29
+ expect(env.GIT_SSL_CAINFO).toBeUndefined();
30
+ expect(env.EDITOR).toBeUndefined();
31
+ expect(env.VISUAL).toBeUndefined();
32
+ expect(env.PAGER).toBeUndefined();
33
+ expect(env.GIT_TERMINAL_PROMPT).toBe('0');
34
+ expect((0, registry_git_1.listIgnoredGuardedGitEnvNames)(parent)).toEqual(['GIT_ASKPASS', 'GIT_SSL_CAINFO']);
35
+ });
36
+ it('createRegistryGit against a bad remote fails quickly without hanging', async () => {
37
+ const dest = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-registry-git-'));
38
+ const remote = 'https://127.0.0.1:1/awm-no-such-repo.git';
39
+ const started = Date.now();
40
+ await expect((0, registry_git_1.createRegistryGit)().clone(remote, path_1.default.join(dest, 'repo'))).rejects.toThrow(/terminal prompts disabled|Failed to connect|Could not resolve|timed out|Authentication|Connection refused|Unable to connect|Network is unreachable/i);
41
+ expect(Date.now() - started).toBeLessThan(15_000);
42
+ fs_1.default.rmSync(dest, { recursive: true, force: true });
43
+ }, 20_000);
44
+ });
45
+ describe('registry-git ignored-env notice (#238 S2 / SG-5)', () => {
46
+ afterEach(() => {
47
+ (0, registry_git_1.resetIgnoredGitEnvNoticeForTests)();
48
+ });
49
+ it('listIgnoredGuardedGitEnvNames returns GIT_SSL_CAINFO only (never the value)', () => {
50
+ const parent = {
51
+ PATH: '/usr/bin',
52
+ GIT_SSL_CAINFO: '/secret/ca.pem',
53
+ GIT_TERMINAL_PROMPT: '0',
54
+ };
55
+ expect((0, registry_git_1.listIgnoredGuardedGitEnvNames)(parent)).toEqual(['GIT_SSL_CAINFO']);
56
+ const notice = (0, registry_git_1.formatIgnoredGitEnvNotice)(['GIT_SSL_CAINFO']);
57
+ expect(notice).toContain('GIT_SSL_CAINFO');
58
+ expect(notice).not.toContain('/secret/ca.pem');
59
+ expect(notice).toMatch(/git config|http\.sslCAInfo|file-based|credential\.helper|core\.sshCommand/i);
60
+ });
61
+ it('warnIgnoredGitEnvOnce emits once per process and never prints values', () => {
62
+ const lines = [];
63
+ const parent = {
64
+ GIT_ASKPASS: '/secret/askpass',
65
+ GIT_SSL_CAINFO: '/secret/ca.pem',
66
+ GIT_TERMINAL_PROMPT: '1',
67
+ };
68
+ (0, registry_git_1.warnIgnoredGitEnvOnce)((m) => lines.push(m), parent);
69
+ (0, registry_git_1.warnIgnoredGitEnvOnce)((m) => lines.push(m), parent);
70
+ expect(lines).toHaveLength(1);
71
+ expect(lines[0]).toContain('GIT_ASKPASS');
72
+ expect(lines[0]).toContain('GIT_SSL_CAINFO');
73
+ expect(lines[0]).not.toContain('/secret/');
74
+ expect(lines[0]).not.toMatch(/GIT_TERMINAL_PROMPT/);
75
+ });
76
+ it('SG-5 lists only transport/auth names (not GIT_EDITOR) and includes SSH_ASKPASS (M2/B2)', () => {
77
+ const parent = {
78
+ GIT_EDITOR: 'vim',
79
+ GIT_PAGER: 'less',
80
+ GIT_ASKPASS: '/secret/askpass',
81
+ SSH_ASKPASS: '/secret/ssh-askpass',
82
+ GIT_TERMINAL_PROMPT: '0',
83
+ };
84
+ expect((0, registry_git_1.listIgnoredGuardedGitEnvNames)(parent)).toEqual(['GIT_ASKPASS', 'SSH_ASKPASS']);
85
+ expect((0, registry_git_1.listIgnoredGuardedGitEnvNames)(parent)).not.toContain('GIT_EDITOR');
86
+ });
87
+ });
88
+ describe('classifyRegistryGitError (#238 S2 / SG-6)', () => {
89
+ it('maps terminal prompts disabled → authentication with credential.helper remedy', () => {
90
+ const c = (0, registry_git_errors_1.classifyRegistryGitError)(new Error('fatal: could not read Username for \'https://example.com\': terminal prompts disabled'));
91
+ expect(c.class).toBe('authentication');
92
+ expect(c.fatalLine).toMatch(/terminal prompts disabled/i);
93
+ expect(c.remedy).toMatch(/credential\.helper/);
94
+ });
95
+ it('maps TLS / certificate wording → tls', () => {
96
+ const c = (0, registry_git_errors_1.classifyRegistryGitError)(new Error('fatal: unable to access \'https://x/\': SSL certificate problem: unable to get local issuer certificate'));
97
+ expect(c.class).toBe('tls');
98
+ expect(c.remedy).toMatch(/http\.sslCAInfo|http\.sslCAPath/);
99
+ });
100
+ it('maps Could not resolve host → network', () => {
101
+ const c = (0, registry_git_errors_1.classifyRegistryGitError)(new Error('fatal: unable to access \'https://no.such.host/\': Could not resolve host: no.such.host'));
102
+ expect(c.class).toBe('network');
103
+ });
104
+ it('maps timeout plugin message → timeout', () => {
105
+ const c = (0, registry_git_errors_1.classifyRegistryGitError)(new Error('block timeout reached (60000ms) — killing process'));
106
+ expect(c.class).toBe('timeout');
107
+ expect(c.remedy).toMatch(/60s/);
108
+ });
109
+ it('maps git/curl Connection timed out → network, not awm idle timeout (M1)', () => {
110
+ const c = (0, registry_git_errors_1.classifyRegistryGitError)(new Error("fatal: unable to access 'https://github.com/x/y.git/': Failed to connect to github.com port 443 after 133030 ms: Connection timed out"));
111
+ expect(c.class).toBe('network');
112
+ expect(c.remedy).not.toMatch(/killed after 60s/);
113
+ });
114
+ it('maps ssh Could not resolve hostname → network, not ssh (M1)', () => {
115
+ const c = (0, registry_git_errors_1.classifyRegistryGitError)(new Error('ssh: Could not resolve hostname example.invalid: Name or service not known'));
116
+ expect(c.class).toBe('network');
117
+ });
118
+ it('maps SSH host key failures → ssh with core.sshCommand remedy', () => {
119
+ const c = (0, registry_git_errors_1.classifyRegistryGitError)(new Error('fatal: Host key verification failed.'));
120
+ expect(c.class).toBe('ssh');
121
+ expect(c.remedy).toMatch(/core\.sshCommand|~\/\.ssh\/config/);
122
+ });
123
+ it('maps repository not found → not-found', () => {
124
+ const c = (0, registry_git_errors_1.classifyRegistryGitError)(new Error('remote: Repository not found.\nfatal: repository \'https://github.com/x/y.git\' not found'));
125
+ expect(c.class).toBe('not-found');
126
+ expect(c.fatalLine).toMatch(/not found/i);
127
+ });
128
+ it('prefers not-found over broad SSH wording when remote says Repository not found', () => {
129
+ const c = (0, registry_git_errors_1.classifyRegistryGitError)(new Error('ERROR: Repository not found.\nfatal: Could not read from remote repository.'));
130
+ expect(c.class).toBe('not-found');
131
+ expect(c.class).not.toBe('ssh');
132
+ });
133
+ it('maps unknown transport wording → network (default)', () => {
134
+ const c = (0, registry_git_errors_1.classifyRegistryGitError)(new Error('something unexpected happened in git'));
135
+ expect(c.class).toBe('network');
136
+ });
137
+ });
138
+ describe('formatClassifiedRegistryFailure redacts URL credentials (#238 QA B1)', () => {
139
+ it('strips https userinfo from remote= and fatal lines', () => {
140
+ const token = 'ghp_SECRETTOKEN1234567890';
141
+ const remote = `https://${token}@github.com/org/private-reg.git`;
142
+ const msg = (0, registry_git_errors_1.formatClassifiedRegistryFailure)({
143
+ name: 'corp',
144
+ remote,
145
+ err: new Error(`fatal: repository '${remote}' not found`),
146
+ });
147
+ expect(msg).toContain('github.com/org/private-reg.git');
148
+ expect(msg).not.toContain(token);
149
+ expect(msg).not.toMatch(/ghp_/);
150
+ expect(msg).toMatch(/class=not-found/);
151
+ expect(msg).toMatch(/remedy:/);
152
+ });
153
+ });
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.commitSeededRegistry = commitSeededRegistry;
4
+ // Local `.git` commit for hand-seeded registry content roots used by real
5
+ // (unstubbed) `awm init` / `stepCache` paths.
6
+ //
7
+ // Under SG-7 (#238), any sync/clone error hard-fails init. Diagnostics treat
8
+ // `registryCache.present` as "`.git` exists" (`context.ts`), so a content-only
9
+ // fixture always triggers sync against the unreachable `example.invalid`
10
+ // remote. Committing locally makes cache present and lets stepCache skip.
11
+ const child_process_1 = require("child_process");
12
+ function commitSeededRegistry(root) {
13
+ (0, child_process_1.execFileSync)('git', ['init', '-q'], { cwd: root, stdio: 'ignore' });
14
+ (0, child_process_1.execFileSync)('git', ['config', 'user.email', 'fixture@example.invalid'], { cwd: root, stdio: 'ignore' });
15
+ (0, child_process_1.execFileSync)('git', ['config', 'user.name', 'fixture'], { cwd: root, stdio: 'ignore' });
16
+ (0, child_process_1.execFileSync)('git', ['add', '-A'], { cwd: root, stdio: 'ignore' });
17
+ (0, child_process_1.execFileSync)('git', ['commit', '-qm', 'fixture'], { cwd: root, stdio: 'ignore' });
18
+ }