agentic-workflow-manager 9.18.0 → 9.20.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 (69) hide show
  1. package/dist/src/commands/change/index.js +492 -0
  2. package/dist/src/commands/changes/index.js +84 -0
  3. package/dist/src/commands/plan/index.js +1 -1
  4. package/dist/src/commands/sensors/index.js +9 -0
  5. package/dist/src/commands/sensors/read-only-observation.js +305 -0
  6. package/dist/src/commands/sensors/run.js +24 -60
  7. package/dist/src/core/admission/index.js +11 -1
  8. package/dist/src/core/changes/command.js +38 -0
  9. package/dist/src/core/changes/derive.js +120 -0
  10. package/dist/src/core/changes/fs-safe.js +47 -0
  11. package/dist/src/core/changes/git-env.js +68 -0
  12. package/dist/src/core/changes/git.js +216 -0
  13. package/dist/src/core/changes/list-parse.js +67 -0
  14. package/dist/src/core/changes/list.js +408 -0
  15. package/dist/src/core/changes/parse.js +72 -0
  16. package/dist/src/core/changes/range.js +53 -0
  17. package/dist/src/core/changes/receipt.js +274 -0
  18. package/dist/src/core/changes/record-io.js +149 -0
  19. package/dist/src/core/changes/render-list.js +109 -0
  20. package/dist/src/core/changes/render.js +178 -0
  21. package/dist/src/core/changes/text-safety.js +116 -0
  22. package/dist/src/core/changes/trailers.js +76 -0
  23. package/dist/src/core/changes/types.js +26 -0
  24. package/dist/src/core/changes/validate-shape.js +219 -0
  25. package/dist/src/core/changes/worktree.js +53 -0
  26. package/dist/src/core/currentness/check.js +5 -2
  27. package/dist/src/index.js +4 -0
  28. package/dist/src/release/orchestrator.js +3 -1
  29. package/dist/tests/commands/change/new.test.js +1126 -0
  30. package/dist/tests/commands/change/validate.test.js +1145 -0
  31. package/dist/tests/commands/changes/list.test.js +423 -0
  32. package/dist/tests/commands/sensors/index.test.js +1 -1
  33. package/dist/tests/commands/sensors/read-only-observation.test.js +537 -0
  34. package/dist/tests/commands/sensors/run-read-only.test.js +151 -0
  35. package/dist/tests/core/admission/index.test.js +60 -0
  36. package/dist/tests/core/changes/derive.test.js +455 -0
  37. package/dist/tests/core/changes/fs-safe.test.js +82 -0
  38. package/dist/tests/core/changes/git-env.test.js +143 -0
  39. package/dist/tests/core/changes/git.test.js +442 -0
  40. package/dist/tests/core/changes/helpers.js +269 -0
  41. package/dist/tests/core/changes/list-fixtures.js +122 -0
  42. package/dist/tests/core/changes/list-long-history.test.js +52 -0
  43. package/dist/tests/core/changes/list-paging.test.js +511 -0
  44. package/dist/tests/core/changes/list-parse.test.js +120 -0
  45. package/dist/tests/core/changes/list-tree.test.js +44 -0
  46. package/dist/tests/core/changes/list.test.js +821 -0
  47. package/dist/tests/core/changes/parse.test.js +142 -0
  48. package/dist/tests/core/changes/predicates.test.js +83 -0
  49. package/dist/tests/core/changes/range.test.js +92 -0
  50. package/dist/tests/core/changes/receipt.test.js +947 -0
  51. package/dist/tests/core/changes/record-io.test.js +307 -0
  52. package/dist/tests/core/changes/record.test.js +549 -0
  53. package/dist/tests/core/changes/render-list.test.js +212 -0
  54. package/dist/tests/core/changes/render.test.js +202 -0
  55. package/dist/tests/core/changes/text-safety.test.js +226 -0
  56. package/dist/tests/core/changes/trailers.test.js +181 -0
  57. package/dist/tests/core/changes/worktree.test.js +259 -0
  58. package/dist/tests/core/currentness/check-insteadof.test.js +120 -0
  59. package/dist/tests/core/currentness/check.test.js +75 -14
  60. package/dist/tests/release/orchestrator-trailer.test.js +72 -0
  61. package/dist/tests/release/orchestrator.test.js +1 -0
  62. package/dist/tests/structural/change-commands-are-registered.test.js +29 -0
  63. package/dist/tests/structural/change-record-git-and-platform-conventions.test.js +77 -0
  64. package/dist/tests/structural/cloud-admission-docs-contract.test.js +54 -0
  65. package/package.json +1 -1
  66. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  67. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  68. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  69. package/prebuilds/win32-x64/secure_fs.node +0 -0
@@ -0,0 +1,151 @@
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 mockRunCommand = jest.fn();
11
+ jest.mock('../../../src/commands/sensors/exec', () => ({
12
+ runCommand: (...args) => mockRunCommand(...args),
13
+ }));
14
+ const { ok } = require('./exec-fixtures');
15
+ describe('runSensors read-only observation (admission)', () => {
16
+ const ENV_KEYS = ['HOME', 'AWM_HOME', 'GIT_CONFIG_GLOBAL', 'GIT_CONFIG_NOSYSTEM', 'GIT_CEILING_DIRECTORIES'];
17
+ const saved = {};
18
+ let root;
19
+ let project;
20
+ beforeEach(() => {
21
+ jest.resetModules();
22
+ mockRunCommand.mockReset();
23
+ mockRunCommand.mockResolvedValue(ok(''));
24
+ root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-run-readonly-'));
25
+ for (const key of ENV_KEYS)
26
+ saved[key] = process.env[key];
27
+ const home = path_1.default.join(root, 'home');
28
+ fs_1.default.mkdirSync(home, { recursive: true });
29
+ process.env.HOME = home;
30
+ process.env.AWM_HOME = path_1.default.join(home, '.awm');
31
+ const globalConfig = path_1.default.join(home, 'gitconfig');
32
+ fs_1.default.writeFileSync(globalConfig, '[user]\n\temail = test@example.test\n\tname = test\n');
33
+ process.env.GIT_CONFIG_GLOBAL = globalConfig;
34
+ process.env.GIT_CONFIG_NOSYSTEM = '1';
35
+ process.env.GIT_CEILING_DIRECTORIES = root;
36
+ project = path_1.default.join(root, 'project');
37
+ write('.awm/sensors.json', JSON.stringify({ pack: 'js-ts', sensors: { lint: { fast: true, cmd: 'eslint --format json .' } } }));
38
+ });
39
+ afterEach(() => {
40
+ jest.dontMock('../../../src/commands/sensors/read-only-observation');
41
+ for (const key of ENV_KEYS) {
42
+ if (saved[key] === undefined)
43
+ delete process.env[key];
44
+ else
45
+ process.env[key] = saved[key];
46
+ }
47
+ fs_1.default.rmSync(root, { recursive: true, force: true });
48
+ });
49
+ function write(relative, content = 'x') {
50
+ const file = path_1.default.join(project, ...relative.split('/'));
51
+ fs_1.default.mkdirSync(path_1.default.dirname(file), { recursive: true });
52
+ fs_1.default.writeFileSync(file, content);
53
+ }
54
+ const git = (args) => (0, child_process_1.execFileSync)('git', args, { cwd: project, env: { ...process.env }, stdio: 'pipe', encoding: 'utf8' });
55
+ function commitProject(gitignore) {
56
+ write('.gitignore', gitignore);
57
+ git(['init', '-q', '-b', 'main']);
58
+ git(['add', '.']);
59
+ git(['commit', '-qm', 'initial']);
60
+ }
61
+ const load = () => require('../../../src/commands/sensors/run');
62
+ it('skips ignored default directories, including one a sensor creates, and reports them', async () => {
63
+ commitProject('node_modules/\ncoverage/\n');
64
+ write('node_modules/pkg/index.js', 'installed');
65
+ mockRunCommand.mockImplementation(async () => {
66
+ write('coverage/lcov.info', 'sensor output');
67
+ write('node_modules/pkg/index.js', 'rebuilt');
68
+ return ok('');
69
+ });
70
+ const out = await load().runSensors({ cwd: project, readOnly: true });
71
+ expect(out.reason).not.toBe('read-only-mutation-detected');
72
+ expect(out.reason).not.toBe('read-only-observation-unavailable');
73
+ expect(out.readOnlyObservation).toEqual({ defaults: true, excluded: ['coverage', 'node_modules'], rejected: [] });
74
+ });
75
+ it('still detects a mutation inside a matching directory that is not ignored', async () => {
76
+ commitProject('');
77
+ write('node_modules/pkg/index.js', 'installed');
78
+ mockRunCommand.mockImplementation(async () => {
79
+ write('node_modules/pkg/index.js', 'rebuilt');
80
+ return ok('');
81
+ });
82
+ const out = await load().runSensors({ cwd: project, readOnly: true });
83
+ expect(out).toMatchObject({ overall: 'not_certified', reason: 'read-only-mutation-detected' });
84
+ expect(out.readOnlyObservation).toEqual({ defaults: true, excluded: [], rejected: [{ path: 'node_modules', reason: 'not-ignored' }] });
85
+ });
86
+ it('observes everything and says so when the project is not a git worktree', async () => {
87
+ write('node_modules/pkg/index.js', 'installed');
88
+ const out = await load().runSensors({ cwd: project, readOnly: true });
89
+ expect(out.readOnlyObservation).toEqual({ defaults: true, excluded: [], rejected: [{ path: 'node_modules', reason: 'git-unavailable' }] });
90
+ });
91
+ it('blocks a read-only run on an invalid admission file before running sensors', async () => {
92
+ write('.awm/admission.json', JSON.stringify({ schemaVersion: 2 }));
93
+ const out = await load().runSensors({ cwd: project, readOnly: true });
94
+ expect(out).toMatchObject({
95
+ overall: 'not_certified', mode: 'invalid', reason: 'admission-config-invalid',
96
+ remedy: 'repair .awm/admission.json: schemaVersion must be 1',
97
+ });
98
+ expect(mockRunCommand).not.toHaveBeenCalled();
99
+ });
100
+ it('never reads the admission file outside read-only runs', async () => {
101
+ write('.awm/admission.json', JSON.stringify({ schemaVersion: 2 }));
102
+ const out = await load().runSensors({ cwd: project });
103
+ expect(out.reason).not.toBe('admission-config-invalid');
104
+ expect(out.readOnlyObservation).toBeUndefined();
105
+ expect(mockRunCommand).toHaveBeenCalled();
106
+ });
107
+ it('returns an actionable remedy with evidence when the snapshot exceeds a limit', async () => {
108
+ jest.doMock('../../../src/commands/sensors/read-only-observation', () => {
109
+ const actual = jest.requireActual('../../../src/commands/sensors/read-only-observation');
110
+ return { ...actual, filesystemSnapshot: () => ({ ok: false, failure: 'limit-bytes' }) };
111
+ });
112
+ const out = await load().runSensors({ cwd: project, readOnly: true });
113
+ expect(out).toMatchObject({ overall: 'not_certified', reason: 'read-only-observation-unavailable' });
114
+ expect(out.remedy).toMatch(/1 GiB/);
115
+ expect(out.remedy).toContain('.awm/admission.json');
116
+ expect(out.readOnlyObservation).toEqual({ defaults: true, excluded: [], rejected: [] });
117
+ expect(mockRunCommand).not.toHaveBeenCalled();
118
+ });
119
+ describe('the post-run snapshot', () => {
120
+ const DIGEST = 'a'.repeat(64);
121
+ const stubSnapshot = (second) => {
122
+ const calls = jest.fn()
123
+ .mockReturnValueOnce({ ok: true, digest: DIGEST })
124
+ .mockReturnValueOnce(second);
125
+ jest.doMock('../../../src/commands/sensors/read-only-observation', () => {
126
+ const actual = jest.requireActual('../../../src/commands/sensors/read-only-observation');
127
+ return { ...actual, filesystemSnapshot: (...args) => calls(...args) };
128
+ });
129
+ return calls;
130
+ };
131
+ it('never certifies a run whose worktree cannot be observed after the sensors ran', async () => {
132
+ const snapshot = stubSnapshot({ ok: false, failure: 'unreadable' });
133
+ const out = await load().runSensors({ cwd: project, readOnly: true });
134
+ expect(out).toMatchObject({
135
+ overall: 'not_certified', reason: 'read-only-mutation-detected',
136
+ remedy: 'sensor execution changed the worktree; repair it before using this evidence for admission',
137
+ });
138
+ expect(out.readOnlyObservation).toEqual({ defaults: true, excluded: expect.any(Array), rejected: expect.any(Array) });
139
+ expect(snapshot).toHaveBeenCalledTimes(2);
140
+ expect(mockRunCommand).toHaveBeenCalledTimes(1);
141
+ });
142
+ it('keeps the verdict when the post-run snapshot matches the first one', async () => {
143
+ const snapshot = stubSnapshot({ ok: true, digest: DIGEST });
144
+ const out = await load().runSensors({ cwd: project, readOnly: true });
145
+ expect(out.reason).not.toBe('read-only-mutation-detected');
146
+ expect(out.readOnlyObservation).toEqual({ defaults: true, excluded: expect.any(Array), rejected: expect.any(Array) });
147
+ expect(snapshot).toHaveBeenCalledTimes(2);
148
+ expect(mockRunCommand).toHaveBeenCalledTimes(1);
149
+ });
150
+ });
151
+ });
@@ -258,6 +258,66 @@ describe('admitPlan', () => {
258
258
  expect(() => (0, admission_1.sanitizeAdmissionReport)({ ...report, sensorEvidence: 'opt-out' })).toThrow('invalid report');
259
259
  });
260
260
  });
261
+ describe('the sensor-blocked diagnostic', () => {
262
+ const base = () => ({ plan: valid, provider: 'codex', cwd: process.cwd(), enabledAgents: ['codex'], verifySensors: true });
263
+ it('carries the sensor reason and remedy when the verdict is not-certified', async () => {
264
+ const report = await (0, admission_1.admitPlan)({ ...base(), sensors: {
265
+ overall: 'not_certified', sensors: [], mode: 'invalid', reason: 'read-only-observation-unavailable',
266
+ remedy: 'declare heavy directories in .awm/admission.json.', projectRoot: '/r', manifestPath: '/r/.awm/sensors.json',
267
+ } });
268
+ expect(report.diagnostics[0]).toEqual({
269
+ code: 'ADMISSION_SENSORS_BLOCKED',
270
+ message: 'Sensor verdict is not-certified (read-only-observation-unavailable): declare heavy directories in .awm/admission.json.',
271
+ });
272
+ expect(report.diagnostics[0].message.match(/ADMISSION_[A-Z_]+/g)).toBeNull();
273
+ });
274
+ it('keeps the reason alone when the run has no remedy', async () => {
275
+ const report = await (0, admission_1.admitPlan)({ ...base(), sensors: {
276
+ overall: 'not_certified', sensors: [], mode: 'native-gate', reason: 'native-gate-declared', projectRoot: '/r', manifestPath: '/r/.awm/sensors.json',
277
+ } });
278
+ expect(report.diagnostics[0].message).toBe('Sensor verdict is not-certified (native-gate-declared).');
279
+ });
280
+ it.each([
281
+ ['remedy', { reason: 'admission-config-invalid', remedy: 'repair .awm/admission.json: ADMISSION_FOO is not a known field' }, 'ADMISSION-FOO'],
282
+ ['reason', { reason: 'ADMISSION_X-unavailable', remedy: 'fix it' }, 'ADMISSION-X'],
283
+ ])('leaves the diagnostic code as the only ADMISSION_ token when the %s echoes one', async (_name, fields, neutralized) => {
284
+ const report = await (0, admission_1.admitPlan)({ ...base(), sensors: {
285
+ overall: 'not_certified', sensors: [], mode: 'invalid', ...fields, projectRoot: '/r', manifestPath: '/r/.awm/sensors.json',
286
+ } });
287
+ expect(report.diagnostics[0].code).toBe('ADMISSION_SENSORS_BLOCKED');
288
+ expect(report.diagnostics[0].message).toContain(neutralized);
289
+ expect(`${report.diagnostics[0].code} ${report.diagnostics[0].message}`.match(/ADMISSION_[A-Z_]+/g)).toEqual(['ADMISSION_SENSORS_BLOCKED']);
290
+ });
291
+ it.each([
292
+ ['trims a padded reason and remedy', ' read-only-observation-unavailable ', ' declare it in .awm/admission.json. ', 'Sensor verdict is not-certified (read-only-observation-unavailable): declare it in .awm/admission.json.'],
293
+ ['drops a whitespace-only reason', ' ', 'declare it in .awm/admission.json.', 'Sensor verdict is not-certified.'],
294
+ ['drops a whitespace-only remedy', 'some-reason', ' ', 'Sensor verdict is not-certified (some-reason).'],
295
+ ])('%s', async (_name, reason, remedy, message) => {
296
+ const report = await (0, admission_1.admitPlan)({ ...base(), sensors: {
297
+ overall: 'not_certified', sensors: [], mode: 'invalid', reason, remedy, projectRoot: '/r', manifestPath: '/r/.awm/sensors.json',
298
+ } });
299
+ expect(report.diagnostics[0]).toEqual({ code: 'ADMISSION_SENSORS_BLOCKED', message });
300
+ });
301
+ it('neutralizes every ADMISSION_ token when several are echoed in the reason and the remedy', async () => {
302
+ const report = await (0, admission_1.admitPlan)({ ...base(), sensors: {
303
+ overall: 'not_certified', sensors: [], mode: 'invalid',
304
+ reason: 'admission-config-invalid ADMISSION_BAZ ADMISSION_QUX',
305
+ remedy: 'repair .awm/admission.json: ADMISSION_FOO is not allowed, ADMISSION_BAR neither',
306
+ projectRoot: '/r', manifestPath: '/r/.awm/sensors.json',
307
+ } });
308
+ const { code, message } = report.diagnostics[0];
309
+ expect(code).toBe('ADMISSION_SENSORS_BLOCKED');
310
+ for (const token of ['ADMISSION-FOO', 'ADMISSION-BAR', 'ADMISSION-BAZ', 'ADMISSION-QUX'])
311
+ expect(message).toContain(token);
312
+ expect(`${code} ${message}`.match(/ADMISSION_[A-Z_]+/g)).toEqual(['ADMISSION_SENSORS_BLOCKED']);
313
+ });
314
+ it('keeps the bare verdict for a failing run', async () => {
315
+ const report = await (0, admission_1.admitPlan)({ ...base(), sensors: {
316
+ overall: 'fail', sensors: [], mode: 'project-sensors', reason: 'configured-v3', projectRoot: '/r', manifestPath: '/r/.awm/sensors.json',
317
+ } });
318
+ expect(report.diagnostics[0]).toEqual({ code: 'ADMISSION_SENSORS_BLOCKED', message: 'Sensor verdict is fail.' });
319
+ });
320
+ });
261
321
  it('admits unattended work only with an exact schema-2 plan binding', async () => {
262
322
  const unattended = { ...valid, manifest: { ...valid.manifest, executionMode: 'desatendido' } };
263
323
  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' } };