agentic-workflow-manager 9.3.0 → 9.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,437 @@
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 child_process_1 = require("child_process");
8
+ const os_1 = __importDefault(require("os"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const json_1 = require("../../../src/core/plan/json");
11
+ const validate_1 = require("../../../src/core/plan/validate");
12
+ jest.mock('child_process', () => ({ exec: jest.fn(), execFileSync: jest.fn(), execSync: jest.fn(), spawn: jest.fn(), spawnSync: jest.fn() }));
13
+ const START = '<!-- AWM:COMPACT-SLICES:START v1 -->';
14
+ const END = '<!-- AWM:COMPACT-SLICES:END v1 -->';
15
+ function expectApprovedPlanValid(report) {
16
+ if (report.state !== 'valid') {
17
+ const diagnostics = 'diagnostics' in report
18
+ ? report.diagnostics.map(({ code, message }) => `${code}: ${message}`).join('; ')
19
+ : 'none';
20
+ throw new Error(`approved compact plan validation failed: state=${report.state}; diagnostics=${diagnostics}`);
21
+ }
22
+ expect(report.schema).toBe('compact-slices/v1');
23
+ }
24
+ function fixture(root, mutate) {
25
+ fs_1.default.mkdirSync(path_1.default.join(root, 'docs'), { recursive: true });
26
+ fs_1.default.writeFileSync(path_1.default.join(root, 'docs', 'source.md'), '## Source\nKnown fact\n');
27
+ const manifest = {
28
+ schema: 'compact-slices/v1', planId: 'valid-plan', requirements: ['R4-VAL-2'],
29
+ sources: [{ id: 'SRC-ONE', path: 'docs/source.md', locator: '## Source', fact: 'Known fact' }],
30
+ commands: [{ id: 'CMD-ONE', program: 'npm', args: ['test'], covers: ['R4-VAL-2'] }],
31
+ slices: [{ id: 'S1', title: 'Validate one thing', requirements: ['R4-VAL-2'], dependsOn: [], sectionAnchor: 'slice-s1', sources: ['SRC-ONE'], redCommands: ['CMD-ONE'], greenCommands: ['CMD-ONE'], reviewEvidence: ['specification', 'code-quality'], risk: 'bounded', fallback: ['Use a reviewed fallback'] }],
32
+ closureCommands: ['CMD-ONE'],
33
+ };
34
+ mutate?.(manifest);
35
+ const markdown = `${START}\n${JSON.stringify(manifest, null, 2)}\n${END}\n\n<a id="slice-s1"></a>\n### Slice S1: Validate one thing\n\n#### Surfaces\n\nOne surface.\n\n#### Implementation\n\nOne implementation.\n\n#### Edge cases\n\nOne edge case.\n\n#### Evidence\n\nOne evidence item.\n\n#### Fallback\n\nOne fallback.\n`;
36
+ const plan = path_1.default.join(root, 'plan.md');
37
+ fs_1.default.writeFileSync(plan, markdown);
38
+ return plan;
39
+ }
40
+ describe('validatePlanFile', () => {
41
+ let root;
42
+ beforeEach(() => { root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-plan-')); });
43
+ afterEach(() => { fs_1.default.rmSync(root, { recursive: true, force: true }); });
44
+ test('accepts a valid compact plan without modifying it', () => {
45
+ const plan = fixture(root);
46
+ const before = fs_1.default.readFileSync(plan, 'utf8');
47
+ expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'valid', schema: 'compact-slices/v1' });
48
+ expect(fs_1.default.readFileSync(plan, 'utf8')).toBe(before);
49
+ });
50
+ test('accepts a valid compact plan converted to CRLF', () => {
51
+ const plan = fixture(root);
52
+ fs_1.default.writeFileSync(plan, fs_1.default.readFileSync(plan, 'utf8').replace(/\n/g, '\r\n'));
53
+ expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'valid', schema: 'compact-slices/v1' });
54
+ });
55
+ test('accepts a contained plan when path.relative returns Windows separators', () => {
56
+ const plan = fixture(root);
57
+ const nestedPlan = path_1.default.join(root, 'docs', 'plan.md');
58
+ fs_1.default.renameSync(plan, nestedPlan);
59
+ const nativeRelative = path_1.default.relative;
60
+ const relative = jest.spyOn(path_1.default, 'relative');
61
+ relative.mockImplementation((from, to) => {
62
+ const result = nativeRelative(from, to);
63
+ return from === root && to === nestedPlan ? result.replace(/\//g, '\\') : result;
64
+ });
65
+ try {
66
+ expect((0, validate_1.validatePlanFile)(nestedPlan, root)).toMatchObject({ state: 'valid', schema: 'compact-slices/v1' });
67
+ }
68
+ finally {
69
+ relative.mockRestore();
70
+ }
71
+ });
72
+ test('accepts the approved plan with cross-cutting command coverage', () => {
73
+ const repositoryRoot = path_1.default.resolve(__dirname, '../../../..');
74
+ expectApprovedPlanValid((0, validate_1.validatePlanFile)('docs/plans/2026-08-26-r4a-compact-plan-cli-plan.md', repositoryRoot));
75
+ });
76
+ test('performs no execution, network request, model work, grouping, or rewrite', () => {
77
+ const plan = fixture(root);
78
+ const before = fs_1.default.readFileSync(plan, 'utf8');
79
+ const network = jest.spyOn(global, 'fetch');
80
+ const rewrite = jest.spyOn(fs_1.default, 'writeFileSync');
81
+ try {
82
+ const report = (0, validate_1.validatePlanFile)(plan, root);
83
+ expect(report).toMatchObject({ state: 'valid', manifest: { slices: [expect.objectContaining({ id: 'S1', requirements: ['R4-VAL-2'] })] } });
84
+ expect(child_process_1.execFileSync).not.toHaveBeenCalled();
85
+ expect(child_process_1.exec).not.toHaveBeenCalled();
86
+ expect(child_process_1.execSync).not.toHaveBeenCalled();
87
+ expect(child_process_1.spawn).not.toHaveBeenCalled();
88
+ expect(child_process_1.spawnSync).not.toHaveBeenCalled();
89
+ expect(network).not.toHaveBeenCalled();
90
+ expect(rewrite).not.toHaveBeenCalled();
91
+ expect(fs_1.default.readFileSync(plan, 'utf8')).toBe(before);
92
+ }
93
+ finally {
94
+ network.mockRestore();
95
+ rewrite.mockRestore();
96
+ }
97
+ });
98
+ test('rejects invalid UTF-8 bytes before interpreting plan text', () => {
99
+ const plan = path_1.default.join(root, 'invalid-utf8.md');
100
+ fs_1.default.writeFileSync(plan, Buffer.concat([Buffer.from(`${START}\n`), Buffer.from([0x80]), Buffer.from(`\n${END}\n`)]));
101
+ expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_ENCODING' })] });
102
+ });
103
+ test('rejects a plan reached through a symlinked parent directory', () => {
104
+ const outside = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-plan-outside-'));
105
+ try {
106
+ const plan = fixture(outside);
107
+ fs_1.default.symlinkSync(outside, path_1.default.join(root, 'linked-parent'));
108
+ expect((0, validate_1.validatePlanFile)(path_1.default.join('linked-parent', path_1.default.basename(plan)), root)).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_PATH_UNSAFE' })] });
109
+ }
110
+ finally {
111
+ fs_1.default.rmSync(outside, { recursive: true, force: true });
112
+ }
113
+ });
114
+ test('rejects partial markers and identifies a safely readable future schema', () => {
115
+ const partial = path_1.default.join(root, 'partial.md');
116
+ fs_1.default.writeFileSync(partial, `${START}\n# incomplete`);
117
+ expect((0, validate_1.validatePlanFile)(partial, root)).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_MARKERS' })] });
118
+ const future = path_1.default.join(root, 'future.md');
119
+ fs_1.default.writeFileSync(future, `${START}\n{"schema":"compact-slices/v2"}\n${END}`);
120
+ expect((0, validate_1.validatePlanFile)(future, root)).toMatchObject({ state: 'unsupported', schema: 'compact-slices/v2' });
121
+ });
122
+ test('keeps a future schema behind partial markers invalid rather than unsupported', () => {
123
+ const plan = path_1.default.join(root, 'partial-future.md');
124
+ fs_1.default.writeFileSync(plan, `${START}\n{"schema":"compact-slices/v2"}`);
125
+ expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'unsupported', schema: 'compact-slices/v2' });
126
+ });
127
+ test('applies scalar limits before classifying a future schema as unsupported', () => {
128
+ const plan = path_1.default.join(root, 'future-too-large.md');
129
+ fs_1.default.writeFileSync(plan, `${START}\n{"schema":"compact-slices/${'v'.repeat(4097)}"}\n${END}`);
130
+ expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_LIMIT' })] });
131
+ });
132
+ test('rejects a duplicate JSON key with a stable diagnostic', () => {
133
+ const plan = path_1.default.join(root, 'plan.md');
134
+ fs_1.default.writeFileSync(plan, `${START}\n{"schema":"compact-slices/v1","schema":"compact-slices/v1"}\n${END}`);
135
+ expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_JSON' })] });
136
+ });
137
+ test('rejects JSON keys made equivalent by slash escaping', () => {
138
+ expect(() => (0, json_1.parseJsonNoDuplicate)('{"a/b":1,"a\\/b":2}')).toThrow('duplicate key');
139
+ });
140
+ test.each([
141
+ ['requirement', (m) => { m.requirements = [['R4-VAL-2']]; }, 'PLAN_SHAPE'],
142
+ ['source', (m) => { m.sources[0].id = ['SRC-ONE']; }, 'PLAN_SOURCE_SHAPE'],
143
+ ['command', (m) => { m.commands[0].id = ['CMD-ONE']; }, 'PLAN_COMMAND_SHAPE'],
144
+ ['slice', (m) => { m.slices[0].id = ['S1']; }, 'PLAN_SLICE_SHAPE'],
145
+ ])('rejects a non-string %s ID before applying ID syntax validation', (_name, mutate, code) => {
146
+ const report = (0, validate_1.validatePlanFile)(fixture(root, mutate), root);
147
+ expect(report).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code })] });
148
+ });
149
+ test('rejects slices with IDs that are semantically duplicate after coercion', () => {
150
+ const plan = fixture(root, (m) => {
151
+ m.requirements = ['R4-VAL-2', 'R4-VAL-3'];
152
+ m.commands.push({ id: 'CMD-TWO', program: 'npm', args: ['run', 'lint'], covers: ['R4-VAL-3'] });
153
+ m.slices.push({ id: ['S1'], title: 'Validate one thing', requirements: ['R4-VAL-3'], dependsOn: [], sectionAnchor: 'slice-s1', sources: ['SRC-ONE'], redCommands: ['CMD-TWO'], greenCommands: ['CMD-TWO'], reviewEvidence: ['specification', 'code-quality'], risk: 'bounded', fallback: ['Use a reviewed fallback'] });
154
+ m.closureCommands = ['CMD-ONE', 'CMD-TWO'];
155
+ });
156
+ expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_SLICE_SHAPE' })] });
157
+ });
158
+ test.each([
159
+ ['unknown field', (m) => { m.extra = true; }, 'PLAN_UNKNOWN_FIELD'],
160
+ ['missing field', (m) => { delete m.planId; }, 'PLAN_MISSING_FIELD'],
161
+ ['limit', (m) => { m.requirements = Array.from({ length: 257 }, (_, i) => `R${i}`); }, 'PLAN_LIMIT'],
162
+ ['duplicate owner', (m) => { m.slices[0].requirements = ['R4-VAL-2', 'R4-VAL-2']; }, 'PLAN_REQUIREMENT_OWNER'],
163
+ ['orphan requirement', (m) => { m.requirements = ['R4-VAL-2', 'R4-VAL-5']; }, 'PLAN_REQUIREMENT_OWNER'],
164
+ ['cycle', (m) => { m.slices[0].dependsOn = ['S1']; }, 'PLAN_DEPENDENCY'],
165
+ ['review roles', (m) => { m.slices[0].reviewEvidence = ['specification', 'specification']; }, 'PLAN_REVIEW_EVIDENCE'],
166
+ ['shell argv', (m) => { m.commands[0].program = 'sh'; m.commands[0].args = ['-c', 'echo x']; }, 'PLAN_COMMAND_UNSAFE'],
167
+ ])('rejects %s with a stable diagnostic', (_name, mutate, code) => {
168
+ const report = (0, validate_1.validatePlanFile)(fixture(root, mutate), root);
169
+ expect(report).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code })] });
170
+ });
171
+ test('rejects source symlinks and missing locators', () => {
172
+ const linked = path_1.default.join(root, 'docs', 'linked.md');
173
+ fs_1.default.mkdirSync(path_1.default.dirname(linked), { recursive: true });
174
+ fs_1.default.symlinkSync(path_1.default.join(root, 'docs', 'source.md'), linked);
175
+ const symlink = (0, validate_1.validatePlanFile)(fixture(root, (m) => { m.sources[0].path = 'docs/linked.md'; }), root);
176
+ expect(symlink).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_SOURCE_UNSAFE' })] });
177
+ const locator = (0, validate_1.validatePlanFile)(fixture(root, (m) => { m.sources[0].locator = 'absent'; }), root);
178
+ expect(locator).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_SOURCE_LOCATOR' })] });
179
+ });
180
+ test('rejects an oversized source before reading its body', () => {
181
+ const plan = fixture(root);
182
+ const source = path_1.default.join(root, 'docs', 'source.md');
183
+ fs_1.default.writeFileSync(source, Buffer.alloc(1024 * 1024 + 1, 0x61));
184
+ const read = jest.spyOn(fs_1.default, 'readFileSync');
185
+ try {
186
+ expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_SOURCE_LIMIT' })] });
187
+ expect(read.mock.calls.some(([file]) => file === source)).toBe(false);
188
+ }
189
+ finally {
190
+ read.mockRestore();
191
+ }
192
+ });
193
+ test.each([
194
+ ['invalid UTF-8 source', (root) => fs_1.default.writeFileSync(path_1.default.join(root, 'docs', 'source.md'), Buffer.from([0x80])), 'PLAN_SOURCE_ENCODING'],
195
+ ['multiline locator', (_root) => undefined, 'PLAN_SOURCE_SHAPE'],
196
+ ])('rejects %s', (_name, prepare, code) => {
197
+ const report = (0, validate_1.validatePlanFile)(fixture(root, (m) => {
198
+ prepare(root);
199
+ if (code === 'PLAN_SOURCE_SHAPE')
200
+ m.sources[0].locator = 'Source\nKnown';
201
+ }), root);
202
+ expect(report).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code })] });
203
+ });
204
+ test.each(['', '.', '..', '/tmp/source.md', 'C:\\source.md', 'docs\\source.md', 'docs/../source.md'])('rejects adversarial source path %j', (sourcePath) => {
205
+ const report = (0, validate_1.validatePlanFile)(fixture(root, (m) => { m.sources[0].path = sourcePath; }), root);
206
+ expect(report).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_SOURCE_SHAPE' })] });
207
+ });
208
+ test.each(['redCommands', 'greenCommands'])('requires a nonempty %s list', (field) => {
209
+ const report = (0, validate_1.validatePlanFile)(fixture(root, (m) => { m.slices[0][field] = []; }), root);
210
+ expect(report).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_SLICE_SHAPE' })] });
211
+ });
212
+ test('rejects a slice requirement absent from the top-level requirements', () => {
213
+ const report = (0, validate_1.validatePlanFile)(fixture(root, (m) => {
214
+ m.requirements = [];
215
+ m.slices[0].requirements = ['R4-VAL-9'];
216
+ m.commands[0].covers = [];
217
+ }), root);
218
+ expect(report).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_SLICE_SHAPE' })] });
219
+ });
220
+ test('requires a non-whitespace fallback condition', () => {
221
+ const report = (0, validate_1.validatePlanFile)(fixture(root, (m) => { m.slices[0].fallback = [' ']; }), root);
222
+ expect(report).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_SLICE_SHAPE' })] });
223
+ });
224
+ test.each(['locator', 'fact'])('requires source %s to contain non-whitespace text', (field) => {
225
+ const report = (0, validate_1.validatePlanFile)(fixture(root, (m) => { m.sources[0][field] = ' '; }), root);
226
+ expect(report).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_SOURCE_SHAPE' })] });
227
+ });
228
+ test('bounds sectionAnchor as a scalar string', () => {
229
+ const report = (0, validate_1.validatePlanFile)(fixture(root, (m) => { m.slices[0].sectionAnchor = 'a'.repeat(4097); }), root);
230
+ expect(report).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_SLICE_SHAPE' })] });
231
+ });
232
+ test('treats a document without any optimized signal as legacy', () => {
233
+ const plan = path_1.default.join(root, 'legacy.md');
234
+ fs_1.default.writeFileSync(plan, '# Legacy plan\n');
235
+ expect((0, validate_1.validatePlanFile)(plan, root)).toEqual({ state: 'legacy' });
236
+ });
237
+ test('does not classify an escaped future schema signal as legacy', () => {
238
+ const plan = path_1.default.join(root, 'escaped-future.md');
239
+ fs_1.default.writeFileSync(plan, '{"schema":"compact-slices\\u002fv2"}');
240
+ expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'unsupported', schema: 'compact-slices/v2' });
241
+ });
242
+ test.each([
243
+ ['escaped schema key', '{"\\u0073chema":"compact-slices/v2"}'],
244
+ ['escaped schema hyphen', '{"schema":"compact\\u002dslices/v2"}'],
245
+ ])('does not classify a future schema with an %s as legacy', (_name, manifest) => {
246
+ const plan = path_1.default.join(root, 'escaped-future-schema.md');
247
+ fs_1.default.writeFileSync(plan, manifest);
248
+ expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'unsupported', schema: 'compact-slices/v2' });
249
+ });
250
+ test('does not classify an escaped future schema embedded in Markdown without markers as legacy', () => {
251
+ const plan = path_1.default.join(root, 'embedded-escaped-future.md');
252
+ fs_1.default.writeFileSync(plan, '# Plan notes\n\n```json\n{"\\u0073chema":"compact\\u002dslices\\u002fv2"}\n```\n');
253
+ expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'unsupported', schema: 'compact-slices/v2' });
254
+ });
255
+ test('rejects an unparseable escaped compact schema embedded in Markdown rather than treating it as legacy', () => {
256
+ const plan = path_1.default.join(root, 'embedded-escaped-malformed.md');
257
+ fs_1.default.writeFileSync(plan, '# Plan notes\n\n{"\\u0073chema":"compact\\u002dslices\\u002fv2"\n');
258
+ expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_MARKERS' })] });
259
+ });
260
+ test('rejects a markerless malformed compact schema with an escaped JSON slash', () => {
261
+ const plan = path_1.default.join(root, 'escaped-slash-malformed.md');
262
+ fs_1.default.writeFileSync(plan, '{"schema":"compact-slices\\/v2"');
263
+ expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_MARKERS' })] });
264
+ });
265
+ test('rejects an escaped current schema without compact markers', () => {
266
+ const plan = path_1.default.join(root, 'escaped-current-schema.md');
267
+ fs_1.default.writeFileSync(plan, '{"\\u0073chema":"compact\\u002dslices/v1"}');
268
+ expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_MARKERS' })] });
269
+ });
270
+ test('rejects duplicate trace references and an oversized plan id', () => {
271
+ const duplicate = (0, validate_1.validatePlanFile)(fixture(root, (m) => { m.slices[0].sources = ['SRC-ONE', 'SRC-ONE']; }), root);
272
+ expect(duplicate).toMatchObject({ state: 'invalid' });
273
+ const planId = (0, validate_1.validatePlanFile)(fixture(root, (m) => { m.planId = 'a'.repeat(4097); }), root);
274
+ expect(planId).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_LIMIT' })] });
275
+ });
276
+ test('requires the slice heading immediately after its anchor', () => {
277
+ const plan = fixture(root);
278
+ fs_1.default.writeFileSync(plan, fs_1.default.readFileSync(plan, 'utf8').replace('<a id="slice-s1"></a>\n', '<a id="slice-s1"></a>\nintervening text\n'));
279
+ expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_MARKDOWN_HEADING' })] });
280
+ });
281
+ test('rejects a blank line between the slice anchor and heading', () => {
282
+ const plan = fixture(root);
283
+ fs_1.default.writeFileSync(plan, fs_1.default.readFileSync(plan, 'utf8').replace('<a id="slice-s1"></a>\n', '<a id="slice-s1"></a>\n\n'));
284
+ expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_MARKDOWN_HEADING' })] });
285
+ });
286
+ test('rejects a slice heading with a suffix beyond the manifest title', () => {
287
+ const plan = fixture(root);
288
+ fs_1.default.writeFileSync(plan, fs_1.default.readFileSync(plan, 'utf8').replace('### Slice S1: Validate one thing\n', '### Slice S1: Validate one thing extra\n'));
289
+ expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_MARKDOWN_HEADING' })] });
290
+ });
291
+ test('rejects a repeated required Markdown subsection', () => {
292
+ const plan = fixture(root);
293
+ fs_1.default.writeFileSync(plan, fs_1.default.readFileSync(plan, 'utf8').replace('#### Fallback', '#### Evidence\n\nDuplicate evidence.\n\n#### Fallback'));
294
+ expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_MARKDOWN_SECTION' })] });
295
+ });
296
+ test('ignores apparent slice delimiters and subsections inside fenced code blocks', () => {
297
+ const plan = fixture(root);
298
+ fs_1.default.writeFileSync(plan, fs_1.default.readFileSync(plan, 'utf8').replace('#### Fallback', '```markdown\n### Slice S99: Example only\n#### Surfaces\n#### Implementation\n#### Edge cases\n#### Evidence\n#### Fallback\n```\n\n#### Fallback'));
299
+ expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'valid' });
300
+ });
301
+ test('rejects an additional slice heading that is not immediately anchored', () => {
302
+ const plan = fixture(root);
303
+ fs_1.default.appendFileSync(plan, '\n### Slice S1: Validate one thing\n');
304
+ expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_MARKDOWN_HEADING' })] });
305
+ });
306
+ test.each(['TODO', 'TBD', 'placeholder', 'draft'])('rejects incomplete Markdown sentinel %s', (sentinel) => {
307
+ const plan = fixture(root);
308
+ fs_1.default.writeFileSync(plan, fs_1.default.readFileSync(plan, 'utf8').replace('One evidence item.', sentinel));
309
+ expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_MARKDOWN_SECTION' })] });
310
+ });
311
+ test('rejects a contained command file without an executable mode', () => {
312
+ const report = (0, validate_1.validatePlanFile)(fixture(root, (m) => {
313
+ fs_1.default.writeFileSync(path_1.default.join(root, 'docs', 'program'), 'echo inert\n', { mode: 0o644 });
314
+ m.commands[0].program = 'docs/program';
315
+ }), root);
316
+ expect(report).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_COMMAND_UNSAFE' })] });
317
+ });
318
+ test.each([
319
+ ['env wrapper', 'env', ['sh', '-c', 'echo safe']],
320
+ ['node interpreter', 'node', ['-e', 'process.exit()']],
321
+ ['busybox launcher', 'busybox', ['sh', '-c', 'echo safe']],
322
+ ])('rejects nested shell launcher %s', (_name, program, args) => {
323
+ const report = (0, validate_1.validatePlanFile)(fixture(root, (m) => { m.commands[0].program = program; m.commands[0].args = args; }), root);
324
+ expect(report).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_COMMAND_UNSAFE' })] });
325
+ });
326
+ test.each(['node-v22', 'nodejs-20', 'python3.12', 'bash-static', 'busybox-musl'])('rejects versioned launcher %s', (program) => {
327
+ const report = (0, validate_1.validatePlanFile)(fixture(root, (m) => { m.commands[0].program = program; }), root);
328
+ expect(report).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_COMMAND_UNSAFE' })] });
329
+ });
330
+ test.each(['cmd.exe', 'powershell.exe', 'pwsh.exe', 'wscript.exe', 'cscript.exe'])('rejects Windows launcher %s', (program) => {
331
+ const report = (0, validate_1.validatePlanFile)(fixture(root, (m) => { m.commands[0].program = program; }), root);
332
+ expect(report).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_COMMAND_UNSAFE' })] });
333
+ });
334
+ test.each(['bin/bash', 'tools/cmd.exe'])('rejects path-qualified interpreter launcher %s', (program) => {
335
+ const report = (0, validate_1.validatePlanFile)(fixture(root, (m) => { m.commands[0].program = program; }), root);
336
+ expect(report).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_COMMAND_UNSAFE' })] });
337
+ });
338
+ test.each(['FiSh.ExE', 'tools/EnV.eXe'])('rejects case-insensitive launcher %s even with a safe relative path', (program) => {
339
+ const report = (0, validate_1.validatePlanFile)(fixture(root, (m) => {
340
+ if (program.includes('/')) {
341
+ fs_1.default.mkdirSync(path_1.default.join(root, 'tools'), { recursive: true });
342
+ fs_1.default.writeFileSync(path_1.default.join(root, 'tools', 'EnV.eXe'), 'inert executable', { mode: 0o755 });
343
+ }
344
+ m.commands[0].program = program;
345
+ }), root);
346
+ expect(report).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_COMMAND_UNSAFE' })] });
347
+ });
348
+ test.each(['py.exe', 'pythonw.exe', 'deno.exe', 'bun.exe', 'ruby3.3', 'perl5.38', 'ash', 'csh', 'tcsh'])('rejects interpreter family %s', (program) => {
349
+ const report = (0, validate_1.validatePlanFile)(fixture(root, (m) => { m.commands[0].program = program; }), root);
350
+ expect(report).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_COMMAND_UNSAFE' })] });
351
+ });
352
+ test('rejects a Windows path-like program as a bare executable', () => {
353
+ const report = (0, validate_1.validatePlanFile)(fixture(root, (m) => { m.commands[0].program = 'C:\\tools\\safe.exe'; }), root);
354
+ expect(report).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_COMMAND_UNSAFE' })] });
355
+ });
356
+ test('rejects a Windows drive-relative program', () => {
357
+ const report = (0, validate_1.validatePlanFile)(fixture(root, (m) => { m.commands[0].program = 'C:cmd.exe'; }), root);
358
+ expect(report).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_COMMAND_UNSAFE' })] });
359
+ });
360
+ test('applies Windows contained executable semantics and still denies symlinks', () => {
361
+ const platform = Object.getOwnPropertyDescriptor(process, 'platform');
362
+ Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' });
363
+ try {
364
+ const executable = (0, validate_1.validatePlanFile)(fixture(root, (m) => {
365
+ fs_1.default.writeFileSync(path_1.default.join(root, 'docs', 'safe.exe'), 'inert executable');
366
+ m.commands[0].program = 'docs/safe.exe';
367
+ }), root);
368
+ expect(executable).toMatchObject({ state: 'valid' });
369
+ const symlink = (0, validate_1.validatePlanFile)(fixture(root, (m) => {
370
+ fs_1.default.writeFileSync(path_1.default.join(root, 'docs', 'safe.exe'), 'inert executable');
371
+ fs_1.default.symlinkSync(path_1.default.join(root, 'docs', 'safe.exe'), path_1.default.join(root, 'docs', 'linked.exe'));
372
+ m.commands[0].program = 'docs/linked.exe';
373
+ }), root);
374
+ expect(symlink).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_COMMAND_UNSAFE' })] });
375
+ }
376
+ finally {
377
+ if (platform)
378
+ Object.defineProperty(process, 'platform', platform);
379
+ }
380
+ });
381
+ test('rejects duplicate command coverage', () => {
382
+ const report = (0, validate_1.validatePlanFile)(fixture(root, (m) => { m.commands[0].covers = ['R4-VAL-2', 'R4-VAL-2']; }), root);
383
+ expect(report).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_COMMAND_SHAPE' })] });
384
+ });
385
+ test('rejects command coverage unrelated to every referencing slice', () => {
386
+ const plan = fixture(root, (m) => {
387
+ m.requirements = ['R4-VAL-2', 'R4-VAL-3'];
388
+ m.commands.push({ id: 'CMD-TWO', program: 'npm', args: ['run', 'lint'], covers: ['R4-VAL-2'] });
389
+ m.slices.push({ id: 'S2', title: 'Validate two things', requirements: ['R4-VAL-3'], dependsOn: [], sectionAnchor: 'slice-s2', sources: ['SRC-ONE'], redCommands: ['CMD-TWO'], greenCommands: ['CMD-TWO'], reviewEvidence: ['specification', 'code-quality'], risk: 'bounded', fallback: ['Use a reviewed fallback'] });
390
+ });
391
+ fs_1.default.appendFileSync(plan, '\n<a id="slice-s2"></a>\n### Slice S2: Validate two things\n\n#### Surfaces\n\nOne surface.\n\n#### Implementation\n\nOne implementation.\n\n#### Edge cases\n\nOne edge case.\n\n#### Evidence\n\nOne evidence item.\n\n#### Fallback\n\nOne fallback.\n');
392
+ expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'invalid' });
393
+ });
394
+ test('rejects requirement coverage executed only by a different slice', () => {
395
+ const plan = fixture(root, (m) => {
396
+ m.requirements = ['R4-VAL-2', 'R4-VAL-3'];
397
+ m.commands[0].covers = ['R4-VAL-2'];
398
+ m.commands.push({ id: 'CMD-TWO', program: 'npm', args: ['run', 'lint'], covers: ['R4-VAL-2', 'R4-VAL-3'] });
399
+ m.commands.push({ id: 'CMD-THREE', program: 'npm', args: ['run', 'build'], covers: [] });
400
+ m.slices[0].redCommands.push('CMD-TWO');
401
+ m.slices[0].greenCommands.push('CMD-TWO');
402
+ m.slices.push({ id: 'S2', title: 'Validate two things', requirements: ['R4-VAL-3'], dependsOn: [], sectionAnchor: 'slice-s2', sources: ['SRC-ONE'], redCommands: ['CMD-THREE'], greenCommands: ['CMD-THREE'], reviewEvidence: ['specification', 'code-quality'], risk: 'bounded', fallback: ['Use a reviewed fallback'] });
403
+ m.closureCommands = ['CMD-ONE', 'CMD-TWO', 'CMD-THREE'];
404
+ });
405
+ fs_1.default.appendFileSync(plan, '\n<a id="slice-s2"></a>\n### Slice S2: Validate two things\n\n#### Surfaces\n\nOne surface.\n\n#### Implementation\n\nOne implementation.\n\n#### Edge cases\n\nOne edge case.\n\n#### Evidence\n\nOne evidence item.\n\n#### Fallback\n\nOne fallback.\n');
406
+ expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_REQUIREMENT_COVER' })] });
407
+ });
408
+ test('rejects a command with neither requirement coverage nor closure membership', () => {
409
+ const report = (0, validate_1.validatePlanFile)(fixture(root, (m) => {
410
+ m.commands[0].covers = [];
411
+ m.closureCommands = [];
412
+ }), root);
413
+ expect(report).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_COMMAND_SHAPE' })] });
414
+ });
415
+ test.each(['requirements', 'sources', 'commands', 'slices', 'closureCommands'])('requires a nonempty v1 manifest %s collection', (field) => {
416
+ const report = (0, validate_1.validatePlanFile)(fixture(root, (m) => { m[field] = []; }), root);
417
+ expect(report).toMatchObject({ state: 'invalid' });
418
+ });
419
+ test('recognizes a parseable future schema when only the END marker is broken', () => {
420
+ const plan = path_1.default.join(root, 'future-missing-end.md');
421
+ fs_1.default.writeFileSync(plan, `${START}\n{"schema":"compact-slices/v2"}\n`);
422
+ expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'unsupported', schema: 'compact-slices/v2' });
423
+ });
424
+ test('accepts CRLF Markdown with nested headings and fenced code in a required section', () => {
425
+ const plan = fixture(root);
426
+ const markdown = fs_1.default.readFileSync(plan, 'utf8')
427
+ .replace('One implementation.', 'Implementation prose.\n\n##### Nested detail\n\n```ts\n#### Not a real section\nconst value = true;\n```\n\nMore implementation prose.')
428
+ .replace(/\n/g, '\r\n');
429
+ fs_1.default.writeFileSync(plan, markdown);
430
+ expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'valid', schema: 'compact-slices/v1' });
431
+ });
432
+ test('does not classify a malformed future schema behind one broken marker as unsupported', () => {
433
+ const plan = path_1.default.join(root, 'partial-future-malformed.md');
434
+ fs_1.default.writeFileSync(plan, `${START}\n{"schema":"compact-slices/v2"`);
435
+ expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_MARKERS' })] });
436
+ });
437
+ });
@@ -10,6 +10,44 @@ const path_1 = __importDefault(require("path"));
10
10
  const crypto_1 = __importDefault(require("crypto"));
11
11
  const cliRoot = path_1.default.resolve(__dirname, '../..');
12
12
  const bin = path_1.default.join(cliRoot, 'dist', 'src', 'index.js');
13
+ function wait(ms) {
14
+ return new Promise((resolve) => setTimeout(resolve, ms));
15
+ }
16
+ test.each([
17
+ ['plain preflight', ['preflight']],
18
+ ['JSON preflight', ['preflight', '--json']],
19
+ ['JSON sensor-verification preflight', ['preflight', '--verify-sensors', '--json']],
20
+ ])('%s never starts the passive update worker', async (_name, args) => {
21
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-preflight-update-'));
22
+ const project = path_1.default.join(root, 'project');
23
+ const awmHome = path_1.default.join(root, 'awm-home');
24
+ const fetchStub = path_1.default.join(root, 'fetch-stub.cjs');
25
+ const updateCache = path_1.default.join(awmHome, 'update-check.json');
26
+ // The worker inherits NODE_OPTIONS. A deterministic immediate response makes a
27
+ // pre-fix worker write observable without relying on the network or a timeout.
28
+ fs_1.default.writeFileSync(fetchStub, "global.fetch = async () => ({ ok: true, json: async () => ({ version: '999.0.0' }) });\n");
29
+ fs_1.default.mkdirSync(project, { recursive: true });
30
+ try {
31
+ const result = (0, child_process_1.spawnSync)(process.execPath, [bin, ...args], {
32
+ cwd: project,
33
+ encoding: 'utf8',
34
+ env: {
35
+ ...process.env,
36
+ AWM_HOME: awmHome,
37
+ AWM_NO_UPDATE_CHECK: '',
38
+ NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ''} --require=${fetchStub}`.trim(),
39
+ },
40
+ });
41
+ expect(result.status).toBe(1);
42
+ // A spawned worker using the deterministic response above writes well inside
43
+ // this window; the delay also verifies it was never merely deferred.
44
+ await wait(1_000);
45
+ expect(fs_1.default.existsSync(updateCache)).toBe(false);
46
+ }
47
+ finally {
48
+ fs_1.default.rmSync(root, { recursive: true, force: true });
49
+ }
50
+ });
13
51
  test('preserves actionable no-manifest preflight JSON when verify-sensors is piped', () => {
14
52
  const project = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-preflight-pipe-'));
15
53
  try {
@@ -0,0 +1,37 @@
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
+ const cliReference = fs_1.default.readFileSync(path_1.default.resolve(__dirname, '../../../docs/cli-reference.md'), 'utf8');
9
+ const readme = fs_1.default.readFileSync(path_1.default.resolve(__dirname, '../../../README.md'), 'utf8');
10
+ const installation = fs_1.default.readFileSync(path_1.default.resolve(__dirname, '../../../docs/installation.md'), 'utf8');
11
+ const developmentProcess = fs_1.default.readFileSync(path_1.default.resolve(__dirname, '../../../docs/guides/development-process.md'), 'utf8');
12
+ describe('R4 plan/currentness documentation contract', () => {
13
+ it('documents plan validation and the unchanged legacy path', () => {
14
+ expect(cliReference).toContain('awm plan validate PLAN_PATH');
15
+ expect(cliReference).toMatch(/legacy/i);
16
+ });
17
+ it('documents strict currentness separately from minCliVersion compatibility', () => {
18
+ expect(cliReference).toContain('--require-current');
19
+ expect(cliReference).toContain('minCliVersion');
20
+ expect(cliReference).toMatch(/currentness[\s\S]{0,700}compatib|compatib[\s\S]{0,700}currentness/i);
21
+ });
22
+ it('documents the exact cache-resistant bootstrap and its enforceable boundary', () => {
23
+ expect(cliReference).toContain('npm exec --yes --package=agentic-workflow-manager@latest -- awm preflight --require-current');
24
+ expect(cliReference).toMatch(/fresh CLI\/bootstrap/i);
25
+ expect(cliReference).toMatch(/cannot update a host or cached container that\s+never runs new code/i);
26
+ });
27
+ it('keeps supported commands, remedies, bootstrap, and the enforceable boundary discoverable across user docs', () => {
28
+ expect(readme).toContain('awm plan validate PLAN_PATH');
29
+ expect(readme).toContain('awm preflight --require-current');
30
+ expect(installation).toContain('npm exec --yes --package=agentic-workflow-manager@latest -- awm preflight --require-current');
31
+ expect(installation).toMatch(/cannot update a host or cached container that\s+never runs new code/i);
32
+ expect(developmentProcess).toContain('awm preflight --require-current');
33
+ expect(developmentProcess).toContain('awm plan validate PLAN_PATH');
34
+ expect(developmentProcess).toMatch(/awm update --yes/);
35
+ expect(cliReference).toMatch(/awm unpin REGISTRY_NAME.*awm update --yes/s);
36
+ });
37
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-workflow-manager",
3
- "version": "9.3.0",
3
+ "version": "9.4.1",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"