agentic-workflow-manager 9.14.0 → 9.15.0

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 (40) hide show
  1. package/dist/src/commands/context-budget/budget.js +50 -4
  2. package/dist/src/commands/doctor.js +4 -1
  3. package/dist/src/commands/hooks/cursor.js +359 -0
  4. package/dist/src/commands/hooks/install.js +3 -0
  5. package/dist/src/commands/hooks/resync.js +10 -0
  6. package/dist/src/commands/hooks/status.js +3 -0
  7. package/dist/src/commands/hooks/uninstall.js +3 -0
  8. package/dist/src/commands/update.js +4 -0
  9. package/dist/src/core/admission/index.js +12 -6
  10. package/dist/src/core/admission/registry-contracts.js +14 -1
  11. package/dist/src/core/context/strategies/codex-agents.js +6 -2
  12. package/dist/src/core/cursor-budgets.js +8 -0
  13. package/dist/src/core/dashboard/sanitize.js +2 -2
  14. package/dist/src/core/diagnostics/provider-checks.js +11 -2
  15. package/dist/src/core/init/migrate-cursor-mdc.js +72 -0
  16. package/dist/src/core/init/steps.js +4 -0
  17. package/dist/src/providers/index.js +18 -4
  18. package/dist/tests/commands/add.test.js +5 -7
  19. package/dist/tests/commands/context-budget/budget.test.js +68 -0
  20. package/dist/tests/commands/doctor.test.js +2 -2
  21. package/dist/tests/commands/hooks/cursor.test.js +257 -0
  22. package/dist/tests/commands/hooks/resync.test.js +8 -0
  23. package/dist/tests/commands/init.test.js +23 -22
  24. package/dist/tests/commands/plan/index.test.js +82 -2
  25. package/dist/tests/commands/sensors/exec.test.js +18 -9
  26. package/dist/tests/core/admission/index.test.js +39 -0
  27. package/dist/tests/core/bundle-install.test.js +8 -14
  28. package/dist/tests/core/context/strategies/codex-agents.test.js +31 -12
  29. package/dist/tests/core/diagnostics/provider-tier.test.js +46 -96
  30. package/dist/tests/core/diagnostics/rendered-artifact-visibility.test.js +6 -4
  31. package/dist/tests/core/init/migrate-cursor-mdc.test.js +74 -0
  32. package/dist/tests/core/install-planner.test.js +16 -12
  33. package/dist/tests/integration/cursor-subset-gate.test.js +123 -0
  34. package/dist/tests/providers/index.test.js +34 -21
  35. package/dist/tests/structural/provider-paths-honor-config-home.test.js +3 -2
  36. package/package.json +2 -2
  37. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  38. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  39. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  40. package/prebuilds/win32-x64/secure_fs.node +0 -0
@@ -65,6 +65,7 @@ describe('resyncInstalledHooks', () => {
65
65
  expect(results).toEqual([
66
66
  { agent: 'claude-code', action: 'resynced' },
67
67
  { agent: 'codex', action: 'not-installed' },
68
+ { agent: 'cursor', action: 'not-installed' },
68
69
  ]);
69
70
  const settings = JSON.parse(fs_1.default.readFileSync(path_1.default.join(tmpHome, '.claude/settings.json'), 'utf8'));
70
71
  expect(settings.hooks.SubagentStart[0].hooks[0].command).toBe('awm model-policy hook-event --event start');
@@ -85,6 +86,7 @@ describe('resyncInstalledHooks', () => {
85
86
  expect(results).toEqual([
86
87
  { agent: 'claude-code', action: 'not-installed' },
87
88
  { agent: 'codex', action: 'not-installed' },
89
+ { agent: 'cursor', action: 'not-installed' },
88
90
  ]);
89
91
  expect(fs_1.default.existsSync(path_1.default.join(tmpHome, '.awm/hooks/session-start'))).toBe(false);
90
92
  });
@@ -117,6 +119,7 @@ describe('resyncInstalledHooks', () => {
117
119
  expect(results).toEqual([
118
120
  { agent: 'claude-code', action: 'resynced' },
119
121
  { agent: 'codex', action: 'not-installed' },
122
+ { agent: 'cursor', action: 'not-installed' },
120
123
  ]);
121
124
  expect(fs_1.default.lstatSync(skillDest).isSymbolicLink()).toBe(false);
122
125
  const content = fs_1.default.readFileSync(skillDest, 'utf-8');
@@ -135,6 +138,7 @@ describe('resyncInstalledHooks', () => {
135
138
  expect(results).toEqual([
136
139
  { agent: 'claude-code', action: 'resynced' },
137
140
  { agent: 'codex', action: 'not-installed' },
141
+ { agent: 'cursor', action: 'not-installed' },
138
142
  ]);
139
143
  expect(fs_1.default.lstatSync(path_1.default.join(scriptsDir, 'session-start')).isSymbolicLink()).toBe(true);
140
144
  // Task 6: using-awm.md is materialized regardless of the scripts' install method —
@@ -151,6 +155,7 @@ describe('resyncInstalledHooks', () => {
151
155
  expect(results).toEqual([
152
156
  { agent: 'claude-code', action: 'registry-missing' },
153
157
  { agent: 'codex', action: 'not-installed' },
158
+ { agent: 'cursor', action: 'not-installed' },
154
159
  ]);
155
160
  expect(fs_1.default.readFileSync(path_1.default.join(scriptsDir, 'session-start'), 'utf-8')).toContain('OLD');
156
161
  });
@@ -166,6 +171,7 @@ describe('resyncInstalledHooks', () => {
166
171
  expect(results).toEqual([
167
172
  { agent: 'claude-code', action: 'resynced' },
168
173
  { agent: 'codex', action: 'not-installed' },
174
+ { agent: 'cursor', action: 'not-installed' },
169
175
  ]);
170
176
  expect(fs_1.default.existsSync(path_1.default.join(scriptsDir, 'session-start'))).toBe(true);
171
177
  // detectInstallMethod fell back to copy since lstatSync threw
@@ -189,6 +195,7 @@ describe('resyncInstalledHooks', () => {
189
195
  expect(results).toEqual([
190
196
  { agent: 'claude-code', action: 'registry-missing' },
191
197
  { agent: 'codex', action: 'not-installed' },
198
+ { agent: 'cursor', action: 'not-installed' },
192
199
  ]);
193
200
  // old script left intact — never leave user without hook
194
201
  expect(fs_1.default.readFileSync(path_1.default.join(scriptsDir, 'session-start'), 'utf-8')).toContain('OLD');
@@ -219,6 +226,7 @@ describe('resyncInstalledHooks', () => {
219
226
  expect(results).toEqual([
220
227
  { agent: 'claude-code', action: 'not-installed' },
221
228
  { agent: 'codex', action: 'resynced' },
229
+ { agent: 'cursor', action: 'not-installed' },
222
230
  ]);
223
231
  expect(fs_1.default.readFileSync(path_1.default.join(codexScriptsDir, 'session-start'), 'utf-8')).toContain('NEW CODEX');
224
232
  });
@@ -395,20 +395,22 @@ describe('runInit', () => {
395
395
  expect(JSON.parse(fs_1.default.readFileSync(settingsPath, 'utf8'))).toEqual({ pristine: true, unrelated: 'keep' });
396
396
  });
397
397
  // -----------------------------------------------------------------------
398
- // Gap A (QA panel) — GLOBAL-scope render pipeline for Cursor/Copilot was
399
- // only ever exercised via LOCAL-scope `awm add`/bundle-install tests
400
- // (tests/core/bundle-install.test.ts). This drives a real `awm init
401
- // --agent cursor` through the REAL installBundle/applyInstallPlan path
402
- // (no installBundle override — only syncCache is stubbed, to avoid a
403
- // real network clone) and asserts a real rendered .mdc file lands in the
404
- // GLOBAL ~/.cursor/rules directory with the expected frontmatter shape.
398
+ // Gap A (QA panel) — GLOBAL-scope Cursor skill delivery. Plan B installs
399
+ // Cursor skills as links under ~/.agents/skills (shared with Codex/
400
+ // OpenCode) plus native hooks.json entries (registry floor >= 4.9.0).
401
+ // Drives a real `awm init --agent cursor` through installBundle/
402
+ // applyInstallPlan (only syncCache stubbed) and asserts the link + hooks.
405
403
  // -----------------------------------------------------------------------
406
- it('Gap A — real global-scope render: awm init --agent cursor renders a real .mdc into ~/.cursor/rules', async () => {
404
+ it('Gap A — real global-scope link: awm init --agent cursor links using-awm into ~/.agents/skills', async () => {
405
+ const { spawnSync } = require('child_process');
407
406
  const contentRoot = path_1.default.join(process.env.AWM_HOME, 'registries', 'baseline');
408
- // A 'hooks' dir (even empty) is what makes capabilityRoot('hooks') resolve to
409
- // this content root — that's the registryRoot stepContextInjection uses to
410
- // find skills/using-awm/SKILL.md for the (unrelated) context-injection step.
407
+ // A 'hooks' dir is what makes capabilityRoot('hooks') resolve to this content
408
+ // root — registryRoot for stepContextInjection / stepHook. Plan B also needs
409
+ // the three Cursor hook scripts and an exact v4.9.0+ tag for the registry floor.
411
410
  fs_1.default.mkdirSync(path_1.default.join(contentRoot, 'hooks'), { recursive: true });
411
+ for (const name of ['cursor-session-start', 'cursor-pre-compact', 'cursor-post-tool-use']) {
412
+ fs_1.default.writeFileSync(path_1.default.join(contentRoot, 'hooks', name), '#!/usr/bin/env node\nconsole.log("{}")\n', { mode: 0o755 });
413
+ }
412
414
  fs_1.default.mkdirSync(path_1.default.join(contentRoot, 'skills', 'using-awm'), { recursive: true });
413
415
  fs_1.default.writeFileSync(path_1.default.join(contentRoot, 'skills', 'using-awm', 'SKILL.md'), '---\nname: using-awm\ndescription: Use when starting any development conversation\n---\n\nMUST invoke skills per the tiered policy.\n');
414
416
  fs_1.default.mkdirSync(path_1.default.join(contentRoot, 'bundles', 'dev-core'), { recursive: true });
@@ -420,6 +422,10 @@ describe('runInit', () => {
420
422
  name: 'dev-core', version: '1.0.0', description: 'Baseline', scope: 'baseline',
421
423
  dependsOn: [], skills: ['using-awm'], workflows: [], agents: [],
422
424
  }));
425
+ spawnSync('git', ['init', '-q', '-b', 'main'], { cwd: contentRoot, stdio: 'ignore' });
426
+ spawnSync('git', ['-c', 'user.email=t@t.t', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', 'add', '.'], { cwd: contentRoot, stdio: 'ignore' });
427
+ spawnSync('git', ['-c', 'user.email=t@t.t', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', 'commit', '-qm', 'fixture'], { cwd: contentRoot, stdio: 'ignore' });
428
+ spawnSync('git', ['tag', 'v4.9.0'], { cwd: contentRoot, stdio: 'ignore' });
423
429
  const projectCwd = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-init-cursor-cwd-'));
424
430
  try {
425
431
  const { runInit } = require('../../src/commands/init');
@@ -431,20 +437,15 @@ describe('runInit', () => {
431
437
  // seedBaselineRegistry() writes registries.json pointing at 'baseline'
432
438
  // (our manually-seeded content root above) since none exists yet — no
433
439
  // real network clone happens because that content root already exists
434
- // on disk. Only syncCache is stubbed, so stepCache's own sync (the
435
- // content root above has no `.git`, so it reads as "not yet cloned")
436
- // doesn't try to shell out to git either.
440
+ // on disk. Only syncCache is stubbed.
437
441
  actions: { syncCache: async () => { } },
438
442
  });
439
443
  expect(code).toBeLessThanOrEqual(1);
440
- const mdcPath = path_1.default.join(tmpHome, '.cursor', 'rules', 'using-awm.mdc');
441
- expect(fs_1.default.existsSync(path_1.default.join(tmpHome, '.cursor', 'rules', 'using-awm'))).toBe(false);
442
- expect(fs_1.default.existsSync(mdcPath)).toBe(true);
443
- const rendered = fs_1.default.readFileSync(mdcPath, 'utf8');
444
- expect(rendered).toContain('description: Use when starting any development conversation');
445
- expect(rendered).toContain('globs:');
446
- expect(rendered).toContain('alwaysApply: false');
447
- expect(rendered).toContain('MUST invoke skills per the tiered policy.');
444
+ const linked = path_1.default.join(tmpHome, '.agents', 'skills', 'using-awm');
445
+ expect(fs_1.default.existsSync(path_1.default.join(tmpHome, '.cursor', 'rules', 'using-awm.mdc'))).toBe(false);
446
+ expect(fs_1.default.lstatSync(linked).isSymbolicLink() || fs_1.default.existsSync(path_1.default.join(linked, 'SKILL.md'))).toBe(true);
447
+ expect(fs_1.default.readFileSync(path_1.default.join(linked, 'SKILL.md'), 'utf8')).toContain('MUST invoke skills per the tiered policy.');
448
+ expect(fs_1.default.existsSync(path_1.default.join(tmpHome, '.cursor', 'hooks.json'))).toBe(true);
448
449
  }
449
450
  finally {
450
451
  fs_1.default.rmSync(projectCwd, { recursive: true, force: true });
@@ -309,6 +309,32 @@ describe('plan admit Commander wiring', () => {
309
309
  process.exitCode = undefined;
310
310
  }
311
311
  });
312
+ it.each([
313
+ ['a content registry', true, 'admitted'],
314
+ ['an ordinary project', false, 'blocked'],
315
+ ])('applies the R8 sensor opt-out exception only to %s', async (_name, isRegistry, expected) => {
316
+ const root = fs_1.default.realpathSync(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-admit-r8-')));
317
+ const output = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
318
+ if (isRegistry)
319
+ fs_1.default.writeFileSync(path_1.default.join(root, 'awm-registry.json'), JSON.stringify({ minCliVersion: '1.0.0' }));
320
+ const sensors = jest.fn().mockResolvedValue({ overall: 'not_certified', sensors: [], mode: 'opt-out', reason: 'opt-out-declared', projectRoot: root, manifestPath: path_1.default.join(root, '.awm', 'sensors.json') });
321
+ const program = new commander_1.Command();
322
+ (0, plan_1.registerPlanCommand)(program, { validatePlanFile: () => valid, listRegistries: () => [], readPreferences: () => ({ defaultAgent: 'codex', enabledAgents: ['codex'], installMethod: 'symlink', defaultScope: 'local' }), runSensors: sensors });
323
+ try {
324
+ await program.parseAsync(['node', 'awm', 'plan', 'admit', 'plan.md', '--provider', 'codex', '--cwd', root, '--execution-mode', 'interactivo', '--verify-sensors', '--json']);
325
+ const report = JSON.parse(String(output.mock.calls.at(-1)[0]));
326
+ expect(report).toMatchObject({ state: expected, sensors: 'not-certified' });
327
+ if (isRegistry)
328
+ expect(report.sensorEvidence).toBe('r8-registry-opt-out');
329
+ else
330
+ expect(report.diagnostics[0].code).toBe('ADMISSION_SENSORS_BLOCKED');
331
+ }
332
+ finally {
333
+ output.mockRestore();
334
+ fs_1.default.rmSync(root, { recursive: true, force: true });
335
+ process.exitCode = undefined;
336
+ }
337
+ });
312
338
  it('derives desatendido from the canonical plan header; a native v1 plan needs no journal (opt-in durable custody)', async () => {
313
339
  const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-admit-header-'));
314
340
  const output = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
@@ -462,7 +488,11 @@ describe('plan admit Commander wiring', () => {
462
488
  process.exitCode = undefined;
463
489
  }
464
490
  });
465
- it.each([['cursor', 'global'], ['cursor', 'local'], ['copilot', 'local']])('does not prove absence for actual unowned rendered %s %s runtime contracts', async (target, scope) => {
491
+ // Cursor skills are link-installed under ~/.agents/skills (same as Codex); only
492
+ // content renderers still need this "rendered copy ≠ ownership" proof. Link
493
+ // unowned cases are covered by the runtime-unowned scenarios above.
494
+ it('does not prove absence for actual unowned rendered copilot local runtime contracts', async () => {
495
+ const target = 'copilot';
466
496
  const root = fs_1.default.realpathSync(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-rendered-admission-')));
467
497
  const oldHome = process.env.HOME;
468
498
  process.env.HOME = path_1.default.join(root, 'home');
@@ -474,7 +504,7 @@ describe('plan admit Commander wiring', () => {
474
504
  fs_1.default.writeFileSync(path_1.default.join(registryRoot, 'awm-registry.json'), JSON.stringify({ minCliVersion: '1.0.0' }));
475
505
  fs_1.default.writeFileSync(path_1.default.join(root, 'source.md'), 'project source');
476
506
  const provider = (0, providers_1.providerFor)(target);
477
- const directory = scope === 'global' ? provider.skill.global : path_1.default.resolve(root, provider.skill.local);
507
+ const directory = path_1.default.resolve(root, provider.skill.local);
478
508
  fs_1.default.mkdirSync(directory, { recursive: true });
479
509
  const installedFile = path_1.default.join(directory, (0, registry_1.renderedFilename)('using-awm', provider.skill.renderer));
480
510
  fs_1.default.writeFileSync(installedFile, (0, registry_1.renderArtifact)(provider.skill.renderer, skillSource));
@@ -514,6 +544,56 @@ describe('plan admit Commander wiring', () => {
514
544
  process.exitCode = undefined;
515
545
  }
516
546
  });
547
+ it.each([['cursor', 'global'], ['cursor', 'local']])('does not prove absence for actual unowned linked %s %s runtime contracts', async (target, scope) => {
548
+ const root = fs_1.default.realpathSync(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-linked-admission-')));
549
+ const oldHome = process.env.HOME;
550
+ process.env.HOME = path_1.default.join(root, 'home');
551
+ const outputSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
552
+ const registryRoot = path_1.default.join(root, 'registry');
553
+ fs_1.default.mkdirSync(path_1.default.join(registryRoot, 'skills'), { recursive: true });
554
+ fs_1.default.writeFileSync(path_1.default.join(registryRoot, 'awm-registry.json'), JSON.stringify({ minCliVersion: '1.0.0' }));
555
+ fs_1.default.writeFileSync(path_1.default.join(root, 'source.md'), 'project source');
556
+ const unowned = path_1.default.join(root, 'unowned', 'using-awm');
557
+ fs_1.default.mkdirSync(unowned, { recursive: true });
558
+ fs_1.default.writeFileSync(path_1.default.join(unowned, 'SKILL.md'), '---\nname: using-awm\ndescription: contract\n---\nRuntime contract.\n');
559
+ const provider = (0, providers_1.providerFor)(target);
560
+ const directory = scope === 'global' ? provider.skill.global : path_1.default.resolve(root, provider.skill.local);
561
+ fs_1.default.mkdirSync(directory, { recursive: true });
562
+ const artifact = path_1.default.join(directory, (0, registry_1.renderedFilename)('using-awm', provider.skill.renderer));
563
+ fs_1.default.symlinkSync(unowned, artifact, process.platform === 'win32' ? 'junction' : 'dir');
564
+ const sensors = jest.fn();
565
+ const program = new commander_1.Command();
566
+ (0, plan_1.registerPlanCommand)(program, {
567
+ validatePlanFile: () => ({ ...valid, manifest: { ...valid.manifest, sources: [{ id: 'source', path: 'source.md', locator: 'contract', fact: 'contract' }] } }),
568
+ listRegistries: () => [{ name: 'fixture', remote: 'https://example.invalid/fixture.git', contentRoot: registryRoot }],
569
+ readPreferences: () => ({ defaultAgent: target, enabledAgents: [target], installMethod: 'symlink', defaultScope: 'local' }),
570
+ checkCurrentness: async () => ({ checkedAt: 'x', compatibility: { status: 'not-checked' }, components: [{ component: 'cli', installed: '1.0.0', latest: '1.0.0', channel: 'stable', source: 'fixture', checkedAt: 'x', status: 'current', detail: 'ok', remedy: 'none' }] }),
571
+ runSensors: sensors,
572
+ });
573
+ try {
574
+ await program.parseAsync(['node', 'awm', 'plan', 'admit', 'plan.md', '--provider', target, '--cwd', root, '--execution-mode', 'interactivo', '--require-current', '--verify-sensors', '--json']);
575
+ const output = JSON.parse(String(outputSpy.mock.calls.at(-1)[0]));
576
+ expect(output.diagnostics[0].code).toBe('ADMISSION_CURRENTNESS_PROVENANCE_REQUIRED');
577
+ expect(output.state).toBe('blocked');
578
+ const reported = /^Runtime artifact (.+) has no unique physical registry owner/.exec(output.diagnostics[0].message);
579
+ expect(reported).not.toBeNull();
580
+ const reportedIdentity = fs_1.default.lstatSync(reported[1], { bigint: true });
581
+ const installedIdentity = fs_1.default.lstatSync(artifact, { bigint: true });
582
+ expect(reportedIdentity.isSymbolicLink() || reportedIdentity.isDirectory()).toBe(true);
583
+ expect(reportedIdentity.ino).not.toBe(0n);
584
+ expect([reportedIdentity.dev, reportedIdentity.ino]).toEqual([installedIdentity.dev, installedIdentity.ino]);
585
+ expect(sensors).not.toHaveBeenCalled();
586
+ }
587
+ finally {
588
+ if (oldHome === undefined)
589
+ delete process.env.HOME;
590
+ else
591
+ process.env.HOME = oldHome;
592
+ outputSpy.mockRestore();
593
+ fs_1.default.rmSync(root, { recursive: true, force: true });
594
+ process.exitCode = undefined;
595
+ }
596
+ });
517
597
  it.each(['symlink', 'directory', 'oversized', 'duplicate-floor', 'unsafe-floor', 'stale-before-compatibility'])('keeps registry manifest %s noncertifying before sensors', async (scenario) => {
518
598
  const root = fs_1.default.realpathSync(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-manifest-admission-')));
519
599
  const registryRoot = path_1.default.join(root, 'registry');
@@ -161,21 +161,30 @@ describe('runCommand — exit codes and output', () => {
161
161
  });
162
162
  itLinux('never lets a structured capture file grow beyond maxBuffer', async () => {
163
163
  const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-exec-cap-'));
164
- const marker = path_1.default.join(dir, 'written');
165
- const existing = new Set(fs_1.default.readdirSync(os_1.default.tmpdir()).filter(name => name.startsWith('awm-sensor-output-')));
164
+ // The overflow watcher kills the child and deletes the capture as soon as
165
+ // the cap is reached, so polling for the file races that cleanup. Capture
166
+ // files only grow, so their size at deletion is the largest they ever were.
167
+ const captureSizes = [];
168
+ const rmSync = fs_1.default.rmSync.bind(fs_1.default);
169
+ const removal = jest.spyOn(fs_1.default, 'rmSync').mockImplementation((target, options) => {
170
+ if (typeof target === 'string' && path_1.default.basename(target).startsWith('awm-sensor-output-')) {
171
+ captureSizes.push(fs_1.default.statSync(path_1.default.join(target, 'stdout')).size);
172
+ }
173
+ return rmSync(target, options);
174
+ });
166
175
  try {
167
- const result = (0, exec_1.runStructuredCommand)({
176
+ const result = await (0, exec_1.runStructuredCommand)({
168
177
  executable: 'node',
169
178
  resolution: 'path',
170
- args: ['-e', `const fs = require('fs'); try { fs.writeSync(1, Buffer.alloc(8192, 'x')); } catch {} fs.writeFileSync(${JSON.stringify(marker)}, 'written'); setTimeout(() => {}, 1000);`],
179
+ args: ['-e', `const fs = require('fs'); try { fs.writeSync(1, Buffer.alloc(8192, 'x')); } catch {} setTimeout(() => {}, 1000);`],
171
180
  }, { timeout: 5_000, cwd: dir, maxBuffer: 1_024 });
172
- expect(await until(() => fs_1.default.existsSync(marker))).toBe(true);
173
- const capture = fs_1.default.readdirSync(os_1.default.tmpdir()).find(name => name.startsWith('awm-sensor-output-') && !existing.has(name));
174
- expect(capture).toBeDefined();
175
- expect(fs_1.default.statSync(path_1.default.join(os_1.default.tmpdir(), capture, 'stdout')).size).toBeLessThanOrEqual(1_024);
176
- await expect(result).resolves.toMatchObject({ overflowed: true });
181
+ expect(result).toMatchObject({ overflowed: true });
182
+ expect(captureSizes).toHaveLength(1);
183
+ expect(captureSizes[0]).toBeGreaterThan(0);
184
+ expect(captureSizes[0]).toBeLessThanOrEqual(1_024);
177
185
  }
178
186
  finally {
187
+ removal.mockRestore();
179
188
  fs_1.default.rmSync(dir, { recursive: true, force: true });
180
189
  }
181
190
  });
@@ -178,6 +178,45 @@ describe('admitPlan', () => {
178
178
  expect(report.diagnostics[0]).toMatchObject({ code: 'ADMISSION_CONTROLLER_AUTONOMY_REQUIRED' });
179
179
  });
180
180
  });
181
+ // R8: a content registry whose sensors are ALL deliberately disabled may close
182
+ // work; its release proof lives in CI. The local verdict stays not-certified —
183
+ // never reported as pass — and the exception is named in the evidence.
184
+ describe('the R8 registry opt-out closure exception', () => {
185
+ const optOut = { overall: 'not_certified', sensors: [], mode: 'opt-out', reason: 'opt-out-declared', projectRoot: '/r', manifestPath: '/r/.awm/sensors.json' };
186
+ const unattended = () => ({ ...valid, manifest: { ...valid.manifest, executionMode: 'desatendido' } });
187
+ const base = () => ({ plan: valid, provider: 'codex', cwd: process.cwd(), enabledAgents: ['codex'], verifySensors: true, sensors: optOut, sensorClosurePolicy: 'r8-registry-opt-out' });
188
+ it('admits interactive work without reporting the opt-out as a pass', async () => {
189
+ const report = await (0, admission_1.admitPlan)(base());
190
+ expect(report).toMatchObject({ state: 'admitted', sensors: 'not-certified', sensorEvidence: 'r8-registry-opt-out' });
191
+ });
192
+ it('admits journal-less unattended work under the same exception', async () => {
193
+ const report = await (0, admission_1.admitPlan)({ ...base(), plan: unattended(), planPath: 'docs/plan.md', controllerAutonomy: 'approval-free' });
194
+ expect(report).toMatchObject({ state: 'admitted', executionMode: 'desatendido', journal: 'not-required', sensors: 'not-certified', sensorEvidence: 'r8-registry-opt-out' });
195
+ });
196
+ it('reports the preserved verdict when a later gate blocks', async () => {
197
+ const report = await (0, admission_1.admitPlan)({ ...base(), plan: unattended(), planPath: 'docs/plan.md' });
198
+ expect(report).toMatchObject({ state: 'blocked', sensors: 'not-certified', sensorEvidence: 'r8-registry-opt-out' });
199
+ expect(report.diagnostics[0]).toMatchObject({ code: 'ADMISSION_CONTROLLER_AUTONOMY_REQUIRED' });
200
+ });
201
+ it.each([
202
+ ['no declared policy', { sensorClosurePolicy: undefined }],
203
+ ['a failing sensor', { sensors: { ...optOut, overall: 'fail', mode: 'project-sensors' } }],
204
+ ['an absent manifest', { sensors: { ...optOut, mode: 'missing', reason: 'manifest-absent' } }],
205
+ ['a native gate', { sensors: { ...optOut, mode: 'native-gate', reason: 'native-gate-declared' } }],
206
+ ['an unavailable sensor source', { sensors: { ...optOut, mode: 'source-unavailable' } }],
207
+ ['all-disabled legacy sensors', { sensors: { ...optOut, overall: 'skipped', mode: 'project-sensors' } }],
208
+ ])('keeps blocking with %s', async (_name, fields) => {
209
+ const report = await (0, admission_1.admitPlan)({ ...base(), ...fields });
210
+ expect(report.state).toBe('blocked');
211
+ expect(report.diagnostics[0]).toMatchObject({ code: 'ADMISSION_SENSORS_BLOCKED' });
212
+ expect(report.sensorEvidence).toBeUndefined();
213
+ });
214
+ it('accepts only the known sensor evidence values at the public sanitizer', () => {
215
+ const report = { state: 'admitted', planState: 'valid', journal: 'not-required', currentness: 'not-checked', sensors: 'not-certified', diagnostics: [] };
216
+ expect((0, admission_1.sanitizeAdmissionReport)({ ...report, sensorEvidence: 'r8-registry-opt-out' }).sensorEvidence).toBe('r8-registry-opt-out');
217
+ expect(() => (0, admission_1.sanitizeAdmissionReport)({ ...report, sensorEvidence: 'opt-out' })).toThrow('invalid report');
218
+ });
219
+ });
181
220
  it('admits unattended work only with an exact schema-2 plan binding', async () => {
182
221
  const unattended = { ...valid, manifest: { ...valid.manifest, executionMode: 'desatendido' } };
183
222
  const journal = { ...(0, types_1.emptyState)('main'), schema: 2, planBinding: { path: 'docs/plan.md', digest: 'a'.repeat(64), schema: 'compact-slices/v1', executionMode: 'desatendido', boundAt: '2026-09-15T00:00:00.000Z' } };
@@ -284,12 +284,8 @@ describe('installBundle', () => {
284
284
  expect(fs_1.default.existsSync(path_1.default.join(projectRoot, '.awm/profile.json'))).toBe(true);
285
285
  expect(fs_1.default.existsSync(path_1.default.join(projectRoot, '.gitignore'))).toBe(true);
286
286
  });
287
- it('materializes a skill as a rendered Cursor .mdc via the real default applyInstallPlan (Task 4.3 e2e)', () => {
287
+ it('materializes a Cursor skill as a link under .agents/skills via the real default applyInstallPlan (Plan B e2e)', () => {
288
288
  const { content, projectRoot, bundles } = makeFixture();
289
- // s-base's fixture SKILL.md (makeFixture, above) has no `description`
290
- // field — real for a bare skill fixture, but cursor-mdc.ts's renderer
291
- // requires one (parseSkillSource). Seed a real description here so
292
- // this is a genuine end-to-end render, not just a source-existence check.
293
289
  fs_1.default.writeFileSync(path_1.default.join(content, 'skills', 's-base', 'SKILL.md'), '---\nname: s-base\ndescription: Base skill for bundle-install fixtures\n---\n\nDo the base thing.\n');
294
290
  const base = bundles.find((bundle) => bundle.name === 'base');
295
291
  const result = (0, bundle_install_1.installBundle)({
@@ -300,15 +296,12 @@ describe('installBundle', () => {
300
296
  projectRoot,
301
297
  contentDir: content,
302
298
  });
303
- const mdcPath = path_1.default.join(projectRoot, '.cursor/rules/s-base.mdc');
304
- expect(fs_1.default.existsSync(path_1.default.join(projectRoot, '.cursor/rules/s-base'))).toBe(false);
305
- expect(fs_1.default.existsSync(mdcPath)).toBe(true);
306
- const rendered = fs_1.default.readFileSync(mdcPath, 'utf8');
307
- expect(rendered).toContain('description: Base skill for bundle-install fixtures');
308
- expect(rendered).toContain('alwaysApply: false');
309
- expect(rendered).toContain('Do the base thing.');
299
+ const linked = path_1.default.join(projectRoot, '.agents/skills/s-base');
300
+ expect(fs_1.default.existsSync(path_1.default.join(projectRoot, '.cursor/rules/s-base.mdc'))).toBe(false);
301
+ expect(fs_1.default.lstatSync(linked).isSymbolicLink() || fs_1.default.existsSync(path_1.default.join(linked, 'SKILL.md'))).toBe(true);
302
+ expect(fs_1.default.readFileSync(path_1.default.join(linked, 'SKILL.md'), 'utf8')).toContain('Do the base thing.');
310
303
  expect(result.transactionId).toBeTruthy();
311
- expect(result.modifiedFiles).toContain(mdcPath);
304
+ expect(result.modifiedFiles).toContain(linked);
312
305
  });
313
306
  it('materializes a skill as rendered Copilot .instructions.md via the real default applyInstallPlan (Task 4.3 e2e)', () => {
314
307
  const { content, projectRoot, bundles } = makeFixture();
@@ -338,7 +331,8 @@ describe('installBundle', () => {
338
331
  bundleName: 'base', bundles, agents: ['cursor'],
339
332
  method: 'symlink', projectRoot, contentDir: content,
340
333
  });
341
- expect(fs_1.default.existsSync(path_1.default.join(projectRoot, '.cursor/rules/s-base.mdc'))).toBe(true);
334
+ expect(fs_1.default.existsSync(path_1.default.join(projectRoot, '.agents/skills/s-base'))).toBe(true);
335
+ expect(fs_1.default.existsSync(path_1.default.join(projectRoot, '.cursor/rules/s-base.mdc'))).toBe(false);
342
336
  expect(cursorResult.recordedExtension).toBe('base');
343
337
  const copilotResult = (0, bundle_install_1.addBundle)({
344
338
  bundleName: 'base', bundles, agents: ['copilot'],
@@ -162,14 +162,8 @@ describe('CodexAgentsStrategy', () => {
162
162
  expect(fs_1.default.existsSync(path_1.default.join(project, 'AGENTS.md'))).toBe(false);
163
163
  expect(fs_1.default.existsSync(path_1.default.join(project, '.cursor'))).toBe(false);
164
164
  });
165
- it('regression: local-scope context injection + project constitution injection no longer collide on the same AGENTS.md managed block (R4 QA blocker)', () => {
166
- // Before the fix: inject() (context) and injectProject() (constitution) both wrote
167
- // the SAME single-slot managed block in <projectRoot>/AGENTS.md for a local-scope
168
- // provider (Cursor/Copilot) — whichever ran second silently discarded the other's
169
- // content. stepContextInjection runs before stepConstitutionInjection in the real
170
- // init orchestrator (init/orchestrator.ts), so constitution always won, and the real
171
- // AWM skill/context guidance never survived a real `awm init --agent cursor` run.
172
- const project = path_1.default.join(tmpWork, 'collision-repo');
165
+ it('Cursor inject writes PROJECT_GUIDANCE only (using-awm lives in the hook — Plan B R4/R10)', () => {
166
+ const project = path_1.default.join(tmpWork, 'cursor-guidance-only');
173
167
  fs_1.default.mkdirSync(project, { recursive: true });
174
168
  const materialized = path_1.default.join(project, '.awm/context/awm-context.md');
175
169
  const contextMarkdown = '# AWM\n\nUse `development-process`. MUST invoke skills per policy.';
@@ -186,15 +180,40 @@ describe('CodexAgentsStrategy', () => {
186
180
  projectRoot: project,
187
181
  };
188
182
  expect(strategy.inject(input, provider)).toBe('injected');
189
- expect(strategy.injectProject(project, provider, 'cursor')).toBe('injected'); // carrier only
183
+ expect(strategy.injectProject(project, provider, 'cursor')).toBe('injected'); // awm.mdc carrier
190
184
  const written = fs_1.default.readFileSync(path_1.default.join(project, 'AGENTS.md'), 'utf8');
191
- expect(written).toContain('MUST invoke skills per policy');
192
185
  expect(written).toContain('Read and obey `CONSTITUTION.md` before work');
186
+ expect(written).not.toContain('MUST invoke skills');
187
+ expect(written).not.toContain('# AWM');
193
188
  expect(strategy.status(input, provider)).toBe('injected');
194
- // Idempotent: a second full pass (context re-inject, then constitution/carrier) changes nothing.
189
+ const mdc = fs_1.default.readFileSync(path_1.default.join(project, '.cursor/rules/awm.mdc'), 'utf8');
190
+ expect(mdc).toContain('alwaysApply: true');
191
+ expect(mdc).toContain('Read and obey `CONSTITUTION.md` before work');
195
192
  expect(strategy.inject(input, provider)).toBe('unchanged');
196
193
  expect(strategy.injectProject(project, provider, 'cursor')).toBe('unchanged');
197
- expect(fs_1.default.readFileSync(path_1.default.join(project, 'AGENTS.md'), 'utf8')).toBe(written);
194
+ });
195
+ it('Copilot inject still writes full using-awm plus PROJECT_GUIDANCE (sole carrier)', () => {
196
+ const project = path_1.default.join(tmpWork, 'copilot-full-context');
197
+ fs_1.default.mkdirSync(project, { recursive: true });
198
+ const materialized = path_1.default.join(project, '.awm/context/awm-context.md');
199
+ const contextMarkdown = '# AWM\n\nUse `development-process`. MUST invoke skills per policy.';
200
+ fs_1.default.mkdirSync(path_1.default.dirname(materialized), { recursive: true });
201
+ fs_1.default.writeFileSync(materialized, contextMarkdown);
202
+ const provider = copilotProvider();
203
+ const strategy = new codex_agents_1.CodexAgentsStrategy();
204
+ const input = {
205
+ ref: { absPath: materialized, scope: 'local', contentHash: (0, provider_1.sha256)(contextMarkdown) },
206
+ registryRoot: '/registry',
207
+ installMethod: 'copy',
208
+ agent: 'copilot',
209
+ scope: 'local',
210
+ projectRoot: project,
211
+ };
212
+ expect(strategy.inject(input, provider)).toBe('injected');
213
+ const written = fs_1.default.readFileSync(path_1.default.join(project, 'AGENTS.md'), 'utf8');
214
+ expect(written).toContain('MUST invoke skills per policy');
215
+ expect(written).toContain('Read and obey `CONSTITUTION.md` before work');
216
+ expect(strategy.status(input, provider)).toBe('injected');
198
217
  });
199
218
  it('implements global status and remove while preserving user bytes', () => {
200
219
  const file = path_1.default.join(tmpHome, '.codex/AGENTS.md');