agentic-workflow-manager 9.7.0 → 9.8.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 (114) hide show
  1. package/README.md +5 -1
  2. package/dist/src/commands/doctor.js +4 -2
  3. package/dist/src/commands/export.js +3 -0
  4. package/dist/src/commands/init.js +10 -2
  5. package/dist/src/commands/job/exec-wrapper.js +53 -3
  6. package/dist/src/commands/plan/index.js +124 -6
  7. package/dist/src/commands/registry/add.js +2 -2
  8. package/dist/src/commands/registry/index.js +7 -5
  9. package/dist/src/commands/registry/install-bundles.js +4 -4
  10. package/dist/src/commands/registry/status.js +4 -4
  11. package/dist/src/commands/sensors/bootstrap.js +36 -5
  12. package/dist/src/commands/sensors/compatibility/contract.js +19 -4
  13. package/dist/src/commands/sensors/compatibility/discovery.js +23 -3
  14. package/dist/src/commands/sensors/compatibility/live.js +18 -1
  15. package/dist/src/commands/sensors/compatibility/probe.js +8 -2
  16. package/dist/src/commands/sensors/compatibility/resolve.js +5 -2
  17. package/dist/src/commands/sensors/coverage/evidence.js +19 -10
  18. package/dist/src/commands/sensors/coverage/index.js +11 -2
  19. package/dist/src/commands/sensors/coverage/resolve.js +10 -0
  20. package/dist/src/commands/sensors/exec.js +16 -1
  21. package/dist/src/commands/sensors/migrate.js +62 -2
  22. package/dist/src/commands/sensors/prepare.js +2 -0
  23. package/dist/src/commands/sensors/result.js +2 -0
  24. package/dist/src/commands/sensors/run.js +64 -1
  25. package/dist/src/commands/sensors/status.js +6 -4
  26. package/dist/src/commands/sync.js +2 -1
  27. package/dist/src/commands/update.js +3 -1
  28. package/dist/src/commands/watch/archive-unused.js +138 -0
  29. package/dist/src/commands/watch/generations.js +32 -9
  30. package/dist/src/commands/watch/index.js +108 -2
  31. package/dist/src/commands/watch/init.js +149 -18
  32. package/dist/src/commands/watch/supervisor.js +167 -13
  33. package/dist/src/commands/watch/teardown-driver.js +13 -13
  34. package/dist/src/commands/watch/track-state.js +59 -0
  35. package/dist/src/commands/watch/tracks.js +38 -102
  36. package/dist/src/core/admission/index.js +181 -0
  37. package/dist/src/core/admission/registry-contracts.js +290 -0
  38. package/dist/src/core/bundle-install.js +2 -2
  39. package/dist/src/core/bundles.js +92 -19
  40. package/dist/src/core/diagnostics/context.js +1 -1
  41. package/dist/src/core/export/index.js +1 -1
  42. package/dist/src/core/export/resolve.js +2 -2
  43. package/dist/src/core/journal/fingerprint.js +35 -1
  44. package/dist/src/core/journal/paths.js +11 -0
  45. package/dist/src/core/journal/store.js +146 -4
  46. package/dist/src/core/journal/types.js +35 -1
  47. package/dist/src/core/migration/historical-provenance.js +18 -0
  48. package/dist/src/core/migration/index.js +322 -0
  49. package/dist/src/core/plan/identity.js +75 -0
  50. package/dist/src/core/plan/validate.js +178 -64
  51. package/dist/src/core/reconciliation.js +1 -1
  52. package/dist/src/index.js +46 -39
  53. package/dist/src/utils/grouping.js +1 -1
  54. package/dist/src/utils/registry-view.js +1 -1
  55. package/dist/tests/commands/bundle-diagnostics.test.js +145 -0
  56. package/dist/tests/commands/export.test.js +28 -2
  57. package/dist/tests/commands/job/exec-wrapper.test.js +16 -0
  58. package/dist/tests/commands/job/gate-reconcile.test.js +24 -10
  59. package/dist/tests/commands/multi-agent-targeting.test.js +1 -1
  60. package/dist/tests/commands/pin.test.js +23 -23
  61. package/dist/tests/commands/plan/index.test.js +630 -14
  62. package/dist/tests/commands/sensors/bootstrap.test.js +57 -4
  63. package/dist/tests/commands/sensors/compatibility/contract.test.js +21 -0
  64. package/dist/tests/commands/sensors/compatibility/discovery.test.js +19 -0
  65. package/dist/tests/commands/sensors/compatibility/probe.test.js +14 -1
  66. package/dist/tests/commands/sensors/compatibility/resolve.test.js +17 -0
  67. package/dist/tests/commands/sensors/coverage/evidence.test.js +51 -27
  68. package/dist/tests/commands/sensors/coverage/resolve.test.js +51 -0
  69. package/dist/tests/commands/sensors/exec.test.js +112 -0
  70. package/dist/tests/commands/sensors/run-changed.test.js +11 -0
  71. package/dist/tests/commands/sensors/run.test.js +15 -2
  72. package/dist/tests/commands/sensors/status.test.js +3 -3
  73. package/dist/tests/commands/watch/e2e-crash.test.js +126 -11
  74. package/dist/tests/commands/watch/generations.test.js +113 -4
  75. package/dist/tests/commands/watch/plan-lifecycle.test.js +263 -0
  76. package/dist/tests/commands/watch/supervisor-loop.test.js +247 -49
  77. package/dist/tests/commands/watch/track-finalize.test.js +20 -3
  78. package/dist/tests/commands/watch/track-freeze.test.js +43 -30
  79. package/dist/tests/commands/watch/track-state-dependency.test.js +15 -0
  80. package/dist/tests/commands/watch/watch-init.test.js +244 -0
  81. package/dist/tests/core/admission/index.test.js +87 -0
  82. package/dist/tests/core/bundle-install.test.js +36 -0
  83. package/dist/tests/core/bundles-multiroot.test.js +24 -2
  84. package/dist/tests/core/bundles-overrides.test.js +10 -7
  85. package/dist/tests/core/bundles.test.js +75 -6
  86. package/dist/tests/core/diagnostics/context.test.js +2 -2
  87. package/dist/tests/core/export/engine.test.js +1 -1
  88. package/dist/tests/core/export/resolve.test.js +1 -1
  89. package/dist/tests/core/init/mutation-targets.test.js +1 -1
  90. package/dist/tests/core/init/orchestrator.test.js +1 -1
  91. package/dist/tests/core/init/steps.test.js +1 -1
  92. package/dist/tests/core/journal/fingerprint.test.js +22 -0
  93. package/dist/tests/core/journal/store.test.js +53 -0
  94. package/dist/tests/core/journal/types.test.js +7 -0
  95. package/dist/tests/core/migration/facts.test.js +289 -0
  96. package/dist/tests/core/migration/historical-provenance.test.js +25 -0
  97. package/dist/tests/core/plan/validate.test.js +320 -6
  98. package/dist/tests/core/reconciliation.test.js +1 -1
  99. package/dist/tests/core/sync-profile-atomicity.test.js +1 -1
  100. package/dist/tests/integration/pack-e2e.test.js +4 -1
  101. package/dist/tests/integration/preflight-json-pipe.e2e.test.js +7 -3
  102. package/dist/tests/integration/sensor-bootstrap.e2e.test.js +37 -0
  103. package/dist/tests/integration/sensor-structured-run.e2e.test.js +12 -10
  104. package/dist/tests/providers/injection-config.test.js +14 -6
  105. package/dist/tests/structural/bundle-skill-reference-contract.test.js +56 -0
  106. package/dist/tests/structural/jest-environment-is-isolated.test.js +29 -24
  107. package/dist/tests/structural/native-release-artifacts.test.js +16 -0
  108. package/dist/tests/utils/grouping.test.js +2 -2
  109. package/dist/tests/utils/registry-view.test.js +7 -7
  110. package/package.json +2 -2
  111. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  112. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  113. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  114. package/prebuilds/win32-x64/secure_fs.node +0 -0
@@ -0,0 +1,75 @@
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
+ exports.EXECUTION_IDENTITY_SCHEMA = exports.MAX_PLAN_SNAPSHOT_BYTES = void 0;
7
+ exports.normalizedPlanSnapshot = normalizedPlanSnapshot;
8
+ exports.fullPlanDigest = fullPlanDigest;
9
+ exports.executionPlanDigest = executionPlanDigest;
10
+ const crypto_1 = __importDefault(require("crypto"));
11
+ exports.MAX_PLAN_SNAPSHOT_BYTES = 1024 * 1024;
12
+ exports.EXECUTION_IDENTITY_SCHEMA = 'awm-plan-execution/v1';
13
+ const START = '<!-- AWM:COMPACT-SLICES:START v1 -->';
14
+ const END = '<!-- AWM:COMPACT-SLICES:END v1 -->';
15
+ /** Only lifecycle syntax actually emitted by the skills is non-executable.
16
+ * Unknown metadata, prose, inline comments and code remain identity-bearing. */
17
+ function lifecycleMarker(line) {
18
+ const match = /^ {0,3}<!-- (awm-(?:qa|docs|retro)-complete)(?:: (\d{4}-\d{2}-\d{2}|Release [A-Za-z0-9][A-Za-z0-9_-]*))? -->$/.exec(line);
19
+ if (!match)
20
+ return undefined;
21
+ const metadata = match[2];
22
+ if (metadata && !metadata.startsWith('Release ')) {
23
+ const parsed = new Date(`${metadata}T00:00:00.000Z`);
24
+ if (!Number.isFinite(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== metadata)
25
+ return undefined;
26
+ }
27
+ return `${match[1]}:${metadata?.startsWith('Release ') ? metadata : ''}`;
28
+ }
29
+ function normalizedPlanSnapshot(text) {
30
+ if (typeof text !== 'string' || text.length === 0 || Buffer.byteLength(text, 'utf8') > exports.MAX_PLAN_SNAPSHOT_BYTES || text.includes('\0')) {
31
+ throw new Error('plan snapshot must be nonempty bounded text without NUL');
32
+ }
33
+ return text.replace(/\r\n?/g, '\n');
34
+ }
35
+ function fullPlanDigest(text) {
36
+ return crypto_1.default.createHash('sha256').update(normalizedPlanSnapshot(text), 'utf8').digest('hex');
37
+ }
38
+ /** This versioned identity preserves ALL plan bytes except recognized progress
39
+ * status bytes and unique standalone lifecycle marker lines outside code/JSON.
40
+ * It is never a replacement for the full validated-plan digest. */
41
+ function executionPlanDigest(text) {
42
+ const lines = normalizedPlanSnapshot(text).split('\n');
43
+ let fence;
44
+ let manifest = false;
45
+ const candidates = lines.map(line => {
46
+ const matched = /^(?: {0,3})(`{3,}|~{3,})(.*)$/.exec(line);
47
+ const marker = matched?.[1];
48
+ if (marker && (fence
49
+ ? marker[0] === fence.character && marker.length >= fence.length && /^[ \t]*$/.test(matched[2])
50
+ : marker[0] !== '`' || !matched[2].includes('`'))) {
51
+ fence = fence ? undefined : { character: marker[0], length: marker.length };
52
+ return { line };
53
+ }
54
+ if (fence)
55
+ return { line };
56
+ if (line === START) {
57
+ manifest = true;
58
+ return { line };
59
+ }
60
+ if (line === END) {
61
+ manifest = false;
62
+ return { line };
63
+ }
64
+ if (manifest)
65
+ return { line };
66
+ const lifecycle = lifecycleMarker(line);
67
+ return { line: line.replace(/^( {0,3}[-*+] +)\[[ xX]\]( +\S.*)$/, '$1[ ]$2'), lifecycle };
68
+ });
69
+ const counts = new Map();
70
+ for (const { lifecycle } of candidates)
71
+ if (lifecycle)
72
+ counts.set(lifecycle, (counts.get(lifecycle) ?? 0) + 1);
73
+ const canonical = candidates.filter(({ lifecycle }) => !lifecycle || counts.get(lifecycle) !== 1).map(({ line }) => line).join('\n');
74
+ return crypto_1.default.createHash('sha256').update(`${exports.EXECUTION_IDENTITY_SCHEMA}\0`).update(canonical, 'utf8').digest('hex');
75
+ }
@@ -3,22 +3,51 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.assertVerifiedValidPlanReport = assertVerifiedValidPlanReport;
7
+ exports.verifiedPlanSnapshot = verifiedPlanSnapshot;
6
8
  exports.validatePlanFile = validatePlanFile;
9
+ exports.validatePlanSnapshot = validatePlanSnapshot;
7
10
  const fs_1 = __importDefault(require("fs"));
8
11
  const path_1 = __importDefault(require("path"));
12
+ const crypto_1 = __importDefault(require("crypto"));
9
13
  const json_1 = require("./json");
14
+ const identity_1 = require("./identity");
10
15
  const START = '<!-- AWM:COMPACT-SLICES:START v1 -->';
11
16
  const END = '<!-- AWM:COMPACT-SLICES:END v1 -->';
12
17
  const MAX_PLAN = 1024 * 1024;
13
18
  const MAX_MANIFEST = 256 * 1024;
14
19
  const MAX_SOURCE = 1024 * 1024;
15
20
  const MAX_STRING = 4096;
16
- const ID = /^[A-Z][A-Z0-9-]{0,63}$/;
21
+ const ENTITY_ID = /^[A-Z][A-Z0-9-]{0,63}$/;
22
+ const REQUIREMENT_ID = /^[A-Z][A-Z0-9]*(?:-[A-Z0-9]+)*(?:\.[A-Z0-9]+(?:-[A-Z0-9]+)*)*$/;
17
23
  const PLAN_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
18
24
  const SHELL = new Set([
19
25
  'sh', 'bash', 'zsh', 'dash', 'ksh', 'fish', 'cmd', 'powershell', 'pwsh',
20
26
  'env', 'busybox', 'node', 'nodejs', 'deno', 'bun', 'python', 'python3', 'ruby', 'perl', 'php', 'lua',
21
27
  ]);
28
+ const verifiedValidReports = new WeakSet();
29
+ const verifiedSnapshots = new WeakMap();
30
+ function freezeJson(value) {
31
+ if (value === null || typeof value !== 'object')
32
+ return;
33
+ for (const child of Object.values(value))
34
+ freezeJson(child);
35
+ Object.freeze(value);
36
+ }
37
+ /** Require a process-local, immutable report issued by successful core validation. */
38
+ function assertVerifiedValidPlanReport(report) {
39
+ if (!report || typeof report !== 'object' || report.state !== 'valid' || !verifiedValidReports.has(report)) {
40
+ throw new Error('plan validator returned an invalid valid report');
41
+ }
42
+ }
43
+ /** The exact immutable validator snapshot, never a later reopen of the path. */
44
+ function verifiedPlanSnapshot(report) {
45
+ assertVerifiedValidPlanReport(report);
46
+ const snapshot = verifiedSnapshots.get(report);
47
+ if (snapshot === undefined)
48
+ throw new Error('verified plan snapshot unavailable');
49
+ return snapshot;
50
+ }
22
51
  function isLauncher(program) {
23
52
  const base = path_1.default.posix.basename(program).toLowerCase();
24
53
  return SHELL.has(base.replace(/\.exe$/, '')) || /^(?:node(?:js)?|py(?:thon(?:w)?\d*(?:\.\d+)?)?|deno|bun|ruby(?:\d+(?:\.\d+)*)?|perl(?:\d+(?:\.\d+)*)?|(?:a|ba|c|tc|z|da|k)?sh|busybox|cmd|powershell|pwsh|wscript|cscript)(?:\.exe)?(?:[-.][a-z0-9][a-z0-9.-]*)?$/i.test(base);
@@ -37,39 +66,49 @@ function schemaClassification(candidate) {
37
66
  return diagnostic('PLAN_MARKERS', 'compact markers must occur once in order');
38
67
  }
39
68
  function embeddedJsonObjects(text) {
40
- const objects = [];
41
- for (let start = 0; start < text.length && objects.length < 64; start += 1) {
42
- if (text[start] !== '{')
43
- continue;
44
- let depth = 0;
45
- let quoted = false;
46
- let escaped = false;
47
- let end = start;
48
- for (; end < text.length && end - start <= MAX_MANIFEST; end += 1) {
49
- const character = text[end];
50
- if (quoted) {
51
- if (escaped)
52
- escaped = false;
53
- else if (character === '\\')
54
- escaped = true;
55
- else if (character === '"')
56
- quoted = false;
57
- continue;
58
- }
59
- if (character === '"') {
60
- quoted = true;
61
- continue;
62
- }
69
+ const outer = [];
70
+ const nested = [];
71
+ const starts = [];
72
+ let quoted = false;
73
+ let escaped = false;
74
+ for (let index = 0; index < text.length; index += 1) {
75
+ const character = text[index];
76
+ if (starts.length === 0) {
63
77
  if (character === '{')
64
- depth += 1;
65
- if (character === '}' && --depth === 0) {
66
- objects.push(text.slice(start, end + 1));
67
- start = end;
68
- break;
78
+ starts.push(index);
79
+ continue;
80
+ }
81
+ if (quoted) {
82
+ if (character === '\n') {
83
+ quoted = false;
84
+ escaped = false;
69
85
  }
86
+ else if (escaped)
87
+ escaped = false;
88
+ else if (character === '\\')
89
+ escaped = true;
90
+ else if (character === '"')
91
+ quoted = false;
92
+ continue;
93
+ }
94
+ if (character === '"') {
95
+ quoted = true;
96
+ continue;
97
+ }
98
+ if (character === '{') {
99
+ starts.push(index);
100
+ continue;
70
101
  }
102
+ if (character !== '}')
103
+ continue;
104
+ const start = starts.pop();
105
+ if (start === undefined || index - start > MAX_MANIFEST)
106
+ continue;
107
+ const found = starts.length === 0 ? outer : nested;
108
+ if (found.length < 64)
109
+ found.push(text.slice(start, index + 1));
71
110
  }
72
- return objects;
111
+ return [...outer, ...nested].slice(0, 64);
73
112
  }
74
113
  function markerlessSchemaClassification(text) {
75
114
  const candidates = [text.trim(), ...embeddedJsonObjects(text)];
@@ -123,11 +162,18 @@ function compactSchemaSignal(text) {
123
162
  }
124
163
  return false;
125
164
  }
165
+ function executionModeFromValidatedText(text) {
166
+ const raw = /^\s*\*\*Modo de ejecución:\*\*\s*([^\r\n]+)\s*$/mi.exec(text)?.[1]?.trim().replace(/^`|`$/g, '');
167
+ return raw === 'desatendido' ? 'desatendido' : 'interactivo';
168
+ }
126
169
  function exact(value, fields) {
127
170
  return object(value) && Object.keys(value).length === fields.length && fields.every((field) => Object.prototype.hasOwnProperty.call(value, field));
128
171
  }
129
172
  function allStrings(value, max = MAX_STRING) { return Array.isArray(value) && value.every((item) => typeof item === 'string' && Buffer.byteLength(item, 'utf8') <= max); }
130
- function validId(value) { return typeof value === 'string' && ID.test(value); }
173
+ function validId(value) { return typeof value === 'string' && ENTITY_ID.test(value); }
174
+ function validRequirementId(value) {
175
+ return typeof value === 'string' && Buffer.byteLength(value, 'utf8') <= 64 && REQUIREMENT_ID.test(value);
176
+ }
131
177
  function inside(root, candidate) { const relative = path_1.default.relative(root, candidate); return relative === '' || (!relative.startsWith(`..${path_1.default.sep}`) && relative !== '..' && !path_1.default.isAbsolute(relative)); }
132
178
  function validRelative(value) {
133
179
  return typeof value === 'string' && value.length > 0 && Buffer.byteLength(value, 'utf8') <= MAX_STRING && !path_1.default.isAbsolute(value) && !path_1.default.win32.isAbsolute(value) && !value.includes('\\') && value.split('/').every((part) => part !== '' && part !== '.' && part !== '..') && path_1.default.posix.normalize(value) === value;
@@ -140,30 +186,74 @@ function regularInside(root, relative) {
140
186
  return undefined;
141
187
  const parts = relative.split('/');
142
188
  let current = root;
189
+ let stat;
143
190
  try {
144
191
  for (const part of parts) {
145
192
  current = path_1.default.join(current, part);
146
- if (fs_1.default.lstatSync(current).isSymbolicLink())
193
+ stat = fs_1.default.lstatSync(current, { bigint: true });
194
+ if (stat.isSymbolicLink())
147
195
  return undefined;
148
196
  }
149
- if (!fs_1.default.lstatSync(candidate).isFile())
197
+ if (!stat?.isFile())
150
198
  return undefined;
151
199
  const real = fs_1.default.realpathSync(candidate);
152
- return inside(root, real) ? real : undefined;
200
+ return inside(root, real) ? { path: real, stat } : undefined;
153
201
  }
154
202
  catch {
155
203
  return undefined;
156
204
  }
157
205
  }
158
- function executableInside(root, relative) {
159
- const file = regularInside(root, relative);
206
+ function sameIdentity(inspected, opened) {
207
+ return typeof inspected.dev === 'bigint' && typeof inspected.ino === 'bigint' && inspected.ino !== 0n
208
+ && opened.isFile() && inspected.dev === opened.dev && inspected.ino === opened.ino;
209
+ }
210
+ function readInspected(file, max) {
211
+ if (file.stat.size > BigInt(max))
212
+ return { state: 'limit' };
213
+ let descriptor;
160
214
  try {
161
- return file && (process.platform === 'win32' ? /\.(?:exe|cmd|bat)$/i.test(file) : (fs_1.default.statSync(file).mode & 0o111) !== 0) ? file : undefined;
215
+ descriptor = fs_1.default.openSync(file.path, fs_1.default.constants.O_RDONLY | (fs_1.default.constants.O_NOFOLLOW ?? 0));
216
+ const opened = fs_1.default.fstatSync(descriptor, { bigint: true });
217
+ if (!sameIdentity(file.stat, opened))
218
+ return { state: 'unsafe' };
219
+ if (opened.size > BigInt(max))
220
+ return { state: 'limit' };
221
+ if (opened.size !== file.stat.size)
222
+ return { state: 'unsafe' };
223
+ const bytes = Buffer.alloc(Number(opened.size));
224
+ let offset = 0;
225
+ while (offset < bytes.length) {
226
+ const read = fs_1.default.readSync(descriptor, bytes, offset, bytes.length - offset, offset);
227
+ if (read <= 0)
228
+ return { state: 'read' };
229
+ offset += read;
230
+ }
231
+ const after = fs_1.default.fstatSync(descriptor, { bigint: true });
232
+ if (!sameIdentity(file.stat, after))
233
+ return { state: 'unsafe' };
234
+ if (after.size > BigInt(max) || bytes.length > max)
235
+ return { state: 'limit' };
236
+ if (after.size !== opened.size)
237
+ return { state: 'unsafe' };
238
+ return { state: 'ok', bytes };
162
239
  }
163
- catch {
164
- return undefined;
240
+ catch (error) {
241
+ const code = error.code;
242
+ return { state: code === 'ELOOP' || code === 'ENOENT' || code === 'ENOTDIR' ? 'unsafe' : 'read' };
243
+ }
244
+ finally {
245
+ if (descriptor !== undefined) {
246
+ try {
247
+ fs_1.default.closeSync(descriptor);
248
+ }
249
+ catch { /* best-effort close after a failed inspected read */ }
250
+ }
165
251
  }
166
252
  }
253
+ function executableInside(root, relative) {
254
+ const file = regularInside(root, relative);
255
+ return file && (process.platform === 'win32' ? /\.(?:exe|cmd|bat)$/i.test(file.path) : (file.stat.mode & 73n) !== 0n) ? file.path : undefined;
256
+ }
167
257
  function refs(value, known) { return allStrings(value) && value.every((id) => known.has(id)); }
168
258
  function uniqueRefs(value, known) { return refs(value, known) && new Set(value).size === value.length; }
169
259
  function unsafeCommand(value) { return /[\0\r\n]|[`$]|\$\(|\$\{|[;&|<>*?\[\]{}()]/.test(value); }
@@ -219,8 +309,8 @@ function checkMarkdown(text, slices) {
219
309
  }
220
310
  return undefined;
221
311
  }
222
- /** Read-only validator for a bounded compact-slices/v1 Markdown plan. */
223
- function validatePlanFile(planPath, cwd = process.cwd()) {
312
+ /** Shared parser. A supplied snapshot is already descriptor-anchored by its caller. */
313
+ function validatePlan(planPath, cwd, snapshot) {
224
314
  if (typeof planPath !== 'string' || planPath.length === 0)
225
315
  throw new Error('planPath must be a non-empty path');
226
316
  if (typeof cwd !== 'string' || cwd.length === 0)
@@ -238,21 +328,28 @@ function validatePlanFile(planPath, cwd = process.cwd()) {
238
328
  if (!inside(root, candidate))
239
329
  return diagnostic('PLAN_PATH_UNSAFE', 'plan path must be inside cwd');
240
330
  const relativePlan = path_1.default.relative(root, candidate).replace(/\\/g, '/');
241
- const planFile = regularInside(root, relativePlan);
242
- if (!planFile)
243
- return diagnostic('PLAN_PATH_UNSAFE', 'plan path must be a contained regular non-symlink file');
244
331
  let bytes;
245
- try {
246
- if (fs_1.default.statSync(planFile).size > MAX_PLAN)
332
+ if (snapshot) {
333
+ if (snapshot.length > MAX_PLAN)
247
334
  return diagnostic('PLAN_LIMIT', 'plan exceeds maximum size');
248
- bytes = fs_1.default.readFileSync(planFile);
335
+ bytes = Buffer.from(snapshot);
249
336
  }
250
- catch {
251
- return diagnostic('PLAN_READ', 'plan cannot be read');
337
+ else {
338
+ const planFile = regularInside(root, relativePlan);
339
+ if (!planFile)
340
+ return diagnostic('PLAN_PATH_UNSAFE', 'plan path must be a contained regular non-symlink file');
341
+ const planRead = readInspected(planFile, MAX_PLAN);
342
+ if (planRead.state === 'unsafe')
343
+ return diagnostic('PLAN_PATH_UNSAFE', 'plan path changed after inspection');
344
+ if (planRead.state === 'limit')
345
+ return diagnostic('PLAN_LIMIT', 'plan exceeds maximum size');
346
+ if (planRead.state === 'read')
347
+ return diagnostic('PLAN_READ', 'plan cannot be read');
348
+ bytes = planRead.bytes;
252
349
  }
253
350
  let text;
254
351
  try {
255
- text = normalizeLineEndings(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
352
+ text = normalizeLineEndings(new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes));
256
353
  }
257
354
  catch {
258
355
  return diagnostic('PLAN_ENCODING', 'plan must be valid UTF-8');
@@ -261,7 +358,7 @@ function validatePlanFile(planPath, cwd = process.cwd()) {
261
358
  const ends = count(text, END);
262
359
  const signaled = starts > 0 || ends > 0 || compactSchemaSignal(text);
263
360
  if (!signaled)
264
- return markerlessSchemaClassification(text) ?? { state: 'legacy' };
361
+ return markerlessSchemaClassification(text) ?? { state: 'migration-required', reason: 'unmarked-plan' };
265
362
  if (starts === 0 && ends === 0)
266
363
  return markerlessSchemaClassification(text) ?? diagnostic('PLAN_MARKERS', 'compact markers must occur once in order');
267
364
  if (starts !== 1 || ends !== 1 || text.indexOf(START) > text.indexOf(END)) {
@@ -300,7 +397,7 @@ function validatePlanFile(planPath, cwd = process.cwd()) {
300
397
  return diagnostic('PLAN_LIMIT', 'plan id exceeds maximum string size');
301
398
  if (raw.schema !== 'compact-slices/v1' || typeof raw.planId !== 'string' || !PLAN_ID.test(raw.planId) || !allStrings(raw.requirements) || new Set(raw.requirements).size !== raw.requirements.length)
302
399
  return diagnostic('PLAN_SHAPE', 'manifest scalar fields are invalid');
303
- if (raw.requirements.some((id) => !ID.test(id)) || !allStrings(raw.closureCommands) || new Set(raw.closureCommands).size !== raw.closureCommands.length)
400
+ if (raw.requirements.some((id) => !validRequirementId(id)) || !allStrings(raw.closureCommands) || new Set(raw.closureCommands).size !== raw.closureCommands.length)
304
401
  return diagnostic('PLAN_SHAPE', 'manifest arrays or identifiers are invalid');
305
402
  const sourceIds = new Set();
306
403
  for (const source of raw.sources) {
@@ -310,18 +407,16 @@ function validatePlanFile(planPath, cwd = process.cwd()) {
310
407
  const file = regularInside(root, source.path);
311
408
  if (!file)
312
409
  return diagnostic('PLAN_SOURCE_UNSAFE', 'source path must be a contained regular non-symlink file');
313
- let bytes;
314
- try {
315
- if (fs_1.default.statSync(file).size > MAX_SOURCE)
316
- return diagnostic('PLAN_SOURCE_LIMIT', 'source exceeds maximum size');
317
- bytes = fs_1.default.readFileSync(file);
318
- }
319
- catch {
410
+ const sourceRead = readInspected(file, MAX_SOURCE);
411
+ if (sourceRead.state === 'unsafe')
412
+ return diagnostic('PLAN_SOURCE_UNSAFE', 'source path changed after inspection');
413
+ if (sourceRead.state === 'limit')
414
+ return diagnostic('PLAN_SOURCE_LIMIT', 'source exceeds maximum size');
415
+ if (sourceRead.state === 'read')
320
416
  return diagnostic('PLAN_SOURCE_UNSAFE', 'source cannot be read');
321
- }
322
417
  let contents;
323
418
  try {
324
- contents = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
419
+ contents = new TextDecoder('utf-8', { fatal: true }).decode(sourceRead.bytes);
325
420
  }
326
421
  catch {
327
422
  return diagnostic('PLAN_SOURCE_ENCODING', 'source must be valid UTF-8');
@@ -332,7 +427,7 @@ function validatePlanFile(planPath, cwd = process.cwd()) {
332
427
  const commandIds = new Set();
333
428
  const closureIds = new Set(raw.closureCommands);
334
429
  for (const command of raw.commands) {
335
- if (!exact(command, ['id', 'program', 'args', 'covers']) || !validId(command.id) || commandIds.has(command.id) || typeof command.program !== 'string' || command.program.length === 0 || Buffer.byteLength(command.program, 'utf8') > MAX_STRING || !allStrings(command.args) || command.args.length > 128 || !allStrings(command.covers) || new Set(command.covers).size !== command.covers.length || (command.covers.length === 0 && !closureIds.has(command.id)) || command.covers.some((id) => !raw.requirements.includes(id)))
430
+ if (!exact(command, ['id', 'program', 'args', 'covers']) || !validId(command.id) || commandIds.has(command.id) || typeof command.program !== 'string' || command.program.length === 0 || Buffer.byteLength(command.program, 'utf8') > MAX_STRING || !allStrings(command.args) || command.args.length > 128 || !allStrings(command.covers) || new Set(command.covers).size !== command.covers.length || (command.covers.length === 0 && !closureIds.has(command.id)) || command.covers.some((id) => !validRequirementId(id) || !raw.requirements.includes(id)))
336
431
  return diagnostic('PLAN_COMMAND_SHAPE', 'command fields are invalid');
337
432
  commandIds.add(command.id);
338
433
  const program = command.program;
@@ -344,7 +439,7 @@ function validatePlanFile(planPath, cwd = process.cwd()) {
344
439
  const sliceIds = new Set();
345
440
  const owners = new Map();
346
441
  for (const slice of raw.slices) {
347
- if (!exact(slice, ['id', 'title', 'requirements', 'dependsOn', 'sectionAnchor', 'sources', 'redCommands', 'greenCommands', 'reviewEvidence', 'risk', 'fallback']) || !validId(slice.id) || sliceIds.has(slice.id) || typeof slice.title !== 'string' || slice.title.length === 0 || Buffer.byteLength(slice.title, 'utf8') > MAX_STRING || !allStrings(slice.requirements) || slice.requirements.length === 0 || slice.requirements.some((id) => !raw.requirements.includes(id)) || !allStrings(slice.dependsOn) || typeof slice.sectionAnchor !== 'string' || slice.sectionAnchor.length === 0 || Buffer.byteLength(slice.sectionAnchor, 'utf8') > MAX_STRING || !uniqueRefs(slice.sources, sourceIds) || !uniqueRefs(slice.redCommands, commandIds) || slice.redCommands.length === 0 || !uniqueRefs(slice.greenCommands, commandIds) || slice.greenCommands.length === 0 || !allStrings(slice.fallback) || slice.fallback.length === 0 || slice.fallback.some((item) => item.trim().length === 0) || slice.risk !== 'bounded' && slice.risk !== 'full-context')
442
+ if (!exact(slice, ['id', 'title', 'requirements', 'dependsOn', 'sectionAnchor', 'sources', 'redCommands', 'greenCommands', 'reviewEvidence', 'risk', 'fallback']) || !validId(slice.id) || sliceIds.has(slice.id) || typeof slice.title !== 'string' || slice.title.length === 0 || Buffer.byteLength(slice.title, 'utf8') > MAX_STRING || !allStrings(slice.requirements) || slice.requirements.length === 0 || slice.requirements.some((id) => !validRequirementId(id) || !raw.requirements.includes(id)) || !allStrings(slice.dependsOn) || typeof slice.sectionAnchor !== 'string' || slice.sectionAnchor.length === 0 || Buffer.byteLength(slice.sectionAnchor, 'utf8') > MAX_STRING || !uniqueRefs(slice.sources, sourceIds) || !uniqueRefs(slice.redCommands, commandIds) || slice.redCommands.length === 0 || !uniqueRefs(slice.greenCommands, commandIds) || slice.greenCommands.length === 0 || !allStrings(slice.fallback) || slice.fallback.length === 0 || slice.fallback.some((item) => item.trim().length === 0) || slice.risk !== 'bounded' && slice.risk !== 'full-context')
348
443
  return diagnostic('PLAN_SLICE_SHAPE', 'slice fields are invalid');
349
444
  sliceIds.add(slice.id);
350
445
  if (!Array.isArray(slice.reviewEvidence) || slice.reviewEvidence.length !== 2 || new Set(slice.reviewEvidence).size !== 2 || !slice.reviewEvidence.includes('specification') || !slice.reviewEvidence.includes('code-quality'))
@@ -389,5 +484,24 @@ function validatePlanFile(planPath, cwd = process.cwd()) {
389
484
  const markdown = checkMarkdown(text, slices);
390
485
  if (markdown)
391
486
  return markdown;
392
- return { state: 'valid', schema: 'compact-slices/v1', manifest: raw };
487
+ const report = {
488
+ state: 'valid', schema: 'compact-slices/v1',
489
+ planDigest: crypto_1.default.createHash('sha256').update(text, 'utf8').digest('hex'),
490
+ executionDigest: (0, identity_1.executionPlanDigest)(text),
491
+ manifest: raw, executionMode: executionModeFromValidatedText(text),
492
+ };
493
+ freezeJson(report);
494
+ verifiedValidReports.add(report);
495
+ verifiedSnapshots.set(report, text);
496
+ return report;
497
+ }
498
+ /** Read-only validator for a bounded compact-slices/v1 Markdown plan. */
499
+ function validatePlanFile(planPath, cwd = process.cwd()) {
500
+ return validatePlan(planPath, cwd);
501
+ }
502
+ /** Validate an already-authenticated plan snapshot without reopening planPath. */
503
+ function validatePlanSnapshot(planPath, cwd, text) {
504
+ if (typeof text !== 'string')
505
+ throw new Error('plan snapshot must be text');
506
+ return validatePlan(planPath, cwd, Buffer.from(text, 'utf8'));
393
507
  }
@@ -16,7 +16,7 @@ function intentKey(intent) {
16
16
  */
17
17
  function planReconciliation(params) {
18
18
  const { targets, roots } = params;
19
- const bundles = params.bundles ?? (0, bundles_1.discoverAllBundles)(roots);
19
+ const bundles = params.bundles ?? (0, bundles_1.discoverAllBundles)(roots, params.reporter);
20
20
  const contentDir = roots[0] ?? '';
21
21
  const machineBundles = bundles.filter((b) => b.scope === 'baseline' || b.scope === 'ambient');
22
22
  const seen = new Set();