agentic-workflow-manager 9.16.0 → 9.17.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 (36) hide show
  1. package/dist/src/commands/preflight/checks.js +63 -13
  2. package/dist/src/commands/sensors/bootstrap.js +2 -1
  3. package/dist/src/commands/sensors/compatibility/contract.js +30 -26
  4. package/dist/src/commands/sensors/compatibility/discovery.js +21 -0
  5. package/dist/src/commands/sensors/compatibility/manifest.js +169 -17
  6. package/dist/src/commands/sensors/compatibility/materialize.js +30 -5
  7. package/dist/src/commands/sensors/compatibility/source.js +3 -2
  8. package/dist/src/commands/sensors/coverage/render.js +8 -2
  9. package/dist/src/commands/sensors/coverage/resolve.js +7 -0
  10. package/dist/src/commands/sensors/init.js +15 -2
  11. package/dist/src/commands/sensors/migrate.js +13 -10
  12. package/dist/src/commands/sensors/prepare.js +53 -6
  13. package/dist/src/commands/sensors/project.js +60 -6
  14. package/dist/src/commands/sensors/result.js +18 -3
  15. package/dist/src/commands/sensors/run.js +124 -16
  16. package/dist/src/commands/sensors/status.js +163 -18
  17. package/dist/src/core/init/steps.js +5 -1
  18. package/dist/src/core/journal/process.js +22 -8
  19. package/dist/tests/commands/preflight/preflight.test.js +121 -0
  20. package/dist/tests/commands/sensors/bootstrap.test.js +10 -0
  21. package/dist/tests/commands/sensors/compatibility/manifest.test.js +204 -0
  22. package/dist/tests/commands/sensors/init.test.js +107 -0
  23. package/dist/tests/commands/sensors/migrate.test.js +4 -3
  24. package/dist/tests/commands/sensors/partial-pack-bootstrap.test.js +1 -1
  25. package/dist/tests/commands/sensors/prepare.test.js +62 -0
  26. package/dist/tests/commands/sensors/result.test.js +66 -0
  27. package/dist/tests/commands/sensors/run.test.js +331 -0
  28. package/dist/tests/commands/sensors/status.test.js +204 -0
  29. package/dist/tests/core/journal/process.test.js +33 -10
  30. package/dist/tests/structural/native-release-artifacts.test.js +4 -2
  31. package/dist/tests/structural/project-declared-sensors-docs-contract.test.js +104 -0
  32. package/package.json +1 -1
  33. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  34. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  35. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  36. package/prebuilds/win32-x64/secure_fs.node +0 -0
@@ -7,6 +7,7 @@ const fs_1 = __importDefault(require("fs"));
7
7
  const path_1 = __importDefault(require("path"));
8
8
  const os_1 = __importDefault(require("os"));
9
9
  const status_1 = require("../../../src/commands/sensors/status");
10
+ const coverage_1 = require("../../../src/commands/sensors/coverage");
10
11
  jest.mock('../../../src/commands/sensors/exec', () => ({
11
12
  runCommand: jest.fn(),
12
13
  runStructuredCommand: jest.fn(),
@@ -409,3 +410,206 @@ describe('computeSensorStatus', () => {
409
410
  }
410
411
  });
411
412
  });
413
+ describe('project-declared status and coverage (S3)', () => {
414
+ let tmpDir;
415
+ let pathDir;
416
+ let originalPath;
417
+ beforeEach(() => {
418
+ tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-s3-'));
419
+ pathDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-s3-path-'));
420
+ originalPath = process.env.PATH;
421
+ process.env.PATH = pathDir;
422
+ fs_1.default.writeFileSync(path_1.default.join(pathDir, 'terraform'), '#!/bin/sh\nexit 0\n');
423
+ fs_1.default.chmodSync(path_1.default.join(pathDir, 'terraform'), 0o755);
424
+ });
425
+ afterEach(() => {
426
+ if (originalPath === undefined)
427
+ delete process.env.PATH;
428
+ else
429
+ process.env.PATH = originalPath;
430
+ fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
431
+ fs_1.default.rmSync(pathDir, { recursive: true, force: true });
432
+ });
433
+ function writeOnlyProject(sensors) {
434
+ fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
435
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
436
+ schemaVersion: 3,
437
+ mode: 'project-sensors',
438
+ sensors,
439
+ }));
440
+ }
441
+ it('lists each only-project sensor with project-declared provenance (RF-2.8)', async () => {
442
+ writeOnlyProject({
443
+ 'iac-format': {
444
+ source: 'project',
445
+ enabled: true,
446
+ command: { executable: 'terraform', resolution: 'path', args: ['fmt', '-check'] },
447
+ formatter: 'exit-code',
448
+ },
449
+ });
450
+ const result = await (0, status_1.computeSensorStatus)(tmpDir);
451
+ expect(result.checks['iac-format']).toMatchObject({
452
+ ok: true,
453
+ certification: 'project-declared',
454
+ });
455
+ expect(result.checks['iac-format'].detail).toMatch(/project-declared/i);
456
+ expect(result.overall).not.toBe('NOT_CONFIGURED');
457
+ // Must not be empty Pack:none / DEGRADED with zero detail solely because pack is absent.
458
+ expect(result.pack === null || result.pack === undefined).toBe(true);
459
+ expect(Object.keys(result.checks)).toEqual(expect.arrayContaining(['iac-format']));
460
+ });
461
+ it('reports coverage as inconclusive / no pack-reference for only-project (RF-2.9)', async () => {
462
+ writeOnlyProject({
463
+ 'iac-format': {
464
+ source: 'project',
465
+ enabled: true,
466
+ command: { executable: 'terraform', resolution: 'path', args: ['fmt', '-check'] },
467
+ formatter: 'exit-code',
468
+ },
469
+ });
470
+ const coverage = await (0, coverage_1.runCoverage)(tmpDir);
471
+ expect(coverage.overall).toBe('inconclusive');
472
+ expect(coverage.static.status).toBe('inconclusive');
473
+ expect(coverage.static.reason).toBe('no_reference');
474
+ expect(coverage.static.classes).toEqual([]);
475
+ expect(coverage.overall).not.toBe('covered');
476
+ expect(JSON.stringify(coverage)).not.toMatch(/"certification"\s*:\s*"certified"/);
477
+ });
478
+ it('soft-isolates valid siblings when one project entry is schema-invalid (status vs run)', async () => {
479
+ writeOnlyProject({
480
+ good: {
481
+ source: 'project',
482
+ enabled: true,
483
+ command: { executable: 'terraform', resolution: 'path', args: ['fmt', '-check'] },
484
+ formatter: 'exit-code',
485
+ },
486
+ bad: {
487
+ source: 'project',
488
+ enabled: true,
489
+ command: { executable: 'terraform', resolution: 'path', args: ['fmt', '-check'] },
490
+ formatter: 'not-a-formatter',
491
+ },
492
+ });
493
+ const result = await (0, status_1.computeSensorStatus)(tmpDir);
494
+ expect(Object.keys(result.checks).length).toBeGreaterThan(0);
495
+ expect(result.checks['good']).toMatchObject({
496
+ ok: true,
497
+ certification: 'project-declared',
498
+ });
499
+ expect(result.checks['bad']).toMatchObject({ ok: false });
500
+ expect(result.checks['bad'].detail).toMatch(/formatter|invalid/i);
501
+ expect(result.overall).toBe('DEGRADED');
502
+ });
503
+ });
504
+ describe('project-declared status pack collision (S3 RF-1.7)', () => {
505
+ let project;
506
+ let home;
507
+ let pathDir;
508
+ let originalPath;
509
+ let originalHome;
510
+ const version = '10.0.0';
511
+ beforeEach(() => {
512
+ project = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-collision-'));
513
+ home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-collision-home-'));
514
+ pathDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-collision-path-'));
515
+ originalPath = process.env.PATH;
516
+ originalHome = process.env.AWM_HOME;
517
+ process.env.PATH = pathDir;
518
+ process.env.AWM_HOME = home;
519
+ fs_1.default.writeFileSync(path_1.default.join(pathDir, 'node'), '#!/bin/sh\nexit 0\n');
520
+ fs_1.default.chmodSync(path_1.default.join(pathDir, 'node'), 0o755);
521
+ const registry = path_1.default.join(home, 'registries', 'baseline', 'sensor-packs', 'js-ts');
522
+ fs_1.default.mkdirSync(registry, { recursive: true });
523
+ fs_1.default.writeFileSync(path_1.default.join(registry, 'eslint.config.awm.mjs'), 'export default []');
524
+ fs_1.default.writeFileSync(path_1.default.join(home, 'registries.json'), JSON.stringify([{ name: 'baseline', remote: 'https://example.test/baseline.git' }]));
525
+ fs_1.default.writeFileSync(path_1.default.join(project, 'package.json'), JSON.stringify({ scripts: { lint: 'eslint .' }, devDependencies: { eslint: '^10.0.0' } }));
526
+ fs_1.default.mkdirSync(path_1.default.join(project, 'node_modules', 'eslint'), { recursive: true });
527
+ fs_1.default.writeFileSync(path_1.default.join(project, 'node_modules', 'eslint', 'package.json'), JSON.stringify({ version }));
528
+ fs_1.default.mkdirSync(path_1.default.join(project, 'node_modules', '.bin'), { recursive: true });
529
+ fs_1.default.writeFileSync(path_1.default.join(project, 'node_modules', '.bin', 'eslint'), '');
530
+ fs_1.default.writeFileSync(path_1.default.join(registry, 'pack.json'), JSON.stringify({
531
+ schemaVersion: 2, name: 'js-ts', description: 'fixture', detects: ['package.json'],
532
+ coverage: { schemaVersion: 1, classes: { lint: { description: 'lint', detectors: [{ sensor: 'lint' }], remedy: { summary: 'fix', command: 'awm sensors init' } } } },
533
+ sensors: {
534
+ lint: {
535
+ fast: true,
536
+ applicability: { allFiles: ['package.json'] },
537
+ variants: [{
538
+ id: 'eslint-10', priority: 1, certifiedRange: '>=10 <11',
539
+ requirements: { tool: 'eslint', toolRange: '>=10 <11', runtime: 'node', runtimeRange: '>=0' },
540
+ assets: ['eslint.config.awm.mjs'], formatter: 'generic',
541
+ probe: { kind: 'package-script-present', script: 'lint' },
542
+ command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
543
+ }],
544
+ },
545
+ },
546
+ }));
547
+ fs_1.default.mkdirSync(path_1.default.join(project, '.awm'));
548
+ fs_1.default.writeFileSync(path_1.default.join(project, 'eslint.config.awm.mjs'), 'export default []');
549
+ });
550
+ afterEach(() => {
551
+ if (originalPath === undefined)
552
+ delete process.env.PATH;
553
+ else
554
+ process.env.PATH = originalPath;
555
+ if (originalHome === undefined)
556
+ delete process.env.AWM_HOME;
557
+ else
558
+ process.env.AWM_HOME = originalHome;
559
+ fs_1.default.rmSync(project, { recursive: true, force: true });
560
+ fs_1.default.rmSync(home, { recursive: true, force: true });
561
+ fs_1.default.rmSync(pathDir, { recursive: true, force: true });
562
+ });
563
+ it('does not label a colliding project id as project-declared READY (RF-1.7 status)', async () => {
564
+ fs_1.default.writeFileSync(path_1.default.join(project, '.awm', 'sensors.json'), JSON.stringify({
565
+ schemaVersion: 3,
566
+ mode: 'project-sensors',
567
+ pack: 'js-ts',
568
+ source: { registry: 'baseline' },
569
+ sensors: {
570
+ lint: {
571
+ source: 'project',
572
+ enabled: true,
573
+ command: { executable: 'node', resolution: 'path', args: ['-e', 'process.exit(0)'] },
574
+ formatter: 'exit-code',
575
+ },
576
+ },
577
+ }));
578
+ const result = await (0, status_1.computeSensorStatus)(project);
579
+ expect(result.checks['lint']).toBeDefined();
580
+ expect(result.checks['lint'].certification).not.toBe('project-declared');
581
+ expect(result.checks['lint'].detail).toMatch(/project-sensor-name-collision|pack (sensor )?kept/i);
582
+ expect(result.overall).not.toBe('READY');
583
+ });
584
+ it('soft-isolate + pack + colliding project id does not certify project-declared READY (RF-1.7)', async () => {
585
+ // Invalid sibling forces softIsolatedChecks; lint collides with live pack id.
586
+ fs_1.default.writeFileSync(path_1.default.join(project, '.awm', 'sensors.json'), JSON.stringify({
587
+ schemaVersion: 3,
588
+ mode: 'project-sensors',
589
+ pack: 'js-ts',
590
+ source: { registry: 'baseline' },
591
+ sensors: {
592
+ lint: {
593
+ source: 'project',
594
+ enabled: true,
595
+ command: { executable: 'node', resolution: 'path', args: ['-e', 'process.exit(0)'] },
596
+ formatter: 'exit-code',
597
+ },
598
+ bad: {
599
+ source: 'project',
600
+ enabled: true,
601
+ command: { executable: 'node', resolution: 'path', args: ['-e', 'process.exit(0)'] },
602
+ formatter: 'not-a-formatter',
603
+ },
604
+ },
605
+ }));
606
+ const result = await (0, status_1.computeSensorStatus)(project);
607
+ expect(result.mode).toBe('invalid');
608
+ expect(result.checks['bad']).toMatchObject({ ok: false });
609
+ expect(result.checks['lint']).toBeDefined();
610
+ expect(result.checks['lint'].ok).toBe(false);
611
+ expect(result.checks['lint'].certification).not.toBe('project-declared');
612
+ expect(result.checks['lint'].detail).toMatch(/project-sensor-name-collision|pack (sensor )?kept/i);
613
+ expect(result.overall).not.toBe('READY');
614
+ });
615
+ });
@@ -202,28 +202,51 @@ describe('process identity', () => {
202
202
  test('ignores malformed ps lines instead of inventing processes', () => {
203
203
  expect((0, process_1.descendantActivity)('garbage\n 20 10 00:00:01\n\n x y z', 10)).toMatch(/^1:/);
204
204
  });
205
+ test('keeps Darwin-style rows with blank or dotted cputime in the tree', () => {
206
+ // macOS `ps -o time=` is often `0:00.01`; a brand-new process can also
207
+ // omit the third column entirely on some samples.
208
+ expect((0, process_1.descendantActivity)(' 10 1 0:00.05\n 20 10 0:00.01\n 30 20\n', 10)).toMatch(/^2:/);
209
+ expect((0, process_1.descendantActivity)(' 10 1\n 20 10\n', 10)).toMatch(/^1:/);
210
+ });
205
211
  });
206
212
  test('activitySnapshot ve un nieto fuera del process group del controlador', async () => {
207
213
  if ((0, paths_1.isWindowsNative)())
208
214
  return; // win32 degrada activity sin ps (ver activitySnapshot)
209
- const script = "setTimeout(() => { require('child_process').spawn(process.execPath, ['-e', 'setTimeout(()=>{}, 4000)'], { detached: true, stdio: 'ignore' }).unref(); }, 400); setTimeout(()=>{}, 4000)";
215
+ const script = "setTimeout(() => { require('child_process').spawn(process.execPath, ['-e', 'setTimeout(()=>{}, 15000)'], { detached: true, stdio: 'ignore' }).unref(); }, 200); setTimeout(()=>{}, 15000)";
210
216
  const { child, ref } = (0, process_1.spawnStructured)(['node', '-e', script], process.cwd(), 'n-desc');
211
217
  try {
212
- const before = (0, process_1.activitySnapshot)(ref);
213
- expect(before).not.toBeNull();
214
- let after = (0, process_1.activitySnapshot)(ref);
215
- for (let i = 0; i < 40 && after !== null && after.descendants === before.descendants; i++) {
218
+ // Wait for a concrete baseline (not `unknown` from a transient ps miss).
219
+ let before = (0, process_1.activitySnapshot)(ref);
220
+ for (let i = 0; i < 50 && (before === null || before.descendants === 'unknown'); i++) {
216
221
  await new Promise((resolve) => setTimeout(resolve, 50));
217
- after = (0, process_1.activitySnapshot)(ref);
222
+ before = (0, process_1.activitySnapshot)(ref);
223
+ }
224
+ expect(before).not.toBeNull();
225
+ expect(before.descendants).not.toBe('unknown');
226
+ const beforeGroup = before.groupSize;
227
+ let after = before;
228
+ const deadline = Date.now() + 8000;
229
+ while (Date.now() < deadline) {
230
+ const snap = (0, process_1.activitySnapshot)(ref);
231
+ if (snap !== null)
232
+ after = snap;
233
+ // Success: same process group, at least one PPID-descendant appeared.
234
+ if (after.groupSize === beforeGroup && /^[1-9]\d*:/.test(after.descendants))
235
+ break;
236
+ await new Promise((resolve) => setTimeout(resolve, 100));
218
237
  }
219
- expect(after).not.toBeNull();
220
238
  // The detached grandchild lives in its own process group: the group
221
239
  // signal cannot see it, the descendant signal must.
222
- expect(after.groupSize).toBe(before.groupSize);
223
- expect(after.descendants).not.toBe(before.descendants);
224
- expect(after.descendants).toMatch(/^1:/);
240
+ expect(after.groupSize).toBe(beforeGroup);
241
+ expect(after.descendants).toMatch(/^[1-9]\d*:/);
225
242
  }
226
243
  finally {
244
+ // Detached grandchildren are outside `-PGID`; reap by parent pid too.
245
+ try {
246
+ const { execFileSync } = require('child_process');
247
+ execFileSync('pkill', ['-P', String(ref.pid)], { stdio: 'ignore', timeout: 1000 });
248
+ }
249
+ catch { /* ya ausente */ }
227
250
  try {
228
251
  process.kill(-ref.processGroup, 'SIGKILL');
229
252
  }
@@ -17,8 +17,10 @@ function assertArmPartition(source) {
17
17
  const [matrixJob, rest] = workflow.split(/^ windows-arm-hot:\s*$/m);
18
18
  expect(rest).toBeDefined();
19
19
  const hotJob = rest.split(/^ [a-z][\w-]*:\s*$/m)[0];
20
- expect(matrixJob).toMatch(/- name: Tests?\n if: matrix\.target != 'win32-arm64'\n working-directory: cli\n run: npx jest --runInBand --bail\n/);
21
- const remainder = matrixJob.match(/- name: Tests? \(Windows ARM remainder\)\n if: matrix\.target == 'win32-arm64'\n working-directory: cli\n run: ([^\n]+)/)?.[1];
20
+ // Optional env: (e.g. AWM_BASELINE_REGISTRY_ROOT for RF-5.1 docs contract) may
21
+ // sit between working-directory and run; release.yml still omits it.
22
+ expect(matrixJob).toMatch(/- name: Tests?\n if: matrix\.target != 'win32-arm64'\n working-directory: cli\n(?: env:\n(?: [^\n]+\n)+)? run: npx jest --runInBand --bail\n/);
23
+ const remainder = matrixJob.match(/- name: Tests? \(Windows ARM remainder\)\n if: matrix\.target == 'win32-arm64'\n working-directory: cli\n(?: env:\n(?: [^\n]+\n)+)? run: ([^\n]+)/)?.[1];
22
24
  expect(remainder).toMatch(/^npx jest --runInBand --bail --testPathIgnorePatterns='[^']+'$/);
23
25
  const ignorePattern = remainder?.match(/--testPathIgnorePatterns='([^']+)'/)?.[1];
24
26
  expect(ignorePattern).toBe(String.raw `node_modules|[/\\]cli[/\\]tests[/\\]commands[/\\]watch[/\\]track-(finalize|freeze)\.test\.ts$`);
@@ -0,0 +1,104 @@
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 path_1 = __importDefault(require("path"));
8
+ /**
9
+ * CMD-DOCS — RF-5.1 / RF-5.2 (project-declared sensors documentation).
10
+ *
11
+ * Resolves awm-baseline-registry from (in order):
12
+ * 1. `AWM_BASELINE_REGISTRY_ROOT` (CI docs checkout)
13
+ * 2. nested `repoRoot/awm-baseline-registry` (CI support-matrix layout)
14
+ * 3. sibling `repoRoot/../awm-baseline-registry` (maintainer workspace)
15
+ *
16
+ * Prefer a checkout that already contains RF-5.1 (`## Project sensors vs packs`).
17
+ * Missing / too-old checkouts fail loudly (no silent skip).
18
+ */
19
+ const repoRoot = path_1.default.resolve(__dirname, '../../..');
20
+ const cliReferencePath = path_1.default.join(repoRoot, 'docs', 'cli-reference.md');
21
+ const RF51_HEADING = '## Project sensors vs packs';
22
+ function setupSensorsSkillPath(registryRoot) {
23
+ return path_1.default.join(registryRoot, 'skills', 'setup-sensors', 'SKILL.md');
24
+ }
25
+ function resolveSetupSensorsPath() {
26
+ const envRoot = process.env.AWM_BASELINE_REGISTRY_ROOT?.trim();
27
+ // When CI/maintainer sets the override, that root is authoritative (no silent
28
+ // fallback to a nested immutable pin that lacks RF-5.1 prose).
29
+ const candidates = envRoot
30
+ ? [path_1.default.resolve(envRoot)]
31
+ : [
32
+ path_1.default.join(repoRoot, 'awm-baseline-registry'),
33
+ path_1.default.join(repoRoot, '..', 'awm-baseline-registry'),
34
+ ];
35
+ const existing = candidates
36
+ .map(root => ({ root, skill: setupSensorsSkillPath(root) }))
37
+ .filter(entry => fs_1.default.existsSync(entry.skill));
38
+ if (existing.length === 0) {
39
+ throw new Error('Baseline registry checkout missing for RF-5.1 docs contract. Tried:\n'
40
+ + candidates.map(root => ` - ${setupSensorsSkillPath(root)}`).join('\n')
41
+ + '\nClone awm-baseline-registry as a sibling, nest it under the CLI repo, '
42
+ + 'or set AWM_BASELINE_REGISTRY_ROOT to a checkout that includes setup-sensors RF-5.1.');
43
+ }
44
+ const withRf51 = existing.find(entry => fs_1.default.readFileSync(entry.skill, 'utf8').includes(RF51_HEADING));
45
+ if (!withRf51) {
46
+ throw new Error('Baseline registry checkout is present but too old for RF-5.1 '
47
+ + `(missing ${RF51_HEADING}). Checked:\n`
48
+ + existing.map(entry => ` - ${entry.skill}`).join('\n')
49
+ + '\nPoint AWM_BASELINE_REGISTRY_ROOT at a registry tip that documents project-declared sensors '
50
+ + '(coordinated awm-baseline-registry PR), without replacing the immutable v2.0.1 support-matrix pin.');
51
+ }
52
+ return withRf51.skill;
53
+ }
54
+ function sensorsSection(text) {
55
+ const start = text.indexOf('## Sensors (per-project computational checks)');
56
+ expect(start).toBeGreaterThanOrEqual(0);
57
+ const rest = text.slice(start);
58
+ const nextH2 = rest.search(/\n## (?!Sensors)/);
59
+ return nextH2 === -1 ? rest : rest.slice(0, nextH2);
60
+ }
61
+ /** Skill body after YAML frontmatter (content after the closing `---`). */
62
+ function skillBody(skill) {
63
+ const match = skill.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n([\s\S]*)$/);
64
+ if (!match) {
65
+ throw new Error('setup-sensors SKILL.md missing YAML frontmatter delimiters');
66
+ }
67
+ return match[1];
68
+ }
69
+ /** Named H2 section inside the skill body; fails if the heading is absent. */
70
+ function skillSection(body, heading) {
71
+ const start = body.indexOf(heading);
72
+ expect(start).toBeGreaterThanOrEqual(0);
73
+ const rest = body.slice(start);
74
+ const nextH2 = rest.search(/\n## /);
75
+ return nextH2 === -1 ? rest : rest.slice(0, nextH2);
76
+ }
77
+ describe('project-declared sensors docs contract (CMD-DOCS)', () => {
78
+ it('cli-reference sensors section documents project markers, provenance, exit-code, and non-gating (RF-5.2)', () => {
79
+ const text = fs_1.default.readFileSync(cliReferencePath, 'utf8');
80
+ const section = sensorsSection(text);
81
+ expect(section).toMatch(/source:\s*"project"|source"\s*:\s*"project"/);
82
+ expect(section).toContain('project-declared');
83
+ expect(section).toContain('exit-code');
84
+ expect(section).toMatch(/provenance[\s\S]{0,220}(?:does not|never)\s+(?:block|blocks)[\s\S]{0,100}(?:unattended|gates)/i);
85
+ // Optional pack (omit or null) — RF-5.2 clause already in cli-reference prose.
86
+ expect(section).toMatch(/[Pp]ack may be omitted or set\s+to\s+`?null`?[\s\S]{0,120}project-declared/i);
87
+ // JSON-vs-schema diagnostics distinction — RF-5.2.
88
+ expect(section).toMatch(/not valid JSON[\s\S]{0,220}schema-invalid/i);
89
+ });
90
+ it('setup-sensors skill documents project vs pack, JSON example, and non-gating provenance (RF-5.1)', () => {
91
+ const setupSensorsPath = resolveSetupSensorsPath();
92
+ const skill = fs_1.default.readFileSync(setupSensorsPath, 'utf8');
93
+ const body = skillBody(skill);
94
+ // Scope when-to-use / project-vs-packs (and related RF-5.1 prose) to the
95
+ // dedicated body section — frontmatter description must not satisfy these.
96
+ const projectVsPacks = skillSection(body, RF51_HEADING);
97
+ // Both guidance clauses are required — Prefer|Declare OR let either alone satisfy.
98
+ expect(projectVsPacks).toMatch(/Prefer a \*\*pack\*\*/);
99
+ expect(projectVsPacks).toMatch(/Declare a \*\*project sensor\*\*\s+when\s+the check is project-owned/i);
100
+ expect(projectVsPacks).toMatch(/"source"\s*:\s*"project"/);
101
+ expect(projectVsPacks).toMatch(/```json[\s\S]*?"source"\s*:\s*"project"[\s\S]*?```/);
102
+ expect(projectVsPacks).toMatch(/project-declared[\s\S]{0,220}(?:does not|never)\s+(?:block|blocks)[\s\S]{0,100}unattended/i);
103
+ });
104
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-workflow-manager",
3
- "version": "9.16.0",
3
+ "version": "9.17.0",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"
Binary file
Binary file