agentic-workflow-manager 9.4.1 → 9.6.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 (57) hide show
  1. package/dist/src/commands/preflight/checks.js +58 -27
  2. package/dist/src/commands/sensors/bootstrap.js +171 -0
  3. package/dist/src/commands/sensors/compatibility/contract.js +18 -15
  4. package/dist/src/commands/sensors/compatibility/manifest.js +75 -2
  5. package/dist/src/commands/sensors/compatibility/materialize.js +256 -54
  6. package/dist/src/commands/sensors/compatibility/pack-source.js +61 -17
  7. package/dist/src/commands/sensors/compatibility/safe-file.js +286 -0
  8. package/dist/src/commands/sensors/compatibility/source.js +75 -0
  9. package/dist/src/commands/sensors/coverage/index.js +2 -2
  10. package/dist/src/commands/sensors/coverage/resolve.js +35 -29
  11. package/dist/src/commands/sensors/detection.js +31 -0
  12. package/dist/src/commands/sensors/exec.js +116 -11
  13. package/dist/src/commands/sensors/index.js +51 -10
  14. package/dist/src/commands/sensors/init.js +17 -168
  15. package/dist/src/commands/sensors/migrate.js +209 -0
  16. package/dist/src/commands/sensors/project.js +107 -0
  17. package/dist/src/commands/sensors/run.js +66 -47
  18. package/dist/src/commands/sensors/status.js +97 -56
  19. package/dist/src/core/init/steps.js +24 -11
  20. package/dist/src/core/secure-fs/native-bridge.js +209 -0
  21. package/dist/tests/commands/preflight/preflight.test.js +31 -0
  22. package/dist/tests/commands/sensors/bootstrap-observational.test.js +24 -0
  23. package/dist/tests/commands/sensors/bootstrap.test.js +175 -0
  24. package/dist/tests/commands/sensors/compatibility/contract.test.js +38 -0
  25. package/dist/tests/commands/sensors/compatibility/manifest.test.js +90 -0
  26. package/dist/tests/commands/sensors/compatibility/materialize.test.js +410 -0
  27. package/dist/tests/commands/sensors/compatibility/pack-source.test.js +249 -0
  28. package/dist/tests/commands/sensors/compatibility/safe-file.test.js +52 -0
  29. package/dist/tests/commands/sensors/compatibility/source.test.js +121 -0
  30. package/dist/tests/commands/sensors/coverage/index.test.js +20 -0
  31. package/dist/tests/commands/sensors/coverage/resolve.test.js +36 -34
  32. package/dist/tests/commands/sensors/exec.test.js +86 -0
  33. package/dist/tests/commands/sensors/index.test.js +54 -0
  34. package/dist/tests/commands/sensors/init-pack-unavailable.test.js +21 -84
  35. package/dist/tests/commands/sensors/init.test.js +56 -490
  36. package/dist/tests/commands/sensors/migrate.test.js +513 -0
  37. package/dist/tests/commands/sensors/project.test.js +102 -0
  38. package/dist/tests/commands/sensors/run.test.js +107 -4
  39. package/dist/tests/commands/sensors/status.test.js +106 -2
  40. package/dist/tests/core/init/orchestrator.test.js +3 -1
  41. package/dist/tests/core/secure-fs/native-bridge.test.js +733 -0
  42. package/dist/tests/integration/preflight-json-pipe.e2e.test.js +5 -0
  43. package/dist/tests/integration/sensor-bootstrap.e2e.test.js +165 -0
  44. package/dist/tests/integration/sensor-compatibility.e2e.test.js +14 -23
  45. package/dist/tests/integration/sensor-coverage.e2e.test.js +15 -24
  46. package/dist/tests/integration/sensor-structured-run.e2e.test.js +3 -1
  47. package/dist/tests/structural/native-release-artifacts.test.js +22 -0
  48. package/dist/tests/structural/release-flags-are-reachable-from-ci.test.js +10 -0
  49. package/dist/tests/structural/release-registry-provenance.test.js +4 -0
  50. package/dist/tests/structural/sensor-portability-contract.test.js +44 -0
  51. package/package.json +6 -2
  52. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  53. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  54. package/prebuilds/linux-arm64/secure_fs.node +0 -0
  55. package/prebuilds/linux-x64/secure_fs.node +0 -0
  56. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  57. package/prebuilds/win32-x64/secure_fs.node +0 -0
@@ -0,0 +1,286 @@
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.setSecureFsForTests = setSecureFsForTests;
7
+ exports.setPortableReadForTests = setPortableReadForTests;
8
+ exports.withProjectLease = withProjectLease;
9
+ exports.writeProjectFile = writeProjectFile;
10
+ exports.removeObservedProjectFile = removeObservedProjectFile;
11
+ exports.withNoFollowDirectory = withNoFollowDirectory;
12
+ exports.withDescriptorBoundWriteDirectory = withDescriptorBoundWriteDirectory;
13
+ exports.withNoFollowChildDirectory = withNoFollowChildDirectory;
14
+ exports.readInspectedBoundedFile = readInspectedBoundedFile;
15
+ exports.readInspectedBoundedFileWithIdentity = readInspectedBoundedFileWithIdentity;
16
+ const fs_1 = __importDefault(require("fs"));
17
+ const path_1 = __importDefault(require("path"));
18
+ const native_bridge_1 = require("../../../core/secure-fs/native-bridge");
19
+ let secureFsForTest;
20
+ let portableReadForTests = false;
21
+ /** Test-only seam for filesystem race fixtures. Production always uses the packaged bridge. */
22
+ function setSecureFsForTests(boundary) {
23
+ secureFsForTest = boundary;
24
+ }
25
+ /** Test-only opt-in for Node-level filesystem race fixtures. */
26
+ function setPortableReadForTests(enabled) {
27
+ portableReadForTests = enabled;
28
+ }
29
+ function activeSecureFs() {
30
+ return secureFsForTest ?? native_bridge_1.secureFs;
31
+ }
32
+ /** Hold the native project lease for one complete sensor mutation callback. */
33
+ function withProjectLease(projectRoot, operation) {
34
+ return activeSecureFs().withProjectLease(projectRoot, operation);
35
+ }
36
+ /** Native-only project publication. Production never falls back to pathname writes. */
37
+ function writeProjectFile(projectRoot, destination, content, options) {
38
+ try {
39
+ activeSecureFs().writeProjectTransaction(projectRoot, destination, content, options);
40
+ }
41
+ catch (error) {
42
+ throw new Error(`project destination rejected by secure-fs: ${error.message}`);
43
+ }
44
+ }
45
+ /** Compensate only a file that this transaction read and identity-fenced. */
46
+ function removeObservedProjectFile(projectRoot, destination, identity) {
47
+ try {
48
+ const remove = activeSecureFs().removeObservedProjectFile;
49
+ if (typeof remove !== 'function')
50
+ throw new Error('secure-fs native bridge does not support identity-fenced removal');
51
+ remove(projectRoot, destination, identity);
52
+ }
53
+ catch (error) {
54
+ throw new Error(`project destination rejected by secure-fs: ${error.message}`);
55
+ }
56
+ }
57
+ function sameIdentity(left, right) {
58
+ return left.dev === right.dev && left.ino === right.ino;
59
+ }
60
+ function snapshotDirectoryChain(directory, failure) {
61
+ if (!path_1.default.isAbsolute(directory))
62
+ throw failure();
63
+ const resolved = path_1.default.resolve(directory);
64
+ const parsed = path_1.default.parse(resolved);
65
+ const snapshots = [];
66
+ let current = parsed.root;
67
+ for (const component of ['', ...path_1.default.relative(parsed.root, resolved).split(path_1.default.sep).filter(Boolean)]) {
68
+ if (component)
69
+ current = path_1.default.join(current, component);
70
+ let stat;
71
+ try {
72
+ stat = fs_1.default.lstatSync(current, { bigint: true });
73
+ }
74
+ catch {
75
+ throw failure();
76
+ }
77
+ if (!stat.isDirectory() || stat.isSymbolicLink())
78
+ throw failure();
79
+ snapshots.push({ name: current, stat });
80
+ }
81
+ return snapshots;
82
+ }
83
+ function assertDirectoryChainUnchanged(snapshots, failure) {
84
+ for (const snapshot of snapshots) {
85
+ let current;
86
+ try {
87
+ current = fs_1.default.lstatSync(snapshot.name, { bigint: true });
88
+ }
89
+ catch {
90
+ throw failure();
91
+ }
92
+ if (!current.isDirectory() || current.isSymbolicLink() || !sameIdentity(current, snapshot.stat))
93
+ throw failure();
94
+ }
95
+ }
96
+ function withPortableValidatedDirectory(directory, failure, operation) {
97
+ const snapshots = snapshotDirectoryChain(directory, failure);
98
+ let result;
99
+ let operationError;
100
+ try {
101
+ result = operation(directory);
102
+ }
103
+ catch (error) {
104
+ operationError = error;
105
+ }
106
+ assertDirectoryChainUnchanged(snapshots, failure);
107
+ if (operationError !== undefined)
108
+ throw operationError;
109
+ return result;
110
+ }
111
+ function procfsDescriptorBridgeAvailable() {
112
+ try {
113
+ return process.platform === 'linux' && fs_1.default.statSync('/proc/self/fd').isDirectory();
114
+ }
115
+ catch {
116
+ return false;
117
+ }
118
+ }
119
+ function withOpenedNoFollowDirectory(open, failure, operation) {
120
+ let descriptor;
121
+ try {
122
+ descriptor = open();
123
+ const opened = fs_1.default.fstatSync(descriptor, { bigint: true });
124
+ if (!opened.isDirectory())
125
+ throw failure();
126
+ return operation(`/proc/self/fd/${descriptor}`);
127
+ }
128
+ catch {
129
+ throw failure();
130
+ }
131
+ finally {
132
+ if (descriptor !== undefined) {
133
+ try {
134
+ fs_1.default.closeSync(descriptor);
135
+ }
136
+ catch { /* best effort descriptor cleanup */ }
137
+ }
138
+ }
139
+ }
140
+ /**
141
+ * Bind a directory without following an observable symlink. Linux uses a
142
+ * descriptor path so ancestor renames cannot redirect work. Other platforms
143
+ * validate every component and its identity before and after the operation;
144
+ * Node exposes no portable openat equivalent, so any observable change fails
145
+ * closed rather than being accepted as a pathname race.
146
+ */
147
+ function withNoFollowDirectory(directory, failure, operation) {
148
+ const noFollow = fs_1.default.constants.O_NOFOLLOW;
149
+ const directoryFlag = fs_1.default.constants.O_DIRECTORY;
150
+ if (!path_1.default.isAbsolute(directory))
151
+ throw failure();
152
+ if (typeof noFollow !== 'number' || typeof directoryFlag !== 'number' || !procfsDescriptorBridgeAvailable()) {
153
+ return withPortableValidatedDirectory(directory, failure, operation);
154
+ }
155
+ let descriptor;
156
+ try {
157
+ const parsed = path_1.default.parse(directory);
158
+ descriptor = fs_1.default.openSync(parsed.root, fs_1.default.constants.O_RDONLY | noFollow | directoryFlag);
159
+ const relative = path_1.default.relative(parsed.root, path_1.default.resolve(directory));
160
+ for (const component of relative.split(path_1.default.sep).filter(Boolean)) {
161
+ const next = fs_1.default.openSync(`/proc/self/fd/${descriptor}/${component}`, fs_1.default.constants.O_RDONLY | noFollow | directoryFlag);
162
+ fs_1.default.closeSync(descriptor);
163
+ descriptor = next;
164
+ }
165
+ const finalDescriptor = descriptor;
166
+ descriptor = undefined;
167
+ return withOpenedNoFollowDirectory(() => finalDescriptor, failure, operation);
168
+ }
169
+ catch {
170
+ throw failure();
171
+ }
172
+ finally {
173
+ if (descriptor !== undefined) {
174
+ try {
175
+ fs_1.default.closeSync(descriptor);
176
+ }
177
+ catch { /* already closed during a failed handoff */ }
178
+ }
179
+ }
180
+ }
181
+ /**
182
+ * Run a filesystem mutation only through a descriptor-bound directory path.
183
+ * Identity snapshots are sufficient for bounded reads, but cannot make a
184
+ * pathname write safe: a parent can be replaced after the snapshot and before
185
+ * `mkdtemp`, staging, or publication. Platforms without the Linux descriptor
186
+ * bridge therefore reject before the mutation callback begins.
187
+ */
188
+ function withDescriptorBoundWriteDirectory(directory, failure, operation) {
189
+ const noFollow = fs_1.default.constants.O_NOFOLLOW;
190
+ const directoryFlag = fs_1.default.constants.O_DIRECTORY;
191
+ if (typeof noFollow !== 'number' || typeof directoryFlag !== 'number' || !procfsDescriptorBridgeAvailable())
192
+ throw failure();
193
+ return withNoFollowDirectory(directory, failure, operation);
194
+ }
195
+ /** Open one direct child from a previously bound directory without path re-resolution. */
196
+ function withNoFollowChildDirectory(parentDirectory, component, failure, operation) {
197
+ const noFollow = fs_1.default.constants.O_NOFOLLOW;
198
+ const directoryFlag = fs_1.default.constants.O_DIRECTORY;
199
+ if (!component || component === '.' || component === '..' || /[\\/]/.test(component))
200
+ throw failure();
201
+ if (typeof noFollow !== 'number' || typeof directoryFlag !== 'number' || !procfsDescriptorBridgeAvailable()) {
202
+ return withPortableValidatedDirectory(path_1.default.join(parentDirectory, component), failure, operation);
203
+ }
204
+ return withOpenedNoFollowDirectory(() => fs_1.default.openSync(path_1.default.join(parentDirectory, component), fs_1.default.constants.O_RDONLY | noFollow | directoryFlag), failure, operation);
205
+ }
206
+ /** Read the exact regular file that was previously inspected. */
207
+ function readInspectedBoundedFile(file, inspected, maxBytes, failure) {
208
+ // The JS descriptor bridge passes /proc/self/fd paths internally on Linux.
209
+ // They are already descriptor-bound; the native loader correctly rejects the
210
+ // procfs symlink representation, so retain this narrow internal path only.
211
+ const descriptorBoundInternalPath = process.platform === 'linux' && file.startsWith('/proc/self/fd/');
212
+ // The native boundary is mandatory in every shipped runtime. The
213
+ // filesystem race corpus opts into the portable path through a test-only
214
+ // seam; environment variables can never weaken production reads.
215
+ if (!secureFsForTest && !descriptorBoundInternalPath && !portableReadForTests) {
216
+ return readInspectedBoundedFileWithIdentity(file, inspected, maxBytes, failure).content;
217
+ }
218
+ // Test adapters observe the same boundary, then deliberately exercise the
219
+ // legacy race fixtures below. They are never selected by production code.
220
+ if (secureFsForTest)
221
+ secureFsForTest.readRegularFile(file, maxBytes);
222
+ const noFollow = fs_1.default.constants.O_NOFOLLOW;
223
+ const directorySnapshots = typeof noFollow === 'number' && procfsDescriptorBridgeAvailable()
224
+ ? undefined
225
+ : snapshotDirectoryChain(path_1.default.dirname(file), () => failure('identity'));
226
+ let descriptor;
227
+ try {
228
+ descriptor = fs_1.default.openSync(file, fs_1.default.constants.O_RDONLY | (typeof noFollow === 'number' ? noFollow : 0));
229
+ }
230
+ catch {
231
+ throw failure('open');
232
+ }
233
+ try {
234
+ let opened;
235
+ try {
236
+ opened = fs_1.default.fstatSync(descriptor, { bigint: true });
237
+ }
238
+ catch {
239
+ throw failure('identity');
240
+ }
241
+ if (!opened.isFile())
242
+ throw failure('regular');
243
+ if (!sameIdentity(opened, inspected))
244
+ throw failure('identity');
245
+ if (opened.size !== inspected.size)
246
+ throw failure('size');
247
+ const limit = BigInt(maxBytes);
248
+ if (opened.size > limit)
249
+ throw failure('limit');
250
+ const buffer = Buffer.allocUnsafe(maxBytes + 1);
251
+ const count = fs_1.default.readSync(descriptor, buffer, 0, buffer.length, null);
252
+ if (!Number.isSafeInteger(count) || count < 0 || count > maxBytes)
253
+ throw failure('limit');
254
+ if (BigInt(count) !== opened.size)
255
+ throw failure('size');
256
+ const after = fs_1.default.fstatSync(descriptor, { bigint: true });
257
+ if (!after.isFile() || !sameIdentity(after, opened))
258
+ throw failure('identity');
259
+ if (after.size !== opened.size)
260
+ throw failure('size');
261
+ if (directorySnapshots)
262
+ assertDirectoryChainUnchanged(directorySnapshots, () => failure('identity'));
263
+ return buffer.subarray(0, count);
264
+ }
265
+ finally {
266
+ fs_1.default.closeSync(descriptor);
267
+ }
268
+ }
269
+ /**
270
+ * Preserve the opaque identity minted by the same native handle that supplied
271
+ * the bytes. Replacement callers must pass this observation back unchanged.
272
+ */
273
+ function readInspectedBoundedFileWithIdentity(file, inspected, maxBytes, failure) {
274
+ if (process.platform === 'linux' && file.startsWith('/proc/self/fd/'))
275
+ throw failure('identity');
276
+ let observed;
277
+ try {
278
+ observed = activeSecureFs().readRegularFile(file, maxBytes);
279
+ }
280
+ catch {
281
+ throw failure('open');
282
+ }
283
+ if (BigInt(observed.bytes.length) !== inspected.size)
284
+ throw failure('size');
285
+ return Object.freeze({ content: observed.bytes, identity: observed.identity });
286
+ }
@@ -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.resolveSensorSource = resolveSensorSource;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const contract_1 = require("./contract");
9
+ const pack_source_1 = require("./pack-source");
10
+ const MAX_CANDIDATE_IDENTITIES = 32;
11
+ const MAX_DIAGNOSTIC_IDENTITY_LENGTH = 128;
12
+ function diagnosticRegistryName(name) {
13
+ return name.length <= MAX_DIAGNOSTIC_IDENTITY_LENGTH
14
+ ? name
15
+ : `${name.slice(0, MAX_DIAGNOSTIC_IDENTITY_LENGTH - 3)}...`;
16
+ }
17
+ function unavailable(reason) {
18
+ return { kind: 'source-unavailable', reason, remedy: 'install-registry-or-run-awm-update' };
19
+ }
20
+ function requireRegistries(deps) {
21
+ if (!deps || !Array.isArray(deps.registries))
22
+ throw new Error('resolveSensorSource requires a registry inventory');
23
+ for (const registry of deps.registries) {
24
+ if (!registry || typeof registry.name !== 'string' || !/^[a-z][a-z0-9-]*$/.test(registry.name)
25
+ || typeof registry.remote !== 'string' || typeof registry.contentRoot !== 'string' || registry.contentRoot.trim() === '') {
26
+ throw new Error('resolveSensorSource received an invalid registry inventory entry');
27
+ }
28
+ }
29
+ return deps.registries;
30
+ }
31
+ function compatible(source, manifest) {
32
+ let parsed;
33
+ try {
34
+ parsed = (0, contract_1.parseSensorPack)(JSON.parse(source.content), source.path);
35
+ }
36
+ catch {
37
+ throw new Error(`registry "${diagnosticRegistryName(source.registry.name)}" has an invalid sensor pack`);
38
+ }
39
+ if (parsed.kind !== 'v2' || parsed.pack.name !== manifest.pack)
40
+ return false;
41
+ return Object.entries(manifest.sensors).every(([name, sensor]) => parsed.pack.sensors[name]?.variants.some(variant => variant.id === sensor.variantId) === true);
42
+ }
43
+ /** Resolves only local registry inventory; it never fetches, probes tools, or writes. */
44
+ function resolveSensorSource(manifest, deps) {
45
+ const registries = requireRegistries(deps);
46
+ if (manifest.kind === 'v3') {
47
+ const v3 = manifest.pack;
48
+ if (!v3 || v3.mode !== 'project-sensors' || typeof v3.pack !== 'string'
49
+ || !v3.source || typeof v3.source.registry !== 'string') {
50
+ throw new Error('sensor source resolution requires a valid project-sensors manifest');
51
+ }
52
+ if (!/^[a-z][a-z0-9-]*$/.test(v3.source.registry)) {
53
+ throw new Error('sensor source registry must be a stable lowercase id');
54
+ }
55
+ const matches = registries.filter(entry => entry?.name === v3.source.registry);
56
+ if (matches.length !== 1)
57
+ return unavailable('registry-not-installed');
58
+ const source = (0, pack_source_1.listPackSources)(v3.pack, { registries: matches })[0];
59
+ return source ? { kind: 'logical', source } : unavailable('no-compatible-registry');
60
+ }
61
+ if (manifest.kind !== 'v2')
62
+ throw new Error('sensor source resolution requires a v2 or v3 project-sensors manifest');
63
+ if (manifest.pack.registryRoot !== undefined && fs_1.default.existsSync(manifest.pack.registryRoot)) {
64
+ const source = (0, pack_source_1.listPackSources)(manifest.pack.pack, { registries: [{ name: 'manifest-provenance', remote: 'local', contentRoot: manifest.pack.registryRoot }] })[0];
65
+ if (!source)
66
+ throw new Error('existing v2 registry provenance does not contain its selected sensor pack');
67
+ return { kind: 'legacy-bound', source };
68
+ }
69
+ const candidates = (0, pack_source_1.listPackSources)(manifest.pack.pack, { registries }).filter(source => compatible(source, manifest.pack));
70
+ if (candidates.length === 0)
71
+ return unavailable('no-compatible-registry');
72
+ if (candidates.length === 1)
73
+ return { kind: 'legacy-rebound', source: candidates[0] };
74
+ return { kind: 'source-ambiguous', candidates: candidates.map(candidate => diagnosticRegistryName(candidate.registry.name)).sort().slice(0, MAX_CANDIDATE_IDENTITIES), reason: 'multiple-compatible-registries', remedy: 'configure-one-logical-registry' };
75
+ }
@@ -39,8 +39,8 @@ async function runCoverage(cwd, dependencies = {}, options = {}) {
39
39
  empirical: empirical(deps, input.projectRoot, {}, min, 'unavailable') };
40
40
  }
41
41
  let live;
42
- if (input.manifest.kind === 'v2') {
43
- live = (await deps.resolveLive(input.projectRoot, input.pack, input.manifest.pack.registryRoot)).sensors;
42
+ if (input.manifest.kind !== 'legacy') {
43
+ live = (await deps.resolveLive(input.projectRoot, input.pack, input.registryRoot)).sensors;
44
44
  }
45
45
  else {
46
46
  live = Object.fromEntries(Object.keys(input.manifest.pack.sensors).map((name) => [name, (0, manifest_1.legacyCompatibility)('legacy manifest without schemaVersion')]));
@@ -11,6 +11,9 @@ const contract_1 = require("./contract");
11
11
  const contract_2 = require("../compatibility/contract");
12
12
  const manifest_1 = require("../compatibility/manifest");
13
13
  const pack_source_1 = require("../compatibility/pack-source");
14
+ const source_1 = require("../compatibility/source");
15
+ const safe_file_1 = require("../compatibility/safe-file");
16
+ const registries_1 = require("../../../core/registries");
14
17
  function readFailure(file, error) {
15
18
  return new Error(`Cannot read ${file}: ${error instanceof Error ? error.message : String(error)}`);
16
19
  }
@@ -20,40 +23,33 @@ function readBoundedJson(file) {
20
23
  throw new Error('readBoundedJson: file must be a non-empty string');
21
24
  let listed;
22
25
  try {
23
- listed = fs_1.default.lstatSync(file);
26
+ listed = fs_1.default.lstatSync(file, { bigint: true });
24
27
  }
25
28
  catch (error) {
26
29
  throw readFailure(file, error);
27
30
  }
28
31
  if (!listed.isFile() || listed.isSymbolicLink())
29
32
  throw new Error(`Cannot read ${file}: expected a regular file`);
30
- if (listed.size > contract_1.MAX_COVERAGE_FILE_BYTES)
33
+ if (listed.size > BigInt(contract_1.MAX_COVERAGE_FILE_BYTES))
31
34
  throw new Error(`Cannot read ${file}: exceeds 1 MiB limit`);
32
- const noFollow = fs_1.default.constants.O_NOFOLLOW;
33
- if (typeof noFollow !== 'number')
34
- throw new Error(`Cannot read ${file}: platform cannot guarantee no symlink dereference`);
35
- let descriptor;
35
+ const failure = (reason) => {
36
+ if (reason === 'open')
37
+ return new Error('cannot be safely opened');
38
+ if (reason === 'regular')
39
+ return new Error('expected a regular file');
40
+ if (reason === 'identity')
41
+ return new Error('file changed identity during safe open');
42
+ if (reason === 'size')
43
+ return new Error('file changed size during safe open');
44
+ return new Error('exceeds 1 MiB limit');
45
+ };
36
46
  let content;
37
47
  try {
38
- descriptor = fs_1.default.openSync(file, fs_1.default.constants.O_RDONLY | noFollow);
39
- const opened = fs_1.default.fstatSync(descriptor);
40
- if (!opened.isFile() || opened.size > contract_1.MAX_COVERAGE_FILE_BYTES) {
41
- throw new Error('expected a regular file within the 1 MiB limit');
42
- }
43
- const buffer = Buffer.allocUnsafe(contract_1.MAX_COVERAGE_FILE_BYTES + 1);
44
- const count = fs_1.default.readSync(descriptor, buffer, 0, buffer.length, null);
45
- if (!Number.isSafeInteger(count) || count < 0 || count > contract_1.MAX_COVERAGE_FILE_BYTES) {
46
- throw new Error('exceeds 1 MiB limit');
47
- }
48
- content = buffer.subarray(0, count).toString('utf8');
48
+ content = (0, safe_file_1.readInspectedBoundedFile)(file, listed, contract_1.MAX_COVERAGE_FILE_BYTES, failure).toString('utf8');
49
49
  }
50
50
  catch (error) {
51
51
  throw readFailure(file, error);
52
52
  }
53
- finally {
54
- if (descriptor !== undefined)
55
- fs_1.default.closeSync(descriptor);
56
- }
57
53
  try {
58
54
  return JSON.parse(content);
59
55
  }
@@ -88,9 +84,19 @@ function resolveCoverageInputs(cwd) {
88
84
  return { kind: 'not_configured' };
89
85
  const manifestPath = path_1.default.join(projectRoot, '.awm', 'sensors.json');
90
86
  const manifest = (0, manifest_1.parseSensorManifest)(readBoundedJson(manifestPath), manifestPath);
91
- const source = manifest.kind === 'v2' && manifest.pack.registryRoot !== undefined
92
- ? (0, pack_source_1.resolvePackSource)(manifest.pack.pack, { registries: [{ name: 'manifest-provenance', remote: 'local', contentRoot: manifest.pack.registryRoot }] })
93
- : (0, pack_source_1.resolvePackSource)(manifest.pack.pack);
87
+ if (manifest.kind === 'v3' && manifest.pack.mode !== 'project-sensors')
88
+ return { kind: 'not_configured' };
89
+ const coverageManifest = manifest;
90
+ const source = coverageManifest.kind === 'v3'
91
+ ? (() => {
92
+ const resolution = (0, source_1.resolveSensorSource)(coverageManifest, { registries: (0, registries_1.listRegistries)() });
93
+ if (!('source' in resolution))
94
+ throw new Error(`${resolution.kind}: ${resolution.remedy}`);
95
+ return resolution.source;
96
+ })()
97
+ : coverageManifest.kind === 'v2' && coverageManifest.pack.registryRoot !== undefined
98
+ ? (0, pack_source_1.resolvePackSource)(coverageManifest.pack.pack, { registries: [{ name: 'manifest-provenance', remote: 'local', contentRoot: coverageManifest.pack.registryRoot }] })
99
+ : (0, pack_source_1.resolvePackSource)(coverageManifest.pack.pack);
94
100
  let sourceJson;
95
101
  try {
96
102
  sourceJson = JSON.parse(source.content);
@@ -99,14 +105,14 @@ function resolveCoverageInputs(cwd) {
99
105
  throw new Error(`Invalid JSON at ${source.path}: ${error instanceof Error ? error.message : String(error)}`);
100
106
  }
101
107
  const parsedPack = (0, contract_2.parseSensorPack)(sourceJson, source.path);
102
- if (parsedPack.pack.name !== manifest.pack.pack) {
103
- throw new Error(`Invalid pack at ${source.path}: name must equal '${manifest.pack.pack}'`);
108
+ if (parsedPack.pack.name !== coverageManifest.pack.pack) {
109
+ throw new Error(`Invalid pack at ${source.path}: name must equal '${coverageManifest.pack.pack}'`);
104
110
  }
105
111
  const { coverage } = parsedPack.pack;
106
112
  if (coverage === undefined)
107
- return { kind: 'no_reference', projectRoot, pack: manifest.pack.pack, registry: source.registry.name, manifest };
113
+ return { kind: 'no_reference', projectRoot, pack: coverageManifest.pack.pack, registry: source.registry.name, registryRoot: source.registry.contentRoot, manifest: coverageManifest };
108
114
  return {
109
- kind: 'ready', projectRoot, pack: manifest.pack.pack, registry: source.registry.name,
110
- manifest, contract: (0, contract_1.parseCoverageContract)(coverage, source.path),
115
+ kind: 'ready', projectRoot, pack: coverageManifest.pack.pack, registry: source.registry.name,
116
+ registryRoot: source.registry.contentRoot, manifest: coverageManifest, contract: (0, contract_1.parseCoverageContract)(coverage, source.path),
111
117
  };
112
118
  }
@@ -0,0 +1,31 @@
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.detectStack = detectStack;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const DETECTORS = [
10
+ { pack: 'js-ts', files: ['package.json'] },
11
+ { pack: 'python', files: ['pyproject.toml', 'setup.py', 'setup.cfg', 'requirements.txt', 'Pipfile'] },
12
+ ];
13
+ /** Read-only stack classification shared by run, bootstrap, and legacy init. */
14
+ function detectStack(cwd) {
15
+ for (const { pack, files } of DETECTORS) {
16
+ const indicators = files.filter(file => fs_1.default.existsSync(path_1.default.join(cwd, file)));
17
+ if (indicators.length > 0)
18
+ return { pack, indicators };
19
+ }
20
+ for (const directory of ['.', 'scripts']) {
21
+ const absolute = path_1.default.join(cwd, directory);
22
+ if (!fs_1.default.existsSync(absolute) || !fs_1.default.statSync(absolute).isDirectory())
23
+ continue;
24
+ const indicators = fs_1.default.readdirSync(absolute, { withFileTypes: true })
25
+ .filter(entry => entry.isFile() && entry.name.endsWith('.sh'))
26
+ .map(entry => directory === '.' ? entry.name : path_1.default.join(directory, entry.name));
27
+ if (indicators.length > 0)
28
+ return { pack: 'shell', indicators };
29
+ }
30
+ return { pack: 'generic', indicators: [] };
31
+ }