agentic-workflow-manager 9.5.0 → 9.6.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.
- package/dist/src/commands/context/index.js +3 -1
- package/dist/src/commands/preflight/checks.js +2 -2
- package/dist/src/commands/sensors/bootstrap.js +171 -0
- package/dist/src/commands/sensors/compatibility/contract.js +18 -15
- package/dist/src/commands/sensors/compatibility/manifest.js +29 -1
- package/dist/src/commands/sensors/compatibility/materialize.js +256 -54
- package/dist/src/commands/sensors/compatibility/safe-file.js +240 -5
- package/dist/src/commands/sensors/coverage/index.js +2 -2
- package/dist/src/commands/sensors/coverage/resolve.js +20 -10
- package/dist/src/commands/sensors/detection.js +31 -0
- package/dist/src/commands/sensors/exec.js +116 -11
- package/dist/src/commands/sensors/index.js +51 -10
- package/dist/src/commands/sensors/init.js +17 -170
- package/dist/src/commands/sensors/migrate.js +209 -0
- package/dist/src/commands/sensors/run.js +2 -2
- package/dist/src/core/discovery.js +79 -4
- package/dist/src/core/init/steps.js +24 -11
- package/dist/src/core/orchestrators.js +34 -10
- package/dist/src/core/secure-fs/native-bridge.js +209 -0
- package/dist/src/core/text.js +7 -0
- package/dist/tests/commands/context.test.js +9 -1
- package/dist/tests/commands/hooks/install.test.js +4 -0
- package/dist/tests/commands/hooks/resync.test.js +2 -0
- package/dist/tests/commands/sensors/bootstrap-observational.test.js +24 -0
- package/dist/tests/commands/sensors/bootstrap.test.js +175 -0
- package/dist/tests/commands/sensors/compatibility/contract.test.js +38 -0
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +67 -0
- package/dist/tests/commands/sensors/compatibility/materialize.test.js +410 -0
- package/dist/tests/commands/sensors/compatibility/pack-source.test.js +34 -1
- package/dist/tests/commands/sensors/compatibility/safe-file.test.js +52 -0
- package/dist/tests/commands/sensors/coverage/index.test.js +20 -0
- package/dist/tests/commands/sensors/coverage/resolve.test.js +10 -0
- package/dist/tests/commands/sensors/exec.test.js +86 -0
- package/dist/tests/commands/sensors/index.test.js +54 -0
- package/dist/tests/commands/sensors/init-pack-unavailable.test.js +21 -84
- package/dist/tests/commands/sensors/init.test.js +56 -490
- package/dist/tests/commands/sensors/migrate.test.js +513 -0
- package/dist/tests/core/context/orchestrator.test.js +2 -0
- package/dist/tests/core/discovery-multiroot.test.js +5 -0
- package/dist/tests/core/discovery-parent-race.test.js +95 -0
- package/dist/tests/core/discovery-safe-read.test.js +115 -0
- package/dist/tests/core/discovery.test.js +28 -1
- package/dist/tests/core/init/orchestrator.test.js +3 -1
- package/dist/tests/core/orchestrators.test.js +171 -14
- package/dist/tests/core/secure-fs/native-bridge.test.js +733 -0
- package/dist/tests/core/text.test.js +10 -0
- package/dist/tests/integration/context-orchestrators-e2e.test.js +57 -7
- package/dist/tests/integration/sensor-bootstrap.e2e.test.js +165 -0
- package/dist/tests/integration/sensor-compatibility.e2e.test.js +10 -2
- package/dist/tests/integration/sensor-structured-run.e2e.test.js +3 -1
- package/dist/tests/structural/native-release-artifacts.test.js +22 -0
- package/dist/tests/structural/release-flags-are-reachable-from-ci.test.js +10 -0
- package/dist/tests/structural/release-registry-provenance.test.js +4 -0
- package/dist/tests/structural/sensor-portability-contract.test.js +44 -0
- package/package.json +6 -2
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/linux-arm64/secure_fs.node +0 -0
- package/prebuilds/linux-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
|
@@ -0,0 +1,209 @@
|
|
|
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.planV2Migration = planV2Migration;
|
|
7
|
+
exports.replaceV2ManifestWithV3 = replaceV2ManifestWithV3;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const contract_1 = require("./compatibility/contract");
|
|
11
|
+
const manifest_1 = require("./compatibility/manifest");
|
|
12
|
+
const safe_file_1 = require("./compatibility/safe-file");
|
|
13
|
+
function isRecord(value) {
|
|
14
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
function sameSensorField(v2, v3, pick) {
|
|
17
|
+
const names = Object.keys(v2.sensors);
|
|
18
|
+
return names.length === Object.keys(v3.sensors).length
|
|
19
|
+
&& names.every(name => name in v3.sensors && JSON.stringify(pick(v2.sensors[name])) === JSON.stringify(pick(v3.sensors[name])));
|
|
20
|
+
}
|
|
21
|
+
function compareV2Semantics(v2, v3, registry) {
|
|
22
|
+
return Object.freeze({
|
|
23
|
+
pack: v2.pack === v3.pack && v2.packSelection === v3.packSelection,
|
|
24
|
+
enabledDisabled: sameSensorField(v2, v3, sensor => sensor.enabled),
|
|
25
|
+
structuredCommands: sameSensorField(v2, v3, sensor => sensor.command),
|
|
26
|
+
assets: sameSensorField(v2, v3, sensor => sensor.assets),
|
|
27
|
+
timeouts: sameSensorField(v2, v3, sensor => sensor.timeout),
|
|
28
|
+
fast: sameSensorField(v2, v3, sensor => sensor.fast),
|
|
29
|
+
variants: sameSensorField(v2, v3, sensor => sensor.variantId),
|
|
30
|
+
policies: sameSensorField(v2, v3, sensor => sensor.policyRef),
|
|
31
|
+
sensorIdentityAndOrder: JSON.stringify(Object.keys(v2.sensors)) === JSON.stringify(Object.keys(v3.sensors)),
|
|
32
|
+
concurrency: v2.concurrency === v3.concurrency,
|
|
33
|
+
compatibilityEvidence: sameSensorField(v2, v3, sensor => sensor.initializedCompatibility),
|
|
34
|
+
packageRoot: v2.packageRoot === v3.packageRoot,
|
|
35
|
+
logicalSourceBinding: v3.source.registry === registry,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function allEquivalent(report) {
|
|
39
|
+
return Object.values(report).every(value => value === true);
|
|
40
|
+
}
|
|
41
|
+
function exactLogicalSource(input, pack) {
|
|
42
|
+
if (!isRecord(input))
|
|
43
|
+
throw new Error('v2 migration source is required');
|
|
44
|
+
if (input.kind === 'source-unavailable')
|
|
45
|
+
throw new Error('v2 migration source is unavailable');
|
|
46
|
+
if (input.kind === 'source-ambiguous')
|
|
47
|
+
throw new Error('v2 migration source is ambiguous');
|
|
48
|
+
if ((input.kind !== 'logical' && input.kind !== 'legacy-rebound') || !isRecord(input.source))
|
|
49
|
+
throw new Error('v2 migration source must be a unique logical resolution');
|
|
50
|
+
const source = input.source;
|
|
51
|
+
if (Object.keys(source).some(key => !['path', 'content', 'registry'].includes(key))
|
|
52
|
+
|| typeof source.path !== 'string' || typeof source.content !== 'string' || !isRecord(source.registry)) {
|
|
53
|
+
throw new Error('v2 migration source must contain an exact resolved pack');
|
|
54
|
+
}
|
|
55
|
+
const registry = source.registry;
|
|
56
|
+
if (Object.keys(registry).some(key => !['name', 'remote', 'contentRoot'].includes(key))
|
|
57
|
+
|| typeof registry.name !== 'string' || !/^[a-z][a-z0-9-]*$/.test(registry.name)
|
|
58
|
+
|| typeof registry.remote !== 'string' || registry.remote.trim() === '' || /[\0\r\n]/.test(registry.remote)
|
|
59
|
+
|| typeof registry.contentRoot !== 'string' || !path_1.default.isAbsolute(registry.contentRoot) || path_1.default.normalize(registry.contentRoot) !== registry.contentRoot) {
|
|
60
|
+
throw new Error('v2 migration source registry must be a stable logical registry');
|
|
61
|
+
}
|
|
62
|
+
// `resolveSensorSource` already selected and read this exact source. The
|
|
63
|
+
// migration planner consumes that immutable snapshot; re-canonicalizing its
|
|
64
|
+
// host directory would make a no-write plan depend on later filesystem state.
|
|
65
|
+
const expectedPath = path_1.default.join(registry.contentRoot, 'sensor-packs', pack, 'pack.json');
|
|
66
|
+
if (source.path !== expectedPath)
|
|
67
|
+
throw new Error('v2 migration source must contain an exact resolved pack');
|
|
68
|
+
let parsedSource;
|
|
69
|
+
try {
|
|
70
|
+
parsedSource = (0, contract_1.parseSensorPack)(JSON.parse(source.content), 'v2 migration resolved source');
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
throw new Error('v2 migration source exact v2 pack is invalid');
|
|
74
|
+
}
|
|
75
|
+
if (parsedSource.kind !== 'v2' || parsedSource.pack.name !== pack)
|
|
76
|
+
throw new Error('v2 migration source must contain the exact v2 pack');
|
|
77
|
+
return source;
|
|
78
|
+
}
|
|
79
|
+
function assertSourceCompatibleWithManifest(source, manifest) {
|
|
80
|
+
let parsedSource;
|
|
81
|
+
try {
|
|
82
|
+
parsedSource = (0, contract_1.parseSensorPack)(JSON.parse(source.content), 'v2 migration resolved source');
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
throw new Error('v2 migration source exact v2 pack is invalid');
|
|
86
|
+
}
|
|
87
|
+
if (parsedSource.kind !== 'v2' || parsedSource.pack.name !== manifest.pack) {
|
|
88
|
+
throw new Error('v2 migration source must contain the exact v2 pack');
|
|
89
|
+
}
|
|
90
|
+
for (const [name, sensor] of Object.entries(manifest.sensors)) {
|
|
91
|
+
const selected = parsedSource.pack.sensors[name];
|
|
92
|
+
if (!selected || !selected.variants.some(variant => variant.id === sensor.variantId)) {
|
|
93
|
+
throw new Error('v2 migration source is not compatible with selected manifest sensors');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function hasPhysicalSensorPath(value, registryRoots) {
|
|
98
|
+
if (typeof value === 'string') {
|
|
99
|
+
const normalized = path_1.default.posix.normalize(value.replace(/\\/g, '/')).toLowerCase();
|
|
100
|
+
const withoutWebUrls = value.replace(/https?:\/\/[^\s"']+/gi, '');
|
|
101
|
+
const roots = registryRoots
|
|
102
|
+
.filter((root) => typeof root === 'string' && root.length > 0)
|
|
103
|
+
.map(root => path_1.default.posix.normalize(root.replace(/\\/g, '/')).toLowerCase());
|
|
104
|
+
const hasAbsolutePathAtTokenBoundary = /(?:^|[^A-Za-z0-9_.~\/\\-])["']?(?:\/|\\|[A-Za-z]:|\/\/)/.test(withoutWebUrls);
|
|
105
|
+
return roots.some(root => normalized.includes(root))
|
|
106
|
+
|| hasAbsolutePathAtTokenBoundary;
|
|
107
|
+
}
|
|
108
|
+
return Array.isArray(value) ? value.some(item => hasPhysicalSensorPath(item, registryRoots))
|
|
109
|
+
: isRecord(value) && Object.values(value).some(item => hasPhysicalSensorPath(item, registryRoots));
|
|
110
|
+
}
|
|
111
|
+
function inspectManifest(manifestPath, failure) {
|
|
112
|
+
let stat;
|
|
113
|
+
try {
|
|
114
|
+
stat = fs_1.default.lstatSync(manifestPath, { bigint: true });
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
throw failure('open');
|
|
118
|
+
}
|
|
119
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
120
|
+
throw failure('regular');
|
|
121
|
+
return { stat, ...(0, safe_file_1.readInspectedBoundedFileWithIdentity)(manifestPath, stat, 1024 * 1024, failure) };
|
|
122
|
+
}
|
|
123
|
+
function assertManifestUnchanged(manifestPath, original) {
|
|
124
|
+
const changed = () => new Error('v2 migration original manifest changed before commit');
|
|
125
|
+
const current = inspectManifest(manifestPath, () => changed());
|
|
126
|
+
if (current.stat.dev !== original.stat.dev || current.stat.ino !== original.stat.ino
|
|
127
|
+
|| current.stat.size !== original.stat.size || !current.content.equals(original.content)) {
|
|
128
|
+
throw changed();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** Build, but never persist, a portable v3 replacement for a validated v2 manifest. */
|
|
132
|
+
function planV2Migration(input) {
|
|
133
|
+
if (!input || typeof input !== 'object')
|
|
134
|
+
throw new Error('v2 migration input is required');
|
|
135
|
+
const parsed = (0, manifest_1.parseSensorManifest)(input.manifest, 'v2 migration manifest');
|
|
136
|
+
if (parsed.kind !== 'v2')
|
|
137
|
+
throw new Error('v2 migration requires a v2 project-sensors manifest');
|
|
138
|
+
const source = exactLogicalSource(input.source, parsed.pack.pack);
|
|
139
|
+
assertSourceCompatibleWithManifest(source, parsed.pack);
|
|
140
|
+
if (hasPhysicalSensorPath(parsed.pack.sensors, [parsed.pack.registryRoot, source.registry.contentRoot])
|
|
141
|
+
|| hasPhysicalSensorPath(parsed.pack.packageRoot, []))
|
|
142
|
+
throw new Error('v2 migration sensor semantics contain a physical path');
|
|
143
|
+
const candidate = {
|
|
144
|
+
schemaVersion: 3,
|
|
145
|
+
mode: 'project-sensors',
|
|
146
|
+
pack: parsed.pack.pack,
|
|
147
|
+
...(parsed.pack.packSelection === 'explicit' ? { packSelection: 'explicit' } : {}),
|
|
148
|
+
source: { registry: source.registry.name },
|
|
149
|
+
...(parsed.pack.packageRoot ? { packageRoot: parsed.pack.packageRoot } : {}),
|
|
150
|
+
sensors: parsed.pack.sensors,
|
|
151
|
+
...(parsed.pack.concurrency ? { concurrency: parsed.pack.concurrency } : {}),
|
|
152
|
+
};
|
|
153
|
+
const validated = (0, manifest_1.parseSensorManifest)(candidate, 'v2 migration candidate');
|
|
154
|
+
if (validated.kind !== 'v3' || validated.pack.mode !== 'project-sensors') {
|
|
155
|
+
throw new Error('v2 migration candidate semantic mismatch');
|
|
156
|
+
}
|
|
157
|
+
const equivalence = compareV2Semantics(parsed.pack, validated.pack, source.registry.name);
|
|
158
|
+
if (!allEquivalent(equivalence))
|
|
159
|
+
throw new Error('v2 migration candidate semantic mismatch');
|
|
160
|
+
return { candidate: validated.pack, equivalent: true, equivalence };
|
|
161
|
+
}
|
|
162
|
+
/** Validate the old and new durable contracts, then atomically replace only the manifest. */
|
|
163
|
+
function replaceV2ManifestWithV3(manifestPath, candidate, source) {
|
|
164
|
+
if (typeof manifestPath !== 'string' || !path_1.default.isAbsolute(manifestPath) || path_1.default.normalize(manifestPath) !== manifestPath
|
|
165
|
+
|| manifestPath.includes('\0') || path_1.default.basename(manifestPath) !== 'sensors.json'
|
|
166
|
+
|| path_1.default.basename(path_1.default.dirname(manifestPath)) !== '.awm') {
|
|
167
|
+
throw new Error('v2 migration manifest path must be the canonical project manifest <project>/.awm/sensors.json');
|
|
168
|
+
}
|
|
169
|
+
const manifestDirectory = path_1.default.dirname(manifestPath);
|
|
170
|
+
const projectRoot = path_1.default.dirname(manifestDirectory);
|
|
171
|
+
const destination = '.awm/sensors.json';
|
|
172
|
+
(0, safe_file_1.withProjectLease)(projectRoot, () => {
|
|
173
|
+
const originalFailure = (_reason) => new Error('v2 migration original manifest must be readable JSON');
|
|
174
|
+
let inspected;
|
|
175
|
+
let original;
|
|
176
|
+
try {
|
|
177
|
+
inspected = inspectManifest(manifestPath, originalFailure);
|
|
178
|
+
original = JSON.parse(inspected.content.toString('utf8'));
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
throw new Error('v2 migration original manifest must be readable JSON');
|
|
182
|
+
}
|
|
183
|
+
const before = (0, manifest_1.parseSensorManifest)(original, manifestPath);
|
|
184
|
+
const after = (0, manifest_1.parseSensorManifest)(candidate, 'v2 migration candidate');
|
|
185
|
+
if (before.kind !== 'v2' || after.kind !== 'v3' || after.pack.mode !== 'project-sensors') {
|
|
186
|
+
throw new Error('v2 migration candidate semantic mismatch');
|
|
187
|
+
}
|
|
188
|
+
const logicalSource = exactLogicalSource(source, before.pack.pack);
|
|
189
|
+
assertSourceCompatibleWithManifest(logicalSource, before.pack);
|
|
190
|
+
if (after.pack.source.registry !== logicalSource.registry.name) {
|
|
191
|
+
throw new Error('v2 migration candidate semantic mismatch');
|
|
192
|
+
}
|
|
193
|
+
if (hasPhysicalSensorPath(before.pack.sensors, [before.pack.registryRoot, logicalSource.registry.contentRoot])
|
|
194
|
+
|| hasPhysicalSensorPath(after.pack.sensors, [before.pack.registryRoot, logicalSource.registry.contentRoot])
|
|
195
|
+
|| hasPhysicalSensorPath(before.pack.packageRoot, []) || hasPhysicalSensorPath(after.pack.packageRoot, [])) {
|
|
196
|
+
throw new Error('v2 migration candidate contains a physical path');
|
|
197
|
+
}
|
|
198
|
+
if (!allEquivalent(compareV2Semantics(before.pack, after.pack, logicalSource.registry.name))) {
|
|
199
|
+
throw new Error('v2 migration candidate semantic mismatch');
|
|
200
|
+
}
|
|
201
|
+
// The native implementation reopens every ancestor without following links,
|
|
202
|
+
// verifies these exact bytes at the publication fence, and preserves a
|
|
203
|
+
// competing target instead of replacing it.
|
|
204
|
+
assertManifestUnchanged(manifestPath, inspected);
|
|
205
|
+
(0, safe_file_1.writeProjectFile)(projectRoot, destination, Buffer.from((0, manifest_1.serializeManifestV3)(after.pack), 'utf8'), {
|
|
206
|
+
mode: 'replace', expected: inspected.content, expectedIdentity: inspected.identity, createParents: false,
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
}
|
|
@@ -16,7 +16,7 @@ Object.defineProperty(exports, "applyBaseline", { enumerable: true, get: functio
|
|
|
16
16
|
const changed_1 = require("./changed");
|
|
17
17
|
// Solo `detectStack` (puro, lee el arbol) — NO `initSensors`, que escribe. `run` es un
|
|
18
18
|
// verbo de lectura: no debe tener a mano ninguna funcion capaz de mutar el proyecto.
|
|
19
|
-
const
|
|
19
|
+
const detection_1 = require("./detection");
|
|
20
20
|
const live_1 = require("./compatibility/live");
|
|
21
21
|
const contract_1 = require("./compatibility/contract");
|
|
22
22
|
const source_1 = require("./compatibility/source");
|
|
@@ -67,7 +67,7 @@ function findManifestDir(startCwd) {
|
|
|
67
67
|
* and failed on by preflight's `pack` check.
|
|
68
68
|
*/
|
|
69
69
|
function detectPackDrift(manifestDir, manifest) {
|
|
70
|
-
const detection = (0,
|
|
70
|
+
const detection = (0, detection_1.detectStack)(manifestDir);
|
|
71
71
|
if (manifest.pack !== 'generic' || detection.pack === 'generic')
|
|
72
72
|
return { detection };
|
|
73
73
|
return {
|
|
@@ -34,6 +34,73 @@ function readArtifactDescription(filePath) {
|
|
|
34
34
|
return '';
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
+
function inspectSkillDirectory(parent, expected) {
|
|
38
|
+
const stat = fs_1.default.lstatSync(parent, { bigint: true });
|
|
39
|
+
if (stat.isSymbolicLink())
|
|
40
|
+
throw new Error(`${parent}: must not be a symbolic link`);
|
|
41
|
+
if (!stat.isDirectory())
|
|
42
|
+
throw new Error(`${parent}: must be a directory`);
|
|
43
|
+
if (typeof stat.dev !== 'bigint' || typeof stat.ino !== 'bigint' || stat.dev < 0n || stat.ino <= 0n) {
|
|
44
|
+
throw new Error(`${parent}: directory identity is unobservable`);
|
|
45
|
+
}
|
|
46
|
+
if (expected && (stat.dev !== expected.dev || stat.ino !== expected.ino)) {
|
|
47
|
+
throw new Error(`${parent}: directory identity changed after inspection`);
|
|
48
|
+
}
|
|
49
|
+
return stat;
|
|
50
|
+
}
|
|
51
|
+
/** CTX-CONSTITUTION-052: bind the inspected leaf to its observable parent chain. */
|
|
52
|
+
function readSkillDescription(file) {
|
|
53
|
+
const absolute = path_1.default.resolve(file);
|
|
54
|
+
const parents = [];
|
|
55
|
+
for (let parent = path_1.default.dirname(absolute);; parent = path_1.default.dirname(parent)) {
|
|
56
|
+
parents.unshift(parent);
|
|
57
|
+
if (parent === path_1.default.dirname(parent))
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
const inspectedParents = parents.map(parent => ({ path: parent, stat: inspectSkillDirectory(parent) }));
|
|
61
|
+
const validateParents = () => {
|
|
62
|
+
for (const parent of inspectedParents)
|
|
63
|
+
inspectSkillDirectory(parent.path, parent.stat);
|
|
64
|
+
};
|
|
65
|
+
let inspected;
|
|
66
|
+
try {
|
|
67
|
+
inspected = fs_1.default.lstatSync(file, { bigint: true });
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (error?.code === 'ENOENT')
|
|
71
|
+
return null;
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
// A parent swapped before leaf lstat must not establish a new trusted identity.
|
|
75
|
+
validateParents();
|
|
76
|
+
if (inspected.isSymbolicLink())
|
|
77
|
+
throw new Error('must not be a symbolic link');
|
|
78
|
+
if (!inspected.isFile())
|
|
79
|
+
throw new Error('must be a regular file');
|
|
80
|
+
if (typeof inspected.dev !== 'bigint' || typeof inspected.ino !== 'bigint'
|
|
81
|
+
|| typeof inspected.size !== 'bigint' || inspected.dev < 0n
|
|
82
|
+
|| inspected.ino <= 0n || inspected.size < 0n) {
|
|
83
|
+
throw new Error('file identity or size is unobservable');
|
|
84
|
+
}
|
|
85
|
+
const flags = fs_1.default.constants.O_RDONLY | (fs_1.default.constants.O_NOFOLLOW ?? 0) | (fs_1.default.constants.O_NONBLOCK ?? 0);
|
|
86
|
+
const fd = fs_1.default.openSync(file, flags);
|
|
87
|
+
try {
|
|
88
|
+
const opened = fs_1.default.fstatSync(fd, { bigint: true });
|
|
89
|
+
if (!opened.isFile())
|
|
90
|
+
throw new Error('must be a regular file');
|
|
91
|
+
if (opened.dev !== inspected.dev || opened.ino !== inspected.ino || opened.size !== inspected.size) {
|
|
92
|
+
throw new Error('file identity or size changed after inspection');
|
|
93
|
+
}
|
|
94
|
+
// Reject substitutions observable after open, while retaining descriptor authority.
|
|
95
|
+
validateParents();
|
|
96
|
+
const raw = fs_1.default.readFileSync(fd, 'utf-8');
|
|
97
|
+
const frontmatter = (0, frontmatter_2.matchFrontmatterBlock)(raw);
|
|
98
|
+
return frontmatter === null ? '' : (0, frontmatter_2.readFrontmatterDescription)(frontmatter);
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
fs_1.default.closeSync(fd);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
37
104
|
function collisionError(kind, name, first, second) {
|
|
38
105
|
return new Error(`Artifact name collision: ${kind} "${name}" exists in both ${first} and ${second}. ` +
|
|
39
106
|
`Remove or rename one of them, or declare "${name}" in "overrides" of the later registry's awm-registry.json.`);
|
|
@@ -54,7 +121,7 @@ function mergeEntry(kind, byName, entry, rootOverrides) {
|
|
|
54
121
|
}
|
|
55
122
|
/**
|
|
56
123
|
* Scans skills directories across all provided content roots and returns all valid skills.
|
|
57
|
-
* A valid skill is a directory that contains a SKILL.md file.
|
|
124
|
+
* A valid skill is a directory that contains a regular, non-symlink SKILL.md file.
|
|
58
125
|
* Throws on name collision across roots unless the later root declares the name in its awm-registry.json overrides.
|
|
59
126
|
*/
|
|
60
127
|
function discoverSkills(roots = (0, registries_1.contentRoots)()) {
|
|
@@ -68,12 +135,20 @@ function discoverSkills(roots = (0, registries_1.contentRoots)()) {
|
|
|
68
135
|
if (!entry.isDirectory())
|
|
69
136
|
continue;
|
|
70
137
|
const skillPath = path_1.default.join(dir, entry.name);
|
|
71
|
-
|
|
72
|
-
|
|
138
|
+
const skillFile = path_1.default.join(skillPath, 'SKILL.md');
|
|
139
|
+
let description;
|
|
140
|
+
try {
|
|
141
|
+
description = readSkillDescription(skillFile);
|
|
142
|
+
if (description === null)
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
throw new Error(`${skillFile}: cannot read (${e instanceof Error ? e.message : String(e)})`);
|
|
147
|
+
}
|
|
73
148
|
mergeEntry('skill', byName, {
|
|
74
149
|
name: entry.name,
|
|
75
150
|
path: skillPath,
|
|
76
|
-
description
|
|
151
|
+
description,
|
|
77
152
|
}, overrides);
|
|
78
153
|
}
|
|
79
154
|
}
|
|
@@ -23,7 +23,8 @@ exports.stepContextInjection = stepContextInjection;
|
|
|
23
23
|
const registries_1 = require("../registries");
|
|
24
24
|
const install_1 = require("../../commands/hooks/install");
|
|
25
25
|
const bundle_install_1 = require("../bundle-install");
|
|
26
|
-
const
|
|
26
|
+
const bootstrap_1 = require("../../commands/sensors/bootstrap");
|
|
27
|
+
const detection_1 = require("../../commands/sensors/detection");
|
|
27
28
|
const profile_1 = require("../profile");
|
|
28
29
|
const context_1 = require("../diagnostics/context");
|
|
29
30
|
const detector_1 = require("./detector");
|
|
@@ -66,15 +67,15 @@ exports.defaultActions = {
|
|
|
66
67
|
contentDir: o.contentDir,
|
|
67
68
|
}),
|
|
68
69
|
initSensors: async (o) => {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
};
|
|
70
|
+
const plan = await (0, bootstrap_1.planSensorBootstrap)(o.cwd, { mode: 'project-sensors', registryRoot: o.registryRoot, configure: o.configure });
|
|
71
|
+
if (plan.kind === 'blocked')
|
|
72
|
+
throw new Error(`sensor bootstrap blocked: ${plan.reason}; ${plan.remedy}`);
|
|
73
|
+
if (plan.kind === 'migrate')
|
|
74
|
+
throw new Error('sensor init does not migrate an existing v2 manifest; run awm sensors bootstrap');
|
|
75
|
+
if (plan.kind === 'create')
|
|
76
|
+
(0, bootstrap_1.applySensorBootstrap)(plan);
|
|
77
|
+
const pack = plan.kind === 'create' && plan.manifest.mode === 'project-sensors' ? plan.manifest.pack : (0, detection_1.detectStack)(o.cwd).pack;
|
|
78
|
+
return { detection: { pack }, manifest: { pack } };
|
|
78
79
|
},
|
|
79
80
|
addExtension: (root, name) => { (0, profile_1.addExtension)(root, name); },
|
|
80
81
|
ensureProfile: (root) => { (0, profile_1.ensureProfile)(root); },
|
|
@@ -325,7 +326,19 @@ async function stepSensors(d) {
|
|
|
325
326
|
return ok('project.sensors', 'project', 'skipped', 'no project');
|
|
326
327
|
if (proj.sensors.present)
|
|
327
328
|
return ok('project.sensors', 'project', 'skipped');
|
|
328
|
-
|
|
329
|
+
if (!d.sensorPacksRoot)
|
|
330
|
+
return ok('project.sensors', 'project', 'skipped', 'no configured sensor-pack registry');
|
|
331
|
+
let res;
|
|
332
|
+
try {
|
|
333
|
+
res = await d.actions.initSensors({ cwd: proj.root, registryRoot: d.sensorPacksRoot, configure: true });
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
337
|
+
if (/^(?:source-unavailable|source-unsupported|registry-root-not-configured|sensor-variant-unresolvable):/.test(message)) {
|
|
338
|
+
return ok('project.sensors', 'project', 'skipped', message);
|
|
339
|
+
}
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
329
342
|
// A registry without the detected pack yields a fallback (or empty) manifest. The
|
|
330
343
|
// step still succeeded — a manifest was written — but reporting a bare 'applied'
|
|
331
344
|
// would let `awm init` hand back a quality gate that measures less than the
|
|
@@ -17,6 +17,7 @@ exports.collectAndWarn = collectAndWarn;
|
|
|
17
17
|
const fs_1 = __importDefault(require("fs"));
|
|
18
18
|
const path_1 = __importDefault(require("path"));
|
|
19
19
|
const registries_1 = require("./registries");
|
|
20
|
+
const discovery_1 = require("./discovery");
|
|
20
21
|
const text_1 = require("./text");
|
|
21
22
|
const ALLOWED_FIELDS = ['name', 'appliesWhen', 'terminatesTo'];
|
|
22
23
|
// These fields are semantically short (a short identity, a short trigger condition, a
|
|
@@ -24,6 +25,24 @@ const ALLOWED_FIELDS = ['name', 'appliesWhen', 'terminatesTo'];
|
|
|
24
25
|
// untrusted input whose fields flow straight into the AI-provider context payload, so an
|
|
25
26
|
// unbounded string here would let a crafted registry bloat/DoS that context.
|
|
26
27
|
const MAX_FIELD_LENGTH = 500;
|
|
28
|
+
function discoverDeclaredSkillNames() {
|
|
29
|
+
let names = new Set();
|
|
30
|
+
const diagnostics = [];
|
|
31
|
+
const acceptedRoots = [];
|
|
32
|
+
for (const root of (0, registries_1.contentRoots)()) {
|
|
33
|
+
try {
|
|
34
|
+
// Admit the entire root only after global collision/override resolution succeeds.
|
|
35
|
+
const skills = (0, discovery_1.discoverSkills)([...acceptedRoots, root]);
|
|
36
|
+
names = new Set(skills.map((skill) => skill.name));
|
|
37
|
+
acceptedRoots.push(root);
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
41
|
+
diagnostics.push(`${(0, text_1.sanitizeDiagnosticText)(root)}: orchestrator skill discovery unavailable (${(0, text_1.sanitizeDiagnosticText)(message)})`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { names, diagnostics };
|
|
45
|
+
}
|
|
27
46
|
function readDeclaredOrchestrators(root) {
|
|
28
47
|
const file = path_1.default.join(root, registries_1.REGISTRY_MANIFEST_NAME);
|
|
29
48
|
// Shares the same trust boundary as readRegistryManifest (registries.ts): a manifest
|
|
@@ -35,7 +54,7 @@ function readDeclaredOrchestrators(root) {
|
|
|
35
54
|
exists = (0, registries_1.assertRegularRegistryFile)(file);
|
|
36
55
|
}
|
|
37
56
|
catch (e) {
|
|
38
|
-
return { orchestrators: [], diagnostics: [`${file}: ${e instanceof Error ? e.message : String(e)}`] };
|
|
57
|
+
return { orchestrators: [], diagnostics: [(0, text_1.sanitizeDiagnosticText)(`${file}: ${e instanceof Error ? e.message : String(e)}`)] };
|
|
39
58
|
}
|
|
40
59
|
if (!exists)
|
|
41
60
|
return { orchestrators: [], diagnostics: [] };
|
|
@@ -44,28 +63,27 @@ function readDeclaredOrchestrators(root) {
|
|
|
44
63
|
contents = fs_1.default.readFileSync(file, 'utf-8');
|
|
45
64
|
}
|
|
46
65
|
catch (e) {
|
|
47
|
-
return { orchestrators: [], diagnostics: [`${file}: cannot read manifest (${e instanceof Error ? e.message : String(e)})`] };
|
|
66
|
+
return { orchestrators: [], diagnostics: [(0, text_1.sanitizeDiagnosticText)(`${file}: cannot read manifest (${e instanceof Error ? e.message : String(e)})`)] };
|
|
48
67
|
}
|
|
49
68
|
let raw;
|
|
50
69
|
try {
|
|
51
70
|
raw = JSON.parse(contents);
|
|
52
71
|
}
|
|
53
72
|
catch (e) {
|
|
54
|
-
return { orchestrators: [], diagnostics: [`${file}: manifest is not valid JSON (${e instanceof Error ? e.message : String(e)})`] };
|
|
73
|
+
return { orchestrators: [], diagnostics: [(0, text_1.sanitizeDiagnosticText)(`${file}: manifest is not valid JSON (${e instanceof Error ? e.message : String(e)})`)] };
|
|
55
74
|
}
|
|
56
75
|
const decl = raw?.orchestrator;
|
|
57
76
|
if (decl === undefined)
|
|
58
77
|
return { orchestrators: [], diagnostics: [] };
|
|
59
78
|
if (typeof decl !== 'object' || decl === null || Array.isArray(decl)) {
|
|
60
|
-
return { orchestrators: [], diagnostics: [`${file}: "orchestrator" must be an object`] };
|
|
79
|
+
return { orchestrators: [], diagnostics: [(0, text_1.sanitizeDiagnosticText)(`${file}: "orchestrator" must be an object`)] };
|
|
61
80
|
}
|
|
62
81
|
const problems = [];
|
|
63
82
|
const entries = decl;
|
|
64
83
|
for (const key of Object.keys(entries)) {
|
|
65
84
|
if (!ALLOWED_FIELDS.includes(key)) {
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
// control characters, which would otherwise let a crafted key forge extra log lines.
|
|
85
|
+
// Quote keys unambiguously; sanitize the complete diagnostic below because
|
|
86
|
+
// JSON.stringify preserves Unicode line separators and C1 controls.
|
|
69
87
|
problems.push(`unknown field ${JSON.stringify(key)} — the contract admits only ${ALLOWED_FIELDS.join(', ')}`);
|
|
70
88
|
}
|
|
71
89
|
}
|
|
@@ -79,7 +97,7 @@ function readDeclaredOrchestrators(root) {
|
|
|
79
97
|
}
|
|
80
98
|
}
|
|
81
99
|
if (problems.length > 0) {
|
|
82
|
-
return { orchestrators: [], diagnostics: [`${file}: invalid "orchestrator" declaration — ${problems.join('; ')}`] };
|
|
100
|
+
return { orchestrators: [], diagnostics: [(0, text_1.sanitizeDiagnosticText)(`${file}: invalid "orchestrator" declaration — ${problems.join('; ')}`)] };
|
|
83
101
|
}
|
|
84
102
|
return {
|
|
85
103
|
orchestrators: [{
|
|
@@ -126,13 +144,19 @@ function readDeclaredOrchestrators(root) {
|
|
|
126
144
|
*/
|
|
127
145
|
function collectDeclaredOrchestrators() {
|
|
128
146
|
const declared = [];
|
|
129
|
-
const
|
|
147
|
+
const droppedNames = [];
|
|
148
|
+
const { names: availableSkillNames, diagnostics } = discoverDeclaredSkillNames();
|
|
130
149
|
const seenNames = new Set();
|
|
131
150
|
const seenSanitizedNames = new Set();
|
|
132
151
|
for (const reg of (0, registries_1.listRegistries)()) {
|
|
133
152
|
const r = readDeclaredOrchestrators(reg.contentRoot);
|
|
134
153
|
for (const orch of r.orchestrators) {
|
|
135
154
|
const file = path_1.default.join(reg.contentRoot, registries_1.REGISTRY_MANIFEST_NAME);
|
|
155
|
+
if (!availableSkillNames.has(orch.name)) {
|
|
156
|
+
droppedNames.push(orch.name);
|
|
157
|
+
diagnostics.push(`${(0, text_1.sanitizeDiagnosticText)(file)}: orchestrator declaration dropped because skill "${(0, text_1.sanitizeDiagnosticText)(orch.name)}" is not discoverable in configured safe registries`);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
136
160
|
if (seenNames.has(orch.name)) {
|
|
137
161
|
diagnostics.push(`${file}: orchestrator "${(0, text_1.stripControlChars)(orch.name)}" duplicates one already declared by an earlier registry — shadowed duplicate dropped`);
|
|
138
162
|
continue;
|
|
@@ -149,7 +173,7 @@ function collectDeclaredOrchestrators() {
|
|
|
149
173
|
}
|
|
150
174
|
diagnostics.push(...r.diagnostics);
|
|
151
175
|
}
|
|
152
|
-
return { declared, diagnostics };
|
|
176
|
+
return { declared, diagnostics, droppedNames };
|
|
153
177
|
}
|
|
154
178
|
/** Recolecta declarados y emite sus diagnosticos como warnings. Punto unico usado por
|
|
155
179
|
* `InjectionOrchestrator.inputFor`/`statusInputFor` y por `commands/hooks/claude.ts`
|