agentic-workflow-manager 9.21.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 (71) hide show
  1. package/dist/src/commands/change/index.js +16 -5
  2. package/dist/src/commands/init.js +8 -4
  3. package/dist/src/commands/preflight/checks.js +22 -0
  4. package/dist/src/commands/registry/add.js +9 -3
  5. package/dist/src/commands/sensors/compatibility/contract.js +56 -3
  6. package/dist/src/commands/sensors/compatibility/manifest.js +16 -2
  7. package/dist/src/commands/sensors/index.js +10 -1
  8. package/dist/src/commands/sensors/inputs-digest.js +315 -0
  9. package/dist/src/commands/sensors/prepare.js +10 -0
  10. package/dist/src/commands/sensors/result-cache.js +278 -0
  11. package/dist/src/commands/sensors/result.js +31 -12
  12. package/dist/src/commands/sensors/run.js +265 -8
  13. package/dist/src/commands/sensors/status.js +66 -7
  14. package/dist/src/commands/sync.js +11 -5
  15. package/dist/src/commands/update.js +23 -12
  16. package/dist/src/core/changes/derive.js +6 -3
  17. package/dist/src/core/changes/git.js +1 -2
  18. package/dist/src/core/changes/receipt.js +219 -9
  19. package/dist/src/core/changes/worktree.js +15 -3
  20. package/dist/src/core/init/steps.js +3 -8
  21. package/dist/src/core/registries.js +20 -15
  22. package/dist/src/core/registry-git-errors.js +107 -0
  23. package/dist/src/core/registry-git.js +93 -0
  24. package/dist/src/core/registry-sync-report.js +74 -0
  25. package/dist/src/core/versioning.js +3 -6
  26. package/dist/src/index.js +5 -8
  27. package/dist/tests/commands/change/new.test.js +104 -58
  28. package/dist/tests/commands/change/validate-unmeasured.test.js +246 -0
  29. package/dist/tests/commands/change/validate.test.js +164 -108
  30. package/dist/tests/commands/changes/list.test.js +41 -22
  31. package/dist/tests/commands/job/gate-reconcile.test.js +13 -2
  32. package/dist/tests/commands/preflight/sensors-inputs.test.js +110 -0
  33. package/dist/tests/commands/registry-sync-honesty.test.js +175 -0
  34. package/dist/tests/commands/sensors/compatibility/contract.test.js +60 -0
  35. package/dist/tests/commands/sensors/compatibility/manifest.test.js +94 -0
  36. package/dist/tests/commands/sensors/exec.test.js +3 -1
  37. package/dist/tests/commands/sensors/inputs-digest.test.js +373 -0
  38. package/dist/tests/commands/sensors/result-cache.test.js +352 -0
  39. package/dist/tests/commands/sensors/run-cache.test.js +434 -0
  40. package/dist/tests/commands/sensors/run-slice.test.js +171 -0
  41. package/dist/tests/commands/sensors/status-inputs.test.js +271 -0
  42. package/dist/tests/commands/update.test.js +8 -8
  43. package/dist/tests/commands/watch/supervisor-loop.test.js +21 -5
  44. package/dist/tests/core/changes/helpers.js +15 -3
  45. package/dist/tests/core/changes/list-paging.test.js +131 -110
  46. package/dist/tests/core/changes/list.test.js +133 -83
  47. package/dist/tests/core/changes/receipt.test.js +279 -8
  48. package/dist/tests/core/diagnostics/provider-checks.test.js +3 -0
  49. package/dist/tests/core/init/steps-registry-sync.test.js +6 -8
  50. package/dist/tests/core/registry-git.test.js +153 -0
  51. package/dist/tests/helpers/commit-seeded-registry.js +18 -0
  52. package/dist/tests/integration/codex-provider-isolated.test.js +7 -2
  53. package/dist/tests/integration/copilot-init-isolated.test.js +7 -4
  54. package/dist/tests/structural/active-documentation.test.js +0 -41
  55. package/dist/tests/structural/bundle-skill-reference-contract.test.js +0 -12
  56. package/dist/tests/structural/cli-reference-matches-commander.test.js +94 -0
  57. package/dist/tests/structural/native-release-artifacts.test.js +116 -107
  58. package/dist/tests/structural/sensor-documentation-contract.test.js +0 -47
  59. package/dist/tests/structural/sensor-portability-contract.test.js +0 -7
  60. package/dist/tests/structural/simple-git-v4-pin.test.js +49 -0
  61. package/package.json +3 -2
  62. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  63. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  64. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  65. package/prebuilds/win32-x64/secure_fs.node +0 -0
  66. package/dist/tests/structural/cloud-admission-docs-contract.test.js +0 -54
  67. package/dist/tests/structural/documentation-phase-is-mechanized.test.js +0 -29
  68. package/dist/tests/structural/operator-environments-docs-contract.test.js +0 -70
  69. package/dist/tests/structural/process-model-description-contract.test.js +0 -34
  70. package/dist/tests/structural/project-declared-sensors-docs-contract.test.js +0 -104
  71. package/dist/tests/structural/r4-plan-currentness-docs.test.js +0 -37
@@ -0,0 +1,271 @@
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 status_1 = require("../../../src/commands/sensors/status");
10
+ /**
11
+ * RF-3.4 — status surfaces a non-blocking warning when a configured enabled
12
+ * sensor lacks effective `inputs` (not cacheable). Warnings must not degrade
13
+ * `overall`, and opt-out / native-gate must stay silent.
14
+ */
15
+ describe('computeSensorStatus inputs warnings (RF-3.4)', () => {
16
+ let tmpDir;
17
+ let previousHome;
18
+ let home;
19
+ beforeEach(() => {
20
+ tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-inputs-'));
21
+ home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-inputs-home-'));
22
+ previousHome = process.env.AWM_HOME;
23
+ process.env.AWM_HOME = home;
24
+ fs_1.default.writeFileSync(path_1.default.join(home, 'registries.json'), JSON.stringify([]));
25
+ fs_1.default.mkdirSync(path_1.default.join(tmpDir, 'node_modules', '.bin'), { recursive: true });
26
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'node_modules', '.bin', 'eslint'), '');
27
+ fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
28
+ });
29
+ afterEach(() => {
30
+ if (previousHome === undefined)
31
+ delete process.env.AWM_HOME;
32
+ else
33
+ process.env.AWM_HOME = previousHome;
34
+ fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
35
+ fs_1.default.rmSync(home, { recursive: true, force: true });
36
+ });
37
+ function writeOnlyProject(sensors) {
38
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
39
+ schemaVersion: 3,
40
+ mode: 'project-sensors',
41
+ sensors,
42
+ }));
43
+ }
44
+ function projectSensor(extra = {}) {
45
+ return {
46
+ source: 'project',
47
+ enabled: true,
48
+ command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
49
+ formatter: 'exit-code',
50
+ ...extra,
51
+ };
52
+ }
53
+ it('warns when an enabled sensor lacks effective inputs (not cacheable)', async () => {
54
+ writeOnlyProject({ custom: projectSensor() });
55
+ const result = await (0, status_1.computeSensorStatus)(tmpDir);
56
+ expect(result.overall).toBe('READY');
57
+ expect(result.warnings).toEqual(expect.arrayContaining([
58
+ expect.objectContaining({
59
+ sensor: 'custom',
60
+ detail: expect.stringMatching(/not cacheable|no effective inputs|lacks inputs/i),
61
+ }),
62
+ ]));
63
+ });
64
+ it('does not warn when effective inputs are present', async () => {
65
+ writeOnlyProject({ custom: projectSensor({ inputs: ['**/*.ts'] }) });
66
+ const result = await (0, status_1.computeSensorStatus)(tmpDir);
67
+ expect(result.overall).toBe('READY');
68
+ expect(result.warnings ?? []).toEqual([]);
69
+ });
70
+ it('does not warn for disabled sensors that lack inputs', async () => {
71
+ writeOnlyProject({ custom: projectSensor({ enabled: false }) });
72
+ const result = await (0, status_1.computeSensorStatus)(tmpDir);
73
+ expect(result.warnings ?? []).toEqual([]);
74
+ expect(result.checks.custom).toMatchObject({ ok: true, detail: expect.stringMatching(/disabled/i) });
75
+ });
76
+ it.each([
77
+ ['native-gate', 'CI is authoritative.'],
78
+ ['opt-out', 'No local gate.'],
79
+ ])('emits no spurious warnings for v3 %s', async (mode, reason) => {
80
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
81
+ schemaVersion: 3, mode, reason,
82
+ }));
83
+ const result = await (0, status_1.computeSensorStatus)(tmpDir);
84
+ expect(result.mode).toBe(mode);
85
+ expect(result.warnings ?? []).toEqual([]);
86
+ });
87
+ it('uses pack-variant inputs as effective inputs (no warning when only the pack declares them)', async () => {
88
+ const registry = path_1.default.join(home, 'registries', 'baseline');
89
+ fs_1.default.mkdirSync(path_1.default.join(registry, 'sensor-packs', 'js-ts'), { recursive: true });
90
+ fs_1.default.writeFileSync(path_1.default.join(home, 'registries.json'), JSON.stringify([
91
+ { name: 'baseline', remote: 'https://example.test/baseline.git' },
92
+ ]));
93
+ fs_1.default.writeFileSync(path_1.default.join(registry, 'sensor-packs', 'js-ts', 'pack.json'), JSON.stringify({
94
+ schemaVersion: 2, name: 'js-ts', description: 'fixture', detects: ['package.json'],
95
+ coverage: {
96
+ schemaVersion: 1,
97
+ classes: {
98
+ lint: {
99
+ description: 'lint',
100
+ detectors: [{ sensor: 'lint' }],
101
+ remedy: { summary: 'fix', command: 'awm sensors init' },
102
+ },
103
+ },
104
+ },
105
+ sensors: {
106
+ lint: {
107
+ applicability: { allFiles: ['package.json'] },
108
+ variants: [{
109
+ id: 'eslint-10', priority: 10, certifiedRange: '>=10 <11',
110
+ requirements: {
111
+ tool: 'eslint', toolRange: '>=10 <11',
112
+ runtime: 'node', runtimeRange: '>=0',
113
+ },
114
+ assets: [], formatter: 'eslint-llm',
115
+ probe: { kind: 'package-script-present', script: 'lint' },
116
+ command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
117
+ inputs: ['src/**/*.ts'],
118
+ }],
119
+ },
120
+ },
121
+ }));
122
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'package.json'), JSON.stringify({
123
+ scripts: { lint: 'eslint .' },
124
+ devDependencies: { eslint: '^10.0.0' },
125
+ }));
126
+ fs_1.default.mkdirSync(path_1.default.join(tmpDir, 'node_modules', 'eslint'), { recursive: true });
127
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'node_modules', 'eslint', 'package.json'), JSON.stringify({ version: '10.0.0' }));
128
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
129
+ schemaVersion: 2, pack: 'js-ts',
130
+ sensors: {
131
+ lint: {
132
+ enabled: true, variantId: 'eslint-10',
133
+ command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
134
+ assets: [],
135
+ initializedCompatibility: {
136
+ state: 'certified', reason: 'range-and-probe', variantId: 'eslint-10',
137
+ toolVersion: '10.0.0', runtimeVersion: process.versions.node,
138
+ certifiedRange: '>=10 <11', evidence: [],
139
+ },
140
+ },
141
+ },
142
+ }));
143
+ const result = await (0, status_1.computeSensorStatus)(tmpDir);
144
+ expect(result.checks.lint).toMatchObject({ ok: true });
145
+ expect(result.warnings ?? []).toEqual([]);
146
+ });
147
+ it('warns for pack-bound enabled sensors when neither project nor pack variant provides inputs', async () => {
148
+ const registry = path_1.default.join(home, 'registries', 'baseline');
149
+ fs_1.default.mkdirSync(path_1.default.join(registry, 'sensor-packs', 'js-ts'), { recursive: true });
150
+ fs_1.default.writeFileSync(path_1.default.join(home, 'registries.json'), JSON.stringify([
151
+ { name: 'baseline', remote: 'https://example.test/baseline.git' },
152
+ ]));
153
+ fs_1.default.writeFileSync(path_1.default.join(registry, 'sensor-packs', 'js-ts', 'pack.json'), JSON.stringify({
154
+ schemaVersion: 2, name: 'js-ts', description: 'fixture', detects: ['package.json'],
155
+ coverage: {
156
+ schemaVersion: 1,
157
+ classes: {
158
+ lint: {
159
+ description: 'lint',
160
+ detectors: [{ sensor: 'lint' }],
161
+ remedy: { summary: 'fix', command: 'awm sensors init' },
162
+ },
163
+ },
164
+ },
165
+ sensors: {
166
+ lint: {
167
+ applicability: { allFiles: ['package.json'] },
168
+ variants: [{
169
+ id: 'eslint-10', priority: 10, certifiedRange: '>=10 <11',
170
+ requirements: {
171
+ tool: 'eslint', toolRange: '>=10 <11',
172
+ runtime: 'node', runtimeRange: '>=0',
173
+ },
174
+ assets: [], formatter: 'eslint-llm',
175
+ probe: { kind: 'package-script-present', script: 'lint' },
176
+ command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
177
+ }],
178
+ },
179
+ },
180
+ }));
181
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'package.json'), JSON.stringify({
182
+ scripts: { lint: 'eslint .' },
183
+ devDependencies: { eslint: '^10.0.0' },
184
+ }));
185
+ fs_1.default.mkdirSync(path_1.default.join(tmpDir, 'node_modules', 'eslint'), { recursive: true });
186
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'node_modules', 'eslint', 'package.json'), JSON.stringify({ version: '10.0.0' }));
187
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
188
+ schemaVersion: 2, pack: 'js-ts',
189
+ sensors: {
190
+ lint: {
191
+ enabled: true, variantId: 'eslint-10',
192
+ command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
193
+ assets: [],
194
+ initializedCompatibility: {
195
+ state: 'certified', reason: 'range-and-probe', variantId: 'eslint-10',
196
+ toolVersion: '10.0.0', runtimeVersion: process.versions.node,
197
+ certifiedRange: '>=10 <11', evidence: [],
198
+ },
199
+ },
200
+ },
201
+ }));
202
+ const result = await (0, status_1.computeSensorStatus)(tmpDir);
203
+ expect(result.overall).toBe('READY');
204
+ expect(result.warnings).toEqual(expect.arrayContaining([
205
+ expect.objectContaining({
206
+ sensor: 'lint',
207
+ detail: expect.stringMatching(/not cacheable|no effective inputs|lacks inputs/i),
208
+ }),
209
+ ]));
210
+ });
211
+ });
212
+ describe('awm sensors status CLI renders inputs warnings (RF-3.4)', () => {
213
+ let tmpDir;
214
+ let previousHome;
215
+ let home;
216
+ let previousCwd;
217
+ beforeEach(() => {
218
+ previousCwd = process.cwd();
219
+ tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-inputs-cli-'));
220
+ home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-inputs-cli-home-'));
221
+ previousHome = process.env.AWM_HOME;
222
+ process.env.AWM_HOME = home;
223
+ fs_1.default.writeFileSync(path_1.default.join(home, 'registries.json'), JSON.stringify([]));
224
+ fs_1.default.mkdirSync(path_1.default.join(tmpDir, 'node_modules', '.bin'), { recursive: true });
225
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'node_modules', '.bin', 'eslint'), '');
226
+ fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
227
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
228
+ schemaVersion: 3,
229
+ mode: 'project-sensors',
230
+ sensors: {
231
+ custom: {
232
+ source: 'project',
233
+ enabled: true,
234
+ command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
235
+ formatter: 'exit-code',
236
+ },
237
+ },
238
+ }));
239
+ process.chdir(tmpDir);
240
+ });
241
+ afterEach(() => {
242
+ process.chdir(previousCwd);
243
+ if (previousHome === undefined)
244
+ delete process.env.AWM_HOME;
245
+ else
246
+ process.env.AWM_HOME = previousHome;
247
+ fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
248
+ fs_1.default.rmSync(home, { recursive: true, force: true });
249
+ });
250
+ it('prints warning text in human-readable status output without failing READY', async () => {
251
+ const { Command } = require('commander');
252
+ const { registerSensorsCommand } = require('../../../src/commands/sensors/index');
253
+ const program = new Command();
254
+ program.exitOverride();
255
+ registerSensorsCommand(program);
256
+ const output = jest.spyOn(console, 'log').mockImplementation(() => undefined);
257
+ const exitSpy = jest.spyOn(process, 'exit').mockImplementation((() => undefined));
258
+ try {
259
+ await program.parseAsync(['node', 'awm', 'sensors', 'status']);
260
+ const rendered = output.mock.calls.flat().join('\n');
261
+ expect(rendered).toMatch(/READY/);
262
+ expect(rendered).toMatch(/custom/);
263
+ expect(rendered).toMatch(/not cacheable|no effective inputs|lacks inputs/i);
264
+ expect(exitSpy).not.toHaveBeenCalled();
265
+ }
266
+ finally {
267
+ output.mockRestore();
268
+ exitSpy.mockRestore();
269
+ }
270
+ });
271
+ });
@@ -164,9 +164,9 @@ describe('awm update — honest outcome', () => {
164
164
  expect(result.code).toBe(1);
165
165
  expect(result.registries.failed).toEqual(['baseline']);
166
166
  });
167
- it('un registry que falló pero conserva contenido sigue, y el cierre lo nombra en vez de anunciar éxito parejo', async () => {
168
- // `unusableSyncedRegistries` mira contenido EN DISCO: se siembra el content root
169
- // y el registries.json para que `docs` cuente como stale, no como roto.
167
+ it('un registry que falló falla cerrado aunque conserve contenido en disco (SG-7)', async () => {
168
+ // Pre-SG-7 update warn-and-continued on usable cache; #238 SG-7 requires
169
+ // non-zero exit for init/update/sync/registry add on any sync failure.
170
170
  const registriesDir = path_1.default.join(process.env.AWM_HOME, 'registries');
171
171
  fs_1.default.mkdirSync(path_1.default.join(registriesDir, 'docs', 'skills'), { recursive: true });
172
172
  fs_1.default.writeFileSync(path_1.default.join(process.env.AWM_HOME, 'registries.json'), JSON.stringify([{ name: 'docs', remote: 'https://example.test/docs.git' }]));
@@ -177,12 +177,12 @@ describe('awm update — honest outcome', () => {
177
177
  { name: 'docs', action: 'error', error: 'network unreachable' },
178
178
  ],
179
179
  }));
180
- expect(result.code).toBe(0);
180
+ expect(result.code).toBe(1);
181
181
  expect(result.registries).toEqual({ configured: 2, synced: 1, failed: ['docs'] });
182
- const outro = updateOutro(result);
183
- expect(outro).toMatch(/stale/i);
184
- expect(outro).toMatch(/docs/);
185
- expect(outro).not.toMatch(/^✅/);
182
+ expect(updateOutro(result)).toMatch(/Finished with registry sync errors/);
183
+ expect(updateOutro(result)).toMatch(/1\/2/);
184
+ expect(updateOutro(result)).toMatch(/docs/);
185
+ expect(errSpy.mock.calls.flat().join('\n')).toMatch(/docs/);
186
186
  });
187
187
  });
188
188
  describe('awm update — nunca se cuelga esperando a un humano que no está', () => {
@@ -758,21 +758,37 @@ describe('supervisor loop', () => {
758
758
  // subagent kept an idle leader while the work ran in processes outside its
759
759
  // process group, and the supervisor entered a false custody. Work in the
760
760
  // descendant tree is activity: observe, never custody, while it moves.
761
+ // The tree must move deterministically between the two ticks. A `while :; do
762
+ // sleep 0.02; done` loop leaves instants with no `sleep` child, so under CPU
763
+ // load two samples 150 ms apart could read identical (~1% of pairs) and the
764
+ // supervisor entered a real custody: a flaky red on linux-arm64. Each worker
765
+ // child lives 30 s, and the test itself makes the tree grow by one between ticks.
761
766
  itPosix('espera sana: lider ocioso + descendiente fuera del grupo trabajando => observa, no custodia (R4.2)', async () => {
762
767
  initUnattendedFixture(repo);
763
768
  (0, generations_1.beginGeneration)(repo, 'main');
764
- const worker = "require('child_process').spawn('sh', ['-c', 'while :; do sleep 0.02; done'], { detached: true, stdio: 'ignore' }).unref(); setTimeout(()=>{}, 20000)";
765
- const { child, ref } = (0, process_1.spawnStructured)(['node', '-e', worker], process.cwd(), 'nWait');
769
+ const trigger = path_1.default.join(repo, 'grow-descendant');
770
+ const worker = [
771
+ "const { spawn } = require('child_process'); const fs = require('fs');",
772
+ "const grow = () => spawn('sleep', ['30'], { detached: true, stdio: 'ignore' }).unref();",
773
+ 'grow();',
774
+ 'const poll = setInterval(() => { if (fs.existsSync(process.argv[1])) { clearInterval(poll); grow(); } }, 10);',
775
+ 'setTimeout(() => {}, 20000);',
776
+ ].join(' ');
777
+ const { child, ref } = (0, process_1.spawnStructured)(['node', '-e', worker, trigger], process.cwd(), 'nWait');
766
778
  try {
767
779
  let s = (0, store_1.readJournal)(repo, 'main').state;
768
780
  (0, generations_1.activeGeneration)(s).processRef = ref;
769
781
  s.controllerHeartbeatAt = new Date(Date.now() - 3600000).toISOString(); // heartbeat vencido: bloqueado en la espera
770
782
  (0, store_1.writeJournal)(repo, 'main', s);
771
- await until(() => ((0, process_1.activitySnapshot)(ref)?.descendants ?? '0:none') !== '0:none');
783
+ const descendants = () => (0, process_1.activitySnapshot)(ref)?.descendants ?? '0:none';
784
+ await until(() => descendants().startsWith('1:'));
772
785
  const cfg = { ...supervisor_1.DEFAULT_SUPERVISOR_CONFIG, provider: 'codex', heartbeatTimeoutMs: 1, activityWindowMs: 50, tickMs: 20 };
773
786
  const sup = new supervisor_1.Supervisor(repo, 'main', cfg, fakeSpawner, undefined, admitted);
774
787
  await sup.tick();
775
- await new Promise((r) => setTimeout(r, 150)); // mas que la ventana, pero el arbol se movio
788
+ const firstTick = Date.now();
789
+ fs_1.default.writeFileSync(trigger, ''); // el arbol crece: trabajo real fuera del grupo
790
+ await until(() => descendants().startsWith('2:'));
791
+ await new Promise((r) => setTimeout(r, Math.max(0, 150 - (Date.now() - firstTick)))); // mas que la ventana, pero el arbol se movio
776
792
  expect(await sup.tick()).not.toBe('custody');
777
793
  const after = (0, store_1.readJournal)(repo, 'main').state;
778
794
  expect(after.cycle.status).toBe('IN_PROGRESS');
@@ -780,7 +796,7 @@ describe('supervisor loop', () => {
780
796
  expect(child.killed).toBe(false);
781
797
  }
782
798
  finally {
783
- // The detached shell leads its own group: kill that group, never a pattern match.
799
+ // Each detached child leads its own group: kill that group, never a pattern match.
784
800
  let shells = [];
785
801
  try {
786
802
  shells = (0, child_process_1.execFileSync)('pgrep', ['-P', String(ref.pid)], { encoding: 'utf8' }).split('\n').filter(Boolean).map(Number);
@@ -67,19 +67,25 @@ function emptyGitConfig(home) {
67
67
  fs_1.default.writeFileSync(file, '');
68
68
  return file;
69
69
  }
70
+ /**
71
+ * Every test git child: no signing, and no automatic maintenance after a commit. A newer git runs that maintenance detached in
72
+ * the background, where its lock file under `.git/objects` appears and vanishes while a test copies or removes the repository
73
+ * (`cloneRepo` failed with ENOENT on macOS with git 2.55).
74
+ */
75
+ const GIT_TEST_CONFIG = ['-c', 'commit.gpgsign=false', '-c', 'maintenance.auto=false', '-c', 'gc.auto=0'];
70
76
  /** The git runner of a repository: identity and config isolation in an explicit environment, optional explicit dates. */
71
77
  function gitApi(root, home) {
72
78
  const env = {
73
79
  ...process.env, HOME: home, GIT_CONFIG_GLOBAL: path_1.default.join(home, 'gitconfig'), GIT_CONFIG_NOSYSTEM: '1',
74
80
  GIT_AUTHOR_NAME: 't', GIT_AUTHOR_EMAIL: 't@t', GIT_COMMITTER_NAME: 't', GIT_COMMITTER_EMAIL: 't@t',
75
81
  };
76
- const run = (extra, args) => (0, child_process_1.execFileSync)('git', ['-c', 'commit.gpgsign=false', ...args], { cwd: root, encoding: 'utf8', env: { ...env, ...extra } }).trim();
82
+ const run = (extra, args) => (0, child_process_1.execFileSync)('git', [...GIT_TEST_CONFIG, ...args], { cwd: root, encoding: 'utf8', env: { ...env, ...extra } }).trim();
77
83
  const dates = (date) => {
78
84
  const stamp = `${Math.floor(Date.parse(date) / 1000)} +0000`;
79
85
  return { GIT_AUTHOR_DATE: stamp, GIT_COMMITTER_DATE: stamp };
80
86
  };
81
87
  const git = (...args) => run({}, args);
82
- const gitInput = (input, ...args) => (0, child_process_1.execFileSync)('git', ['-c', 'commit.gpgsign=false', ...args], { cwd: root, encoding: 'utf8', env, input }).trim();
88
+ const gitInput = (input, ...args) => (0, child_process_1.execFileSync)('git', [...GIT_TEST_CONFIG, ...args], { cwd: root, encoding: 'utf8', env, input }).trim();
83
89
  const gitAt = (date, ...args) => run(dates(date), args);
84
90
  const commit = (files, message, date) => {
85
91
  for (const [rel, content] of Object.entries(files)) {
@@ -144,7 +150,13 @@ Object.defineProperty(exports, "posixOnly", { enumerable: true, get: function ()
144
150
  */
145
151
  function cloneRepo(template) {
146
152
  const root = fs_1.default.realpathSync.native(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-chg-')));
147
- fs_1.default.cpSync(template.root, root, { recursive: true });
153
+ try {
154
+ fs_1.default.cpSync(template.root, root, { recursive: true });
155
+ }
156
+ catch (error) {
157
+ fs_1.default.rmSync(root, { recursive: true, force: true });
158
+ throw error;
159
+ }
148
160
  const { git, gitInput, gitAt, commit } = gitApi(root, template.home);
149
161
  return { root, home: template.home, git, gitInput, gitAt, commit, cleanup: () => fs_1.default.rmSync(root, { recursive: true, force: true }) };
150
162
  }