agentic-workflow-manager 9.20.4 → 9.22.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 (42) hide show
  1. package/dist/src/commands/change/index.js +14 -3
  2. package/dist/src/commands/preflight/checks.js +31 -0
  3. package/dist/src/commands/sensors/compatibility/contract.js +56 -3
  4. package/dist/src/commands/sensors/compatibility/manifest.js +16 -2
  5. package/dist/src/commands/sensors/index.js +10 -1
  6. package/dist/src/commands/sensors/init.js +7 -1
  7. package/dist/src/commands/sensors/inputs-digest.js +315 -0
  8. package/dist/src/commands/sensors/prepare.js +10 -0
  9. package/dist/src/commands/sensors/result-cache.js +278 -0
  10. package/dist/src/commands/sensors/result.js +31 -12
  11. package/dist/src/commands/sensors/run.js +265 -8
  12. package/dist/src/commands/sensors/status.js +66 -7
  13. package/dist/src/commands/watch/init.js +3 -15
  14. package/dist/src/core/awm-gitignore.js +211 -0
  15. package/dist/src/core/changes/receipt.js +219 -9
  16. package/dist/src/core/diagnostics/checks.js +11 -0
  17. package/dist/tests/commands/change/new.test.js +1 -1
  18. package/dist/tests/commands/change/validate-unmeasured.test.js +246 -0
  19. package/dist/tests/commands/preflight/awm-gitignore.test.js +52 -0
  20. package/dist/tests/commands/preflight/sensors-inputs.test.js +110 -0
  21. package/dist/tests/commands/sensors/compatibility/contract.test.js +60 -0
  22. package/dist/tests/commands/sensors/compatibility/manifest.test.js +94 -0
  23. package/dist/tests/commands/sensors/init-gitignore.test.js +59 -0
  24. package/dist/tests/commands/sensors/inputs-digest.test.js +373 -0
  25. package/dist/tests/commands/sensors/result-cache.test.js +352 -0
  26. package/dist/tests/commands/sensors/run-cache.test.js +434 -0
  27. package/dist/tests/commands/sensors/run-slice.test.js +171 -0
  28. package/dist/tests/commands/sensors/status-inputs.test.js +271 -0
  29. package/dist/tests/commands/watch/init-gitignore.test.js +34 -0
  30. package/dist/tests/commands/watch/track-freeze.test.js +11 -7
  31. package/dist/tests/commands/watch/watch-init.test.js +3 -1
  32. package/dist/tests/core/awm-gitignore.test.js +58 -0
  33. package/dist/tests/core/changes/receipt.test.js +261 -2
  34. package/dist/tests/core/diagnostics/awm-gitignore.test.js +101 -0
  35. package/dist/tests/core/ledger/gitignore.test.js +6 -1
  36. package/dist/tests/structural/sensor-execution-optimization-docs-contract.test.js +102 -0
  37. package/dist/tests/structural/sensor-slice-decision-contract.test.js +63 -0
  38. package/package.json +2 -1
  39. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  40. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  41. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  42. package/prebuilds/win32-x64/secure_fs.node +0 -0
@@ -0,0 +1,246 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ const fs_1 = __importDefault(require("fs"));
40
+ const path_1 = __importDefault(require("path"));
41
+ const helpers_1 = require("../../core/changes/helpers");
42
+ const change_1 = require("../../../src/commands/change");
43
+ const receipt_1 = require("../../../src/core/changes/receipt");
44
+ const types_1 = require("../../../src/core/changes/types");
45
+ const gitModule = __importStar(require("../../../src/core/changes/git"));
46
+ const REAL_CHANGES_DIR = path_1.default.resolve(__dirname, '..', '..', '..', '..', 'docs', 'changes');
47
+ const listReal = () => { try {
48
+ return fs_1.default.readdirSync(REAL_CHANGES_DIR).sort();
49
+ }
50
+ catch {
51
+ return null;
52
+ } };
53
+ const realBefore = listReal();
54
+ afterEach(() => expect(listReal()).toEqual(realBefore));
55
+ let repo;
56
+ let template;
57
+ const now = () => new Date('2026-10-17T12:00:00.000Z');
58
+ const REL = 'docs/changes/2026-10-17-rec.md';
59
+ const abs = (rel) => path_1.default.join(repo.root, ...rel.split('/'));
60
+ beforeAll(() => {
61
+ template = (0, helpers_1.makeRepo)();
62
+ template.git('checkout', '-q', '-b', 'feat/x');
63
+ // Track sensors.json / baseline for RF-4.5 always-measured late-path cases (default makeRepo ignores `.awm/`).
64
+ template.commit({
65
+ '.gitignore': 'node_modules\n.awm/sensors/runs\n.awm/journal\n.awm/ledger\n.awm/evidence\n',
66
+ 'cli/src/a.ts': '1',
67
+ 'cli/package.json': '{"name":"cli"}',
68
+ 'cli/package-lock.json': '{"lockfileVersion":3}',
69
+ '.awm/sensors.json': '{"schemaVersion":2,"pack":"js-ts","sensors":{}}',
70
+ '.awm/sensors.baseline.json': '{}',
71
+ 'README.md': '# root\n',
72
+ }, 'feat(changes): a');
73
+ });
74
+ afterAll(() => template.cleanup());
75
+ beforeEach(() => { repo = (0, helpers_1.cloneRepo)(template); });
76
+ afterEach(() => { jest.restoreAllMocks(); repo.cleanup(); });
77
+ const DIGEST = `sha256:${'a'.repeat(64)}`;
78
+ const FINGERPRINT = `sha256:${'b'.repeat(64)}`;
79
+ function measurement(overrides = {}) {
80
+ return {
81
+ inputs: ['src/**/*.ts'],
82
+ packageRootRel: 'cli',
83
+ inputDigest: DIGEST,
84
+ sensorFingerprint: FINGERPRINT,
85
+ measuredPaths: ['cli/src/a.ts', '.awm/sensors.json', 'cli/package.json', 'cli/package-lock.json'],
86
+ ...overrides,
87
+ };
88
+ }
89
+ function writeSchema2Receipt(args) {
90
+ const head = repo.git('rev-parse', 'HEAD');
91
+ const sensors = args.sensors.map((name) => ({
92
+ name,
93
+ status: 'pass',
94
+ errors: [],
95
+ ...(args.measurements?.[name]
96
+ ? { measurement: args.measurements[name] }
97
+ : {}),
98
+ }));
99
+ (0, receipt_1.writeReceipt)(repo.root, (0, receipt_1.buildReceipt)({
100
+ commit: head,
101
+ ranAt: '2026-10-17T11:00:00.000Z',
102
+ cliVersion: '9.21.0',
103
+ output: { overall: 'pass', sensors },
104
+ alwaysMeasuredPaths: args.alwaysMeasuredPaths ?? [
105
+ '.awm/sensors.json',
106
+ '.awm/sensors.baseline.json',
107
+ 'cli/package.json',
108
+ 'cli/package-lock.json',
109
+ ],
110
+ }));
111
+ }
112
+ function commitRecord(slug = 'rec', receipt = () => writeSchema2Receipt({
113
+ sensors: ['lint', 'typecheck'],
114
+ measurements: { lint: measurement(), typecheck: measurement({ inputs: ['src/**/*.ts'] }) },
115
+ })) {
116
+ receipt();
117
+ expect((0, change_1.runChangeNew)(repo.root, slug, {}, { now }).code).toBe(0);
118
+ const rel = `docs/changes/2026-10-17-${slug}.md`;
119
+ fs_1.default.writeFileSync(abs(rel), fs_1.default.readFileSync(abs(rel), 'utf8').split(types_1.PENDING_MARKER).join('filled'));
120
+ repo.git('add', '-A');
121
+ repo.git('commit', '-q', '-m', `docs(changes): ${slug}`);
122
+ return rel;
123
+ }
124
+ const run = (target = undefined, opts = {}) => (0, change_1.runChangeValidate)(repo.root, target, { json: true, ...opts });
125
+ const report = (r) => JSON.parse(r.stdout);
126
+ const diagsOf = (r, index = 0) => report(r).records[index].diagnostics;
127
+ const codesOf = (r, index = 0) => diagsOf(r, index).map((d) => d.code);
128
+ function diag(r, code, field) {
129
+ const found = diagsOf(r).filter((d) => d.code === code);
130
+ expect(found).toHaveLength(1);
131
+ expect(found[0].field).toBe(field);
132
+ return found[0];
133
+ }
134
+ describe('RF-4.5/4.6 schema 2 unmeasured late paths', () => {
135
+ test('RF-4.5: README-only after verify is OK when every sensor had authored inputs', () => {
136
+ commitRecord();
137
+ repo.commit({ 'README.md': '# docs only\n' }, 'docs: readme');
138
+ expect(codesOf(run())).not.toContain('code-changed-after-verification');
139
+ expect(report(run()).state).toBe('valid');
140
+ });
141
+ test('RF-4.6: any sensor without authored inputs keeps schema-1 late rules (README fails)', () => {
142
+ commitRecord('rec', () => writeSchema2Receipt({
143
+ sensors: ['lint', 'test'],
144
+ measurements: { lint: measurement() }, // test has no measurement / no authored inputs
145
+ }));
146
+ repo.commit({ 'README.md': '# docs only\n' }, 'docs: readme');
147
+ expect(codesOf(run())).toContain('code-changed-after-verification');
148
+ expect(diag(run(), 'code-changed-after-verification', undefined).message).toContain('README.md');
149
+ });
150
+ test('RF-4.5: measured path late still fails when relaxation applies', () => {
151
+ commitRecord();
152
+ repo.commit({ 'cli/src/a.ts': '2' }, 'fix: measured');
153
+ expect(codesOf(run())).toContain('code-changed-after-verification');
154
+ expect(diag(run(), 'code-changed-after-verification', undefined).message).toContain('cli/src/a.ts');
155
+ });
156
+ test('RF-4.5: NEW file matching authored inputs globs after verify fails (not frozen measuredPaths only)', () => {
157
+ // Certify with measuredPaths frozen to a.ts only; a later matching source must still count.
158
+ commitRecord('rec', () => writeSchema2Receipt({
159
+ sensors: ['lint'],
160
+ measurements: {
161
+ lint: measurement({
162
+ inputs: ['src/**/*.ts'],
163
+ packageRootRel: 'cli',
164
+ measuredPaths: ['cli/src/a.ts', '.awm/sensors.json', 'cli/package.json', 'cli/package-lock.json'],
165
+ }),
166
+ },
167
+ }));
168
+ repo.commit({ 'cli/src/brand-new.ts': 'export const x = 1;\n' }, 'feat: new matching source');
169
+ expect(codesOf(run())).toContain('code-changed-after-verification');
170
+ expect(diag(run(), 'code-changed-after-verification', undefined).message).toContain('cli/src/brand-new.ts');
171
+ });
172
+ test('RF-4.5: sensors.json late fails (always-measured)', () => {
173
+ commitRecord();
174
+ repo.commit({ '.awm/sensors.json': '{"schemaVersion":2,"pack":"js-ts","sensors":{"lint":{"enabled":false}}}' }, 'chore: sensors');
175
+ expect(codesOf(run())).toContain('code-changed-after-verification');
176
+ expect(diag(run(), 'code-changed-after-verification', undefined).message).toContain('.awm/sensors.json');
177
+ });
178
+ test('RF-4.5: lockfile late fails (always-measured)', () => {
179
+ commitRecord();
180
+ repo.commit({ 'cli/package-lock.json': '{"lockfileVersion":3,"x":1}' }, 'chore: lock');
181
+ expect(codesOf(run())).toContain('code-changed-after-verification');
182
+ expect(diag(run(), 'code-changed-after-verification', undefined).message).toContain('cli/package-lock.json');
183
+ });
184
+ test('RF-4.5: baseline.json late fails (validate always-measured includes baseline, unlike cache)', () => {
185
+ commitRecord();
186
+ repo.commit({ '.awm/sensors.baseline.json': '{"lint":[]}' }, 'chore: baseline');
187
+ expect(codesOf(run())).toContain('code-changed-after-verification');
188
+ expect(diag(run(), 'code-changed-after-verification', undefined).message).toContain('.awm/sensors.baseline.json');
189
+ });
190
+ test('RF-4.8: monorepo packageRoot paths compare as repo-root POSIX', () => {
191
+ commitRecord('rec', () => writeSchema2Receipt({
192
+ sensors: ['lint'],
193
+ measurements: {
194
+ lint: measurement({
195
+ inputs: ['src/**/*.ts'],
196
+ measuredPaths: ['cli/src/a.ts', '.awm/sensors.json', 'cli/package.json', 'cli/package-lock.json'],
197
+ }),
198
+ },
199
+ }));
200
+ repo.commit({ 'cli/src/a.ts': 'changed' }, 'fix: under packageRoot');
201
+ const d = diag(run(), 'code-changed-after-verification', undefined);
202
+ expect(d.message).toContain('cli/src/a.ts');
203
+ expect(d.message).not.toMatch(/(^|[^/])src\/a\.ts/);
204
+ });
205
+ test('schema 1 receipts keep strict late rules (only docs/changes carved out)', () => {
206
+ const head = repo.git('rev-parse', 'HEAD');
207
+ // Force a schema-1 write through the legacy shape helper used by older tests.
208
+ const { writeReceipt: write, computeReceiptDigest } = require('../../../src/core/changes/receipt');
209
+ const body = {
210
+ schema: 1,
211
+ commit: head,
212
+ ranAt: '2026-10-17T11:00:00.000Z',
213
+ cliVersion: '9.20.0',
214
+ overall: 'pass',
215
+ sensors: { lint: 'pass' },
216
+ };
217
+ // buildReceipt now yields schema 2; write a raw schema-1 receipt for this fixture.
218
+ const digest = computeReceiptDigest(body);
219
+ write(repo.root, { ...body, digest });
220
+ expect((0, change_1.runChangeNew)(repo.root, 'rec', {}, { now }).code).toBe(0);
221
+ fs_1.default.writeFileSync(abs(REL), fs_1.default.readFileSync(abs(REL), 'utf8').split(types_1.PENDING_MARKER).join('filled'));
222
+ repo.git('add', '-A');
223
+ repo.git('commit', '-q', '-m', 'docs(changes): rec');
224
+ repo.commit({ 'README.md': '# docs\n' }, 'docs: readme');
225
+ expect(codesOf(run())).toContain('code-changed-after-verification');
226
+ });
227
+ test('RF-E.2: git failure listing late files is git-failed (fail closed)', () => {
228
+ commitRecord();
229
+ const verified = repo.git('rev-parse', 'HEAD~1');
230
+ const real = gitModule.changedFiles;
231
+ jest.spyOn(gitModule, 'changedFiles').mockImplementation((root, from, to) => (from === verified ? null : real(root, from, to)));
232
+ const r = run();
233
+ expect(r.code).toBe(2);
234
+ expect(diag(r, 'git-failed', undefined).message).toContain('git could not list the files changed since');
235
+ });
236
+ });
237
+ describe('RF-4.7 change new remedy honesty', () => {
238
+ test('missing HEAD receipt remedy names full-gate sensors run and does not claim every binary must re-exec', () => {
239
+ const r = (0, change_1.runChangeNew)(repo.root, 'rec', {}, { now });
240
+ expect(r.code).toBe(2);
241
+ expect(r.stderr).toMatch(/^receipt-missing: /);
242
+ expect(r.stderr).toMatch(/full-gate awm sensors run/);
243
+ expect(r.stderr.toLowerCase()).not.toMatch(/every tool|re-exec|reexecute|must re-run every/);
244
+ expect(r.stderr).toMatch(/cache/i);
245
+ });
246
+ });
@@ -0,0 +1,52 @@
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 child_process_1 = require("child_process");
10
+ const checks_1 = require("../../../src/commands/preflight/checks");
11
+ const awm_gitignore_1 = require("../../../src/core/awm-gitignore");
12
+ function git(cwd, args) {
13
+ (0, child_process_1.execFileSync)('git', args, { cwd, stdio: 'pipe' });
14
+ }
15
+ describe('preflight awm-gitignore check (GI-6/GI-7)', () => {
16
+ let repo;
17
+ beforeEach(() => {
18
+ repo = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-gi-pf-'));
19
+ git(repo, ['init']);
20
+ git(repo, ['config', 'user.email', 't@t']);
21
+ git(repo, ['config', 'user.name', 't']);
22
+ });
23
+ afterEach(() => {
24
+ fs_1.default.rmSync(repo, { recursive: true, force: true });
25
+ });
26
+ it('degrades when versioned sensors.json is ignored', async () => {
27
+ fs_1.default.mkdirSync(path_1.default.join(repo, '.awm'), { recursive: true });
28
+ fs_1.default.writeFileSync(path_1.default.join(repo, '.awm', 'sensors.json'), JSON.stringify({
29
+ schemaVersion: 3, mode: 'opt-out', reason: 'test',
30
+ }));
31
+ fs_1.default.writeFileSync(path_1.default.join(repo, '.gitignore'), '.awm/\n');
32
+ const report = await (0, checks_1.preflight)(repo);
33
+ const check = report.checks.find((c) => c.id === 'awm-gitignore');
34
+ expect(check).toBeDefined();
35
+ expect(check.ok).toBe(false);
36
+ expect(check.advisory).not.toBe(true);
37
+ expect(check.remedy).toMatch(/awm sensors init|awm watch init/);
38
+ expect(report.status).not.toBe('ready');
39
+ });
40
+ it('includes a passing awm-gitignore check when managed block is present for opt-out manifest', async () => {
41
+ fs_1.default.mkdirSync(path_1.default.join(repo, '.awm'), { recursive: true });
42
+ fs_1.default.writeFileSync(path_1.default.join(repo, '.awm', 'sensors.json'), JSON.stringify({
43
+ schemaVersion: 3, mode: 'opt-out', reason: 'test',
44
+ }));
45
+ (0, awm_gitignore_1.upsertAwmGitignore)(repo);
46
+ git(repo, ['add', '-A']);
47
+ git(repo, ['commit', '-m', 'ok']);
48
+ const report = await (0, checks_1.preflight)(repo);
49
+ const check = report.checks.find((c) => c.id === 'awm-gitignore');
50
+ expect(check).toMatchObject({ ok: true });
51
+ });
52
+ });
@@ -0,0 +1,110 @@
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 checks_1 = require("../../../src/commands/preflight/checks");
10
+ const preflight_1 = require("../../../src/commands/preflight");
11
+ /**
12
+ * RF-3.4 — preflight surfaces a non-blocking (advisory) warning when configured
13
+ * enabled sensors lack effective `inputs`. Opt-out / native-gate must stay quiet.
14
+ */
15
+ describe('preflight sensors-inputs warning (RF-3.4)', () => {
16
+ const dirs = [];
17
+ const homes = [];
18
+ let previousHome;
19
+ beforeEach(() => {
20
+ previousHome = process.env.AWM_HOME;
21
+ const home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-pf-inputs-home-'));
22
+ homes.push(home);
23
+ process.env.AWM_HOME = home;
24
+ fs_1.default.writeFileSync(path_1.default.join(home, 'registries.json'), JSON.stringify([]));
25
+ });
26
+ afterEach(() => {
27
+ if (previousHome === undefined)
28
+ delete process.env.AWM_HOME;
29
+ else
30
+ process.env.AWM_HOME = previousHome;
31
+ });
32
+ afterAll(() => {
33
+ for (const dir of dirs)
34
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
35
+ for (const home of homes)
36
+ fs_1.default.rmSync(home, { recursive: true, force: true });
37
+ });
38
+ function project(opts = {}) {
39
+ const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-pf-inputs-'));
40
+ dirs.push(dir);
41
+ for (const f of opts.context ?? ['AGENTS.md']) {
42
+ fs_1.default.writeFileSync(path_1.default.join(dir, f), '# ctx\n');
43
+ }
44
+ for (const b of opts.bins ?? ['eslint']) {
45
+ fs_1.default.mkdirSync(path_1.default.join(dir, 'node_modules', '.bin'), { recursive: true });
46
+ fs_1.default.writeFileSync(path_1.default.join(dir, 'node_modules', '.bin', b), '');
47
+ }
48
+ if (opts.manifest !== undefined) {
49
+ fs_1.default.mkdirSync(path_1.default.join(dir, '.awm'), { recursive: true });
50
+ fs_1.default.writeFileSync(path_1.default.join(dir, '.awm', 'sensors.json'), typeof opts.manifest === 'string' ? opts.manifest : JSON.stringify(opts.manifest));
51
+ }
52
+ return dir;
53
+ }
54
+ function check(report, id) {
55
+ return report.checks.find(c => c.id === id);
56
+ }
57
+ function onlyProjectSensor(extra = {}) {
58
+ return {
59
+ schemaVersion: 3,
60
+ mode: 'project-sensors',
61
+ sensors: {
62
+ custom: {
63
+ source: 'project',
64
+ enabled: true,
65
+ command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
66
+ formatter: 'exit-code',
67
+ ...extra,
68
+ },
69
+ },
70
+ };
71
+ }
72
+ it('emits an advisory sensors-inputs warning when an enabled sensor lacks inputs', async () => {
73
+ const dir = project({ manifest: onlyProjectSensor() });
74
+ const report = await (0, checks_1.preflight)(dir);
75
+ expect(report.status).toBe('ready');
76
+ expect((0, preflight_1.exitCodeFor)(report)).toBe(0);
77
+ expect(check(report, 'sensors-inputs')).toMatchObject({
78
+ ok: false,
79
+ advisory: true,
80
+ detail: expect.stringMatching(/custom/i),
81
+ remedy: expect.stringMatching(/inputs|cache/i),
82
+ });
83
+ expect(check(report, 'sensors-inputs').detail).toMatch(/not cacheable|no effective inputs|lacks inputs/i);
84
+ expect((0, preflight_1.formatReport)(report)).toMatch(/⚠.*sensors-inputs/s);
85
+ });
86
+ it('omits sensors-inputs when every enabled sensor has effective inputs', async () => {
87
+ const dir = project({ manifest: onlyProjectSensor({ inputs: ['**/*.ts'] }) });
88
+ const report = await (0, checks_1.preflight)(dir);
89
+ expect(report.status).toBe('ready');
90
+ expect(check(report, 'sensors-inputs')).toBeUndefined();
91
+ });
92
+ it('does not warn for disabled sensors that lack inputs', async () => {
93
+ const dir = project({
94
+ manifest: onlyProjectSensor({ enabled: false }),
95
+ });
96
+ const report = await (0, checks_1.preflight)(dir);
97
+ expect(check(report, 'sensors-inputs')).toBeUndefined();
98
+ });
99
+ it.each([
100
+ ['native-gate', 'CI is authoritative.', 'native_gate'],
101
+ ['opt-out', 'No local gate.', 'ungated'],
102
+ ])('emits no sensors-inputs warning for v3 %s', async (mode, reason, status) => {
103
+ const dir = project({
104
+ manifest: { schemaVersion: 3, mode, reason },
105
+ });
106
+ const report = await (0, checks_1.preflight)(dir);
107
+ expect(report.status).toBe(status);
108
+ expect(check(report, 'sensors-inputs')).toBeUndefined();
109
+ });
110
+ });
@@ -302,4 +302,64 @@ describe('sensor pack v2 contract', () => {
302
302
  ])('rejects malformed pack %j', (input, message) => {
303
303
  expect(() => (0, contract_1.parseSensorPack)(input, 'pack.json')).toThrow(message);
304
304
  });
305
+ describe('inputs and sensor-level closureOnly (RF-1.1, RF-1.2, RF-1.5, RF-1.7)', () => {
306
+ it('accepts inputs on variants and closureOnly on the sensor (RF-1.2 pack defaults)', () => {
307
+ const pack = validPack();
308
+ pack.sensors.lint.closureOnly = true;
309
+ pack.sensors.lint.variants[0].inputs = ['**/*.ts', '**/*.[cm]js'];
310
+ expect((0, contract_1.parseSensorPack)(pack, 'pack.json')).toMatchObject({
311
+ kind: 'v2',
312
+ pack: {
313
+ sensors: {
314
+ lint: {
315
+ closureOnly: true,
316
+ variants: [{ inputs: ['**/*.ts', '**/*.[cm]js'] }],
317
+ },
318
+ },
319
+ },
320
+ });
321
+ });
322
+ it('treats omitted sensor closureOnly as false and does not invent variant inputs', () => {
323
+ const parsed = (0, contract_1.parseSensorPack)(validPack(), 'pack.json');
324
+ expect(parsed).toMatchObject({ kind: 'v2' });
325
+ if (parsed.kind !== 'v2')
326
+ throw new Error('expected v2 pack');
327
+ const lint = parsed.pack.sensors.lint;
328
+ expect(lint.closureOnly ?? false).toBe(false);
329
+ expect('inputs' in lint.variants[0]).toBe(false);
330
+ });
331
+ it('rejects closureOnly on a variant (RF-1.7 sensor-level only)', () => {
332
+ const pack = validPack();
333
+ pack.sensors.lint.variants[0].closureOnly = true;
334
+ expect(() => (0, contract_1.parseSensorPack)(pack, 'pack.json')).toThrow(/unknown field "closureOnly"/);
335
+ });
336
+ it('rejects inputs on the sensor object (inputs belong on variants)', () => {
337
+ const pack = validPack();
338
+ pack.sensors.lint.inputs = ['**/*.ts'];
339
+ expect(() => (0, contract_1.parseSensorPack)(pack, 'pack.json')).toThrow(/unknown field "inputs"/);
340
+ });
341
+ it.each([
342
+ [[''], /nonempty single-line/],
343
+ [['/abs'], /absolute paths are invalid/],
344
+ [['has\u0000nul'], /NUL or control/],
345
+ [['../escape'], /contained relative glob/],
346
+ [['src/{a,b}.ts'], /brace expansion/],
347
+ [['!negated.ts'], /negation/],
348
+ [[], /must be a nonempty array/],
349
+ [['@(a|b).ts'], /extglob|parentheses/],
350
+ [['+(a).ts'], /extglob|parentheses/],
351
+ [['*(a).ts'], /extglob|parentheses/],
352
+ [['?(a).ts'], /extglob|parentheses/],
353
+ [['(a|b).ts'], /extglob|parentheses/],
354
+ ])('rejects invalid variant inputs %j', (inputs, reason) => {
355
+ const pack = validPack();
356
+ pack.sensors.lint.variants[0].inputs = inputs;
357
+ expect(() => (0, contract_1.parseSensorPack)(pack, 'pack.json')).toThrow(reason);
358
+ });
359
+ it('rejects non-boolean sensor closureOnly', () => {
360
+ const pack = validPack();
361
+ pack.sensors.lint.closureOnly = 1;
362
+ expect(() => (0, contract_1.parseSensorPack)(pack, 'pack.json')).toThrow(/closureOnly must be a boolean/);
363
+ });
364
+ });
305
365
  });
@@ -396,4 +396,98 @@ describe('sensor manifest contract', () => {
396
396
  expect(soft.invalidEntries.some(entry => /not valid JSON|valid JSON/i.test(entry.reason))).toBe(false);
397
397
  });
398
398
  });
399
+ describe('inputs and sensor-level closureOnly (RF-1.1, RF-1.3, RF-1.5, RF-1.7, RF-1.8)', () => {
400
+ it('accepts inputs and closureOnly on pack-bound and project-declared entries', () => {
401
+ const packBound = {
402
+ ...validV2Manifest().sensors.lint,
403
+ inputs: ['**/*.ts', 'src/**/?est.ts'],
404
+ closureOnly: true,
405
+ };
406
+ const project = projectSensor({
407
+ inputs: ['lib/**/*.js'],
408
+ closureOnly: false,
409
+ });
410
+ const parsed = (0, manifest_1.parseSensorManifest)({
411
+ schemaVersion: 3,
412
+ mode: 'project-sensors',
413
+ pack: 'js-ts',
414
+ source: { registry: 'baseline' },
415
+ sensors: { lint: packBound, custom: project },
416
+ }, 'sensors.json');
417
+ expect(parsed).toMatchObject({
418
+ kind: 'v3',
419
+ pack: {
420
+ sensors: {
421
+ lint: { inputs: ['**/*.ts', 'src/**/?est.ts'], closureOnly: true },
422
+ custom: { inputs: ['lib/**/*.js'], closureOnly: false },
423
+ },
424
+ },
425
+ });
426
+ });
427
+ it('accepts inputs and closureOnly on v2 pack-bound sensors', () => {
428
+ const manifest = validV2Manifest();
429
+ manifest.sensors.lint.inputs = ['**/*.[jt]s'];
430
+ manifest.sensors.lint.closureOnly = true;
431
+ expect((0, manifest_1.parseSensorManifest)(manifest, 'sensors.json')).toMatchObject({
432
+ kind: 'v2',
433
+ pack: { sensors: { lint: { inputs: ['**/*.[jt]s'], closureOnly: true } } },
434
+ });
435
+ });
436
+ it('treats omitted closureOnly as false and does not invent inputs (RF-1.8)', () => {
437
+ const parsed = (0, manifest_1.parseSensorManifest)(v3WithPackAndProject(), 'sensors.json');
438
+ expect(parsed).toMatchObject({ kind: 'v3' });
439
+ if (parsed.kind !== 'v3' || parsed.pack.mode !== 'project-sensors')
440
+ throw new Error('expected v3 project-sensors');
441
+ const lint = parsed.pack.sensors.lint;
442
+ const custom = parsed.pack.sensors.custom;
443
+ expect(lint.closureOnly ?? false).toBe(false);
444
+ expect(custom.closureOnly ?? false).toBe(false);
445
+ expect('inputs' in lint).toBe(false);
446
+ expect('inputs' in custom).toBe(false);
447
+ });
448
+ it.each([
449
+ [[''], /sensors\.custom\.inputs\[0\] must be a nonempty single-line/],
450
+ [['/abs/path'], /sensors\.custom\.inputs\[0\].*absolute paths are invalid/],
451
+ [['C:/windows'], /sensors\.custom\.inputs\[0\].*absolute paths are invalid/],
452
+ [['has\u0000nul'], /sensors\.custom\.inputs\[0\].*NUL or control/],
453
+ [['../escape'], /sensors\.custom\.inputs\[0\].*contained relative glob/],
454
+ [['foo/../bar'], /sensors\.custom\.inputs\[0\].*contained relative glob/],
455
+ [['a{b,c}'], /sensors\.custom\.inputs\[0\].*brace expansion/],
456
+ [['!negated'], /sensors\.custom\.inputs\[0\].*negation/],
457
+ [['path/with!bang'], /sensors\.custom\.inputs\[0\].*negation/],
458
+ [[], /sensors\.custom\.inputs must be a nonempty array/],
459
+ [['@(a|b).ts'], /sensors\.custom\.inputs\[0\].*(?:extglob|parentheses)/],
460
+ [['+(a).ts'], /sensors\.custom\.inputs\[0\].*(?:extglob|parentheses)/],
461
+ [['*(a).ts'], /sensors\.custom\.inputs\[0\].*(?:extglob|parentheses)/],
462
+ [['?(a).ts'], /sensors\.custom\.inputs\[0\].*(?:extglob|parentheses)/],
463
+ [['(a|b).ts'], /sensors\.custom\.inputs\[0\].*(?:extglob|parentheses)/],
464
+ ])('rejects invalid project inputs %j', (inputs, reason) => {
465
+ expect(() => (0, manifest_1.parseSensorManifest)(v3WithPackAndProject({ inputs }), 'sensors.json'))
466
+ .toThrow(reason);
467
+ });
468
+ it('soft-isolates a project entry with bad inputs while keeping siblings', () => {
469
+ const soft = (0, manifest_1.parseSensorManifestWithIssues)(v3WithPackAndProject({ inputs: ['**/{a,b}.ts'] }), 'sensors.json');
470
+ expect(soft.invalidEntries).toEqual([
471
+ expect.objectContaining({
472
+ name: 'custom',
473
+ reason: expect.stringMatching(/sensors\.custom\.inputs\[0\].*brace expansion/),
474
+ }),
475
+ ]);
476
+ expect(soft.invalidEntries[0]?.reason).not.toMatch(/Invalid sensor pack/);
477
+ expect(soft).toMatchObject({
478
+ kind: 'v3',
479
+ pack: { sensors: { lint: { variantId: 'eslint-9' } } },
480
+ });
481
+ expect('custom' in soft.pack.sensors).toBe(false);
482
+ });
483
+ it('rejects closureOnly on a non-boolean sensor field', () => {
484
+ const soft = (0, manifest_1.parseSensorManifestWithIssues)(v3WithPackAndProject({ closureOnly: 'yes' }), 'sensors.json');
485
+ expect(soft.invalidEntries).toEqual([
486
+ expect.objectContaining({
487
+ name: 'custom',
488
+ reason: expect.stringMatching(/sensors\.custom\.closureOnly must be a boolean/),
489
+ }),
490
+ ]);
491
+ });
492
+ });
399
493
  });
@@ -0,0 +1,59 @@
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 awm_gitignore_1 = require("../../../src/core/awm-gitignore");
10
+ jest.mock('../../../src/commands/sensors/bootstrap', () => ({
11
+ planSensorBootstrap: jest.fn(),
12
+ applySensorBootstrap: jest.fn(),
13
+ }));
14
+ jest.mock('../../../src/commands/sensors/detection', () => ({
15
+ detectStack: jest.fn(() => ({ pack: 'js-ts', evidence: [] })),
16
+ }));
17
+ const { planSensorBootstrap, applySensorBootstrap } = require('../../../src/commands/sensors/bootstrap');
18
+ const { initSensors } = require('../../../src/commands/sensors/init');
19
+ describe('initSensors managed awm gitignore (GI-1)', () => {
20
+ let tmp;
21
+ beforeEach(() => {
22
+ tmp = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-sensors-gi-'));
23
+ jest.clearAllMocks();
24
+ });
25
+ afterEach(() => {
26
+ fs_1.default.rmSync(tmp, { recursive: true, force: true });
27
+ });
28
+ it('upserts the managed block after a successful create', async () => {
29
+ planSensorBootstrap.mockResolvedValue({
30
+ kind: 'create',
31
+ projectRoot: tmp,
32
+ manifestPath: path_1.default.join(tmp, '.awm', 'sensors.json'),
33
+ dryRun: false,
34
+ changes: [],
35
+ manifest: { schemaVersion: 3, mode: 'project-sensors', pack: 'js-ts', source: { registry: 'baseline' }, sensors: {} },
36
+ });
37
+ applySensorBootstrap.mockImplementation(() => {
38
+ fs_1.default.mkdirSync(path_1.default.join(tmp, '.awm'), { recursive: true });
39
+ fs_1.default.writeFileSync(path_1.default.join(tmp, '.awm', 'sensors.json'), '{}');
40
+ return 'created';
41
+ });
42
+ await initSensors({ cwd: tmp });
43
+ expect(applySensorBootstrap).toHaveBeenCalled();
44
+ const gi = fs_1.default.readFileSync(path_1.default.join(tmp, '.gitignore'), 'utf8');
45
+ expect(gi).toContain(awm_gitignore_1.AWM_GITIGNORE_BEGIN);
46
+ expect(gi).toContain('!.awm/profile.json');
47
+ });
48
+ it('repairs the managed block on noop when .awm/ already exists', async () => {
49
+ fs_1.default.mkdirSync(path_1.default.join(tmp, '.awm'), { recursive: true });
50
+ fs_1.default.writeFileSync(path_1.default.join(tmp, '.awm', 'sensors.json'), '{}');
51
+ fs_1.default.writeFileSync(path_1.default.join(tmp, '.gitignore'), '.awm/\n');
52
+ planSensorBootstrap.mockResolvedValue({ kind: 'noop' });
53
+ await initSensors({ cwd: tmp });
54
+ expect(applySensorBootstrap).not.toHaveBeenCalled();
55
+ const gi = fs_1.default.readFileSync(path_1.default.join(tmp, '.gitignore'), 'utf8');
56
+ expect(gi).toContain(awm_gitignore_1.AWM_GITIGNORE_BEGIN);
57
+ expect(gi.split('\n').map((l) => l.trim()).filter((l) => l === '.awm/')).toEqual([]);
58
+ });
59
+ });