agentic-workflow-manager 9.20.4 → 9.22.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.
- package/dist/src/commands/change/index.js +14 -3
- package/dist/src/commands/preflight/checks.js +31 -0
- package/dist/src/commands/sensors/compatibility/contract.js +56 -3
- package/dist/src/commands/sensors/compatibility/manifest.js +16 -2
- package/dist/src/commands/sensors/index.js +10 -1
- package/dist/src/commands/sensors/init.js +7 -1
- package/dist/src/commands/sensors/inputs-digest.js +315 -0
- package/dist/src/commands/sensors/prepare.js +10 -0
- package/dist/src/commands/sensors/result-cache.js +278 -0
- package/dist/src/commands/sensors/result.js +31 -12
- package/dist/src/commands/sensors/run.js +265 -8
- package/dist/src/commands/sensors/status.js +66 -7
- package/dist/src/commands/watch/init.js +3 -15
- package/dist/src/core/awm-gitignore.js +211 -0
- package/dist/src/core/changes/receipt.js +219 -9
- package/dist/src/core/diagnostics/checks.js +11 -0
- package/dist/tests/commands/change/new.test.js +1 -1
- package/dist/tests/commands/change/validate-unmeasured.test.js +246 -0
- package/dist/tests/commands/preflight/awm-gitignore.test.js +52 -0
- package/dist/tests/commands/preflight/sensors-inputs.test.js +110 -0
- package/dist/tests/commands/sensors/compatibility/contract.test.js +60 -0
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +94 -0
- package/dist/tests/commands/sensors/init-gitignore.test.js +59 -0
- package/dist/tests/commands/sensors/inputs-digest.test.js +373 -0
- package/dist/tests/commands/sensors/result-cache.test.js +352 -0
- package/dist/tests/commands/sensors/run-cache.test.js +434 -0
- package/dist/tests/commands/sensors/run-slice.test.js +171 -0
- package/dist/tests/commands/sensors/status-inputs.test.js +271 -0
- package/dist/tests/commands/watch/init-gitignore.test.js +34 -0
- package/dist/tests/commands/watch/track-freeze.test.js +11 -7
- package/dist/tests/commands/watch/watch-init.test.js +3 -1
- package/dist/tests/core/awm-gitignore.test.js +58 -0
- package/dist/tests/core/changes/receipt.test.js +261 -2
- package/dist/tests/core/diagnostics/awm-gitignore.test.js +101 -0
- package/dist/tests/core/ledger/gitignore.test.js +6 -1
- package/dist/tests/structural/sensor-execution-optimization-docs-contract.test.js +102 -0
- package/dist/tests/structural/sensor-slice-decision-contract.test.js +63 -0
- package/package.json +2 -1
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
|
@@ -9,6 +9,8 @@ exports.detectPackDrift = detectPackDrift;
|
|
|
9
9
|
exports.shouldRun = shouldRun;
|
|
10
10
|
exports.resolveConcurrency = resolveConcurrency;
|
|
11
11
|
exports.runSensors = runSensors;
|
|
12
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
13
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
14
|
const os_1 = __importDefault(require("os"));
|
|
13
15
|
const path_1 = __importDefault(require("path"));
|
|
14
16
|
const baseline_1 = require("./baseline");
|
|
@@ -28,11 +30,205 @@ const prepare_1 = require("./prepare");
|
|
|
28
30
|
const verdict_1 = require("./verdict");
|
|
29
31
|
const project_1 = require("./project");
|
|
30
32
|
const read_only_observation_1 = require("./read-only-observation");
|
|
33
|
+
const inputs_digest_1 = require("./inputs-digest");
|
|
34
|
+
const result_cache_1 = require("./result-cache");
|
|
35
|
+
const cli_version_1 = require("../../core/cli-version");
|
|
36
|
+
const worktree_1 = require("../../core/changes/worktree");
|
|
37
|
+
const git_1 = require("../../core/changes/git");
|
|
31
38
|
// Sensor JSON output can be several MB on large repos (e.g. `eslint --format json`
|
|
32
39
|
// with thousands of findings). A 1MB cap killed the child with SIGTERM when
|
|
33
40
|
// exceeded — which previously surfaced as a false "timeout".
|
|
34
41
|
/** Hard ceiling on parallel sensors: past this, they only contend for the same cores. */
|
|
35
42
|
const MAX_CONCURRENCY = 4;
|
|
43
|
+
/** Relative POSIX under gitRoot; realpath both sides (Windows-safe). Empty when same path. */
|
|
44
|
+
function posixJoinRel(root, abs) {
|
|
45
|
+
return (0, inputs_digest_1.toSafeRepoRel)(root, abs);
|
|
46
|
+
}
|
|
47
|
+
function effectiveClosureOnly(projectOrPack, liveSensor) {
|
|
48
|
+
if (projectOrPack && projectOrPack.closureOnly !== undefined)
|
|
49
|
+
return projectOrPack.closureOnly;
|
|
50
|
+
if (liveSensor && liveSensor.closureOnly !== undefined)
|
|
51
|
+
return liveSensor.closureOnly;
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
function effectiveInputs(manifestInputs, variant) {
|
|
55
|
+
if (manifestInputs && manifestInputs.length > 0)
|
|
56
|
+
return manifestInputs;
|
|
57
|
+
if (variant?.inputs && variant.inputs.length > 0)
|
|
58
|
+
return variant.inputs;
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
function includePackageJsonForVariant(variant) {
|
|
62
|
+
if (!variant)
|
|
63
|
+
return false;
|
|
64
|
+
if (variant.command.packageManager)
|
|
65
|
+
return true;
|
|
66
|
+
if (variant.requirements.packageJsonFields && variant.requirements.packageJsonFields.length > 0)
|
|
67
|
+
return true;
|
|
68
|
+
if (variant.probe.kind === 'package-script-present')
|
|
69
|
+
return true;
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
function sliceSkipExecution(name, requestedScope) {
|
|
73
|
+
return {
|
|
74
|
+
name,
|
|
75
|
+
timeoutMs: 1,
|
|
76
|
+
timeoutSource: 'fallback',
|
|
77
|
+
requestedScope,
|
|
78
|
+
effectiveScope: 'full',
|
|
79
|
+
syntheticStatus: 'skipped',
|
|
80
|
+
syntheticReason: 'closureOnly: skipped by --slice',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function buildCachePlan(args) {
|
|
84
|
+
const toolVersion = args.liveState?.toolVersion;
|
|
85
|
+
const runtimeVersion = args.liveState?.runtimeVersion;
|
|
86
|
+
const command = args.variant?.command ?? args.command ?? null;
|
|
87
|
+
const fingerprintParts = {
|
|
88
|
+
variantId: args.liveState?.variantId ?? args.variant?.id ?? null,
|
|
89
|
+
command,
|
|
90
|
+
changedCommand: args.variant?.changedCommand,
|
|
91
|
+
cliVersion: (0, cli_version_1.cliVersion)(),
|
|
92
|
+
inputs: args.inputGlobs,
|
|
93
|
+
toolVersion,
|
|
94
|
+
runtimeVersion,
|
|
95
|
+
};
|
|
96
|
+
// Cache requires live versions (RF-2.10). Receipt measurements still need a fingerprint
|
|
97
|
+
// when versions are absent so project-declared sensors with inputs do not demote RF-4.6.
|
|
98
|
+
const cacheFingerprint = (0, result_cache_1.computeSensorFingerprint)(fingerprintParts);
|
|
99
|
+
const fingerprint = cacheFingerprint
|
|
100
|
+
?? (0, result_cache_1.computeSensorFingerprint)(fingerprintParts, { allowMissingLiveVersions: true });
|
|
101
|
+
if (fingerprint === null)
|
|
102
|
+
return null;
|
|
103
|
+
const packageRootRel = posixJoinRel(args.gitRoot, args.packageRoot);
|
|
104
|
+
const sensorsJsonRel = posixJoinRel(args.gitRoot, args.manifestPath);
|
|
105
|
+
const includePackageJson = args.variant
|
|
106
|
+
? includePackageJsonForVariant(args.variant)
|
|
107
|
+
: Boolean(args.command?.packageManager);
|
|
108
|
+
const alwaysMeasuredRepoRels = (0, inputs_digest_1.buildAlwaysMeasuredPaths)({
|
|
109
|
+
sensorsJsonRel,
|
|
110
|
+
assetRels: args.assets ?? args.variant?.assets ?? [],
|
|
111
|
+
configFileRels: args.variant?.requirements.configFiles ?? [],
|
|
112
|
+
includePackageJson,
|
|
113
|
+
packageRootRel: packageRootRel === '' ? '' : packageRootRel,
|
|
114
|
+
});
|
|
115
|
+
const digest = (0, inputs_digest_1.computeCacheInputDigest)({
|
|
116
|
+
gitRoot: args.gitRoot,
|
|
117
|
+
packageRoot: args.packageRoot,
|
|
118
|
+
inputGlobs: args.inputGlobs,
|
|
119
|
+
alwaysMeasuredRepoRels,
|
|
120
|
+
});
|
|
121
|
+
if (!digest.ok)
|
|
122
|
+
return null;
|
|
123
|
+
const projectKey = `sha256:${crypto_1.default.createHash('sha256').update(`${args.manifestPath}\0${args.packageRoot}`).digest('hex')}`;
|
|
124
|
+
// Baseline is excluded from the cache digest but required for validate (RF-2.12 / RF-4.5).
|
|
125
|
+
// Derive from the already-safe packageRootRel — do NOT posixJoinRel an absolute path to a
|
|
126
|
+
// often-missing baseline file (realpath falls back → Windows path.relative goes absolute).
|
|
127
|
+
const baselineForValidate = packageRootRel === ''
|
|
128
|
+
? inputs_digest_1.CACHE_DIGEST_BASELINE_REL
|
|
129
|
+
: (0, inputs_digest_1.joinRepoRel)(packageRootRel, inputs_digest_1.CACHE_DIGEST_BASELINE_REL);
|
|
130
|
+
const validateAlwaysMeasuredRepoRels = [...new Set([
|
|
131
|
+
...alwaysMeasuredRepoRels,
|
|
132
|
+
baselineForValidate,
|
|
133
|
+
])].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
134
|
+
const rootRel = packageRootRel === '' ? '' : packageRootRel;
|
|
135
|
+
return {
|
|
136
|
+
gitRoot: args.gitRoot,
|
|
137
|
+
projectKey,
|
|
138
|
+
sensorFingerprint: fingerprint,
|
|
139
|
+
inputDigest: digest.digest,
|
|
140
|
+
inputGlobs: args.inputGlobs,
|
|
141
|
+
packageRootRel: rootRel,
|
|
142
|
+
alwaysMeasuredRepoRels,
|
|
143
|
+
measuredPaths: digest.matchedPaths,
|
|
144
|
+
validateAlwaysMeasuredRepoRels,
|
|
145
|
+
cacheable: cacheFingerprint !== null,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function measurementFromPlan(plan) {
|
|
149
|
+
return {
|
|
150
|
+
inputs: [...plan.inputGlobs],
|
|
151
|
+
packageRootRel: plan.packageRootRel,
|
|
152
|
+
inputDigest: plan.inputDigest,
|
|
153
|
+
sensorFingerprint: plan.sensorFingerprint,
|
|
154
|
+
measuredPaths: [...plan.measuredPaths],
|
|
155
|
+
alwaysMeasuredPaths: [...plan.validateAlwaysMeasuredRepoRels],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
async function runPreparedSlot(slot, projectCwd, baseline, cacheEnabled, writeCache) {
|
|
159
|
+
const { execution, cachePlan } = slot;
|
|
160
|
+
// RF-2.2/2.6/6.1: synthetic skip/disabled/inconclusive prepare results must not
|
|
161
|
+
// be overridden by a prior cache hit.
|
|
162
|
+
const synthetic = execution.syntheticStatus !== undefined;
|
|
163
|
+
// RF-2.10: cacheable is false without live versions — still attach measurements below.
|
|
164
|
+
if (cacheEnabled && cachePlan?.cacheable && !synthetic) {
|
|
165
|
+
const cached = (0, result_cache_1.readResultCache)(cachePlan.gitRoot, {
|
|
166
|
+
sensorId: execution.name,
|
|
167
|
+
projectKey: cachePlan.projectKey,
|
|
168
|
+
sensorFingerprint: cachePlan.sensorFingerprint,
|
|
169
|
+
inputDigest: cachePlan.inputDigest,
|
|
170
|
+
});
|
|
171
|
+
if (cached) {
|
|
172
|
+
const raw = {
|
|
173
|
+
name: cached.raw.name,
|
|
174
|
+
status: cached.raw.status,
|
|
175
|
+
errors: cached.raw.errors,
|
|
176
|
+
...(cached.raw.failureKind ? { failureKind: cached.raw.failureKind } : {}),
|
|
177
|
+
...(cached.raw.certification ? { certification: cached.raw.certification } : {}),
|
|
178
|
+
...(cached.raw.skipReason ? { skipReason: cached.raw.skipReason } : {}),
|
|
179
|
+
cached: true,
|
|
180
|
+
cacheProvenance: {
|
|
181
|
+
...(cached.originCommit ? { originCommit: cached.originCommit } : {}),
|
|
182
|
+
originDirty: cached.originDirty,
|
|
183
|
+
inputDigest: cached.inputDigest,
|
|
184
|
+
sensorFingerprint: cached.sensorFingerprint,
|
|
185
|
+
},
|
|
186
|
+
measurement: measurementFromPlan(cachePlan),
|
|
187
|
+
execution: {
|
|
188
|
+
timeoutMs: execution.timeoutMs,
|
|
189
|
+
timeoutSource: execution.timeoutSource,
|
|
190
|
+
elapsedMs: 0,
|
|
191
|
+
requestedScope: execution.requestedScope,
|
|
192
|
+
effectiveScope: execution.effectiveScope,
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
return baseline ? (0, result_1.applyBaseline)(raw, baseline[execution.name]) : raw;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const raw = await (0, result_1.executePrepared)(execution, projectCwd);
|
|
199
|
+
if (cacheEnabled && writeCache && cachePlan?.cacheable && !synthetic && (0, result_cache_1.canCacheRawResult)(raw) && execution.requestedScope === 'full') {
|
|
200
|
+
const after = (0, inputs_digest_1.computeCacheInputDigest)({
|
|
201
|
+
gitRoot: cachePlan.gitRoot,
|
|
202
|
+
packageRoot: projectCwd,
|
|
203
|
+
inputGlobs: cachePlan.inputGlobs,
|
|
204
|
+
alwaysMeasuredRepoRels: cachePlan.alwaysMeasuredRepoRels,
|
|
205
|
+
});
|
|
206
|
+
if (after.ok && after.digest === cachePlan.inputDigest) {
|
|
207
|
+
const state = (0, worktree_1.gitState)(cachePlan.gitRoot, true);
|
|
208
|
+
const originCommit = state.kind === 'ok' ? state.commit : null;
|
|
209
|
+
const originDirty = state.kind === 'ok' ? state.dirty : true;
|
|
210
|
+
try {
|
|
211
|
+
(0, result_cache_1.writeResultCache)(cachePlan.gitRoot, {
|
|
212
|
+
schema: 1,
|
|
213
|
+
sensorId: execution.name,
|
|
214
|
+
projectKey: cachePlan.projectKey,
|
|
215
|
+
sensorFingerprint: cachePlan.sensorFingerprint,
|
|
216
|
+
inputDigest: cachePlan.inputDigest,
|
|
217
|
+
originCommit,
|
|
218
|
+
originDirty,
|
|
219
|
+
raw: (0, result_cache_1.toCacheRaw)(raw),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// Cache write is best-effort beside the verdict; never fail the run.
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const withMeasurement = cachePlan
|
|
228
|
+
? { ...raw, measurement: measurementFromPlan(cachePlan) }
|
|
229
|
+
: raw;
|
|
230
|
+
return baseline ? (0, result_1.applyBaseline)(withMeasurement, baseline[execution.name]) : withMeasurement;
|
|
231
|
+
}
|
|
36
232
|
async function resolveLiveFromSource(cwd, source, packSelection) {
|
|
37
233
|
try {
|
|
38
234
|
const parsed = (0, contract_1.parseSensorPack)(JSON.parse(source.content), source.path);
|
|
@@ -215,6 +411,22 @@ async function runSensors(opts = {}) {
|
|
|
215
411
|
const drift = parsed.kind === 'legacy' ? detectPackDrift(manifestDir, parsed.pack) : undefined;
|
|
216
412
|
const requestedScope = opts.changed ? 'changed' : 'full';
|
|
217
413
|
const prepared = [];
|
|
414
|
+
const cacheEnabled = opts.useCache === true && !opts.ignoreBaseline && !opts.readOnly;
|
|
415
|
+
const writeCache = cacheEnabled && requestedScope === 'full';
|
|
416
|
+
// Realpath so Windows git toplevel / Node path forms agree before cache digests.
|
|
417
|
+
const gitRootRaw = (0, git_1.repoRoot)(manifestDir) ?? manifestDir;
|
|
418
|
+
let gitRoot = gitRootRaw;
|
|
419
|
+
try {
|
|
420
|
+
gitRoot = fs_1.default.realpathSync.native(path_1.default.resolve(gitRootRaw));
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
try {
|
|
424
|
+
gitRoot = fs_1.default.realpathSync(path_1.default.resolve(gitRootRaw));
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
gitRoot = path_1.default.resolve(gitRootRaw);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
218
430
|
const activeManifest = parsed.pack;
|
|
219
431
|
const sensorsMap = activeManifest.sensors ?? {};
|
|
220
432
|
const packSelected = projectSensorsManifest !== null && hasSelectedPack(projectSensorsManifest);
|
|
@@ -240,6 +452,10 @@ async function runSensors(opts = {}) {
|
|
|
240
452
|
const fast = projectEntry.fast ?? false;
|
|
241
453
|
if (!shouldRun(fast, opts))
|
|
242
454
|
continue;
|
|
455
|
+
if (opts.slice && effectiveClosureOnly(projectEntry, undefined)) {
|
|
456
|
+
prepared.push({ execution: sliceSkipExecution(name, requestedScope), cachePlan: null });
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
243
459
|
const markers = [
|
|
244
460
|
...(projectEntry.applicability?.allFiles ?? []),
|
|
245
461
|
...(projectEntry.applicability?.anyFiles ?? []),
|
|
@@ -255,9 +471,27 @@ async function runSensors(opts = {}) {
|
|
|
255
471
|
projectTimeout: projectEntry.timeout,
|
|
256
472
|
applicabilityPaths,
|
|
257
473
|
});
|
|
258
|
-
|
|
474
|
+
const disabled = projectEntry.enabled === false
|
|
259
475
|
? { ...execution, command: undefined, syntheticStatus: 'skipped', syntheticReason: 'disabled' }
|
|
260
|
-
: execution
|
|
476
|
+
: execution;
|
|
477
|
+
// Project-declared sensors have no live tool/runtime versions → never cache (RF-2.10),
|
|
478
|
+
// but still attach digests/measurements when authored inputs exist (RF-4.6).
|
|
479
|
+
let projectPlan = null;
|
|
480
|
+
const projectInputs = effectiveInputs(projectEntry.inputs, undefined);
|
|
481
|
+
if (projectInputs) {
|
|
482
|
+
projectPlan = buildCachePlan({
|
|
483
|
+
gitRoot,
|
|
484
|
+
packageRoot: projectCwd,
|
|
485
|
+
manifestPath: configured.manifestPath,
|
|
486
|
+
sensorId: name,
|
|
487
|
+
inputGlobs: projectInputs,
|
|
488
|
+
variant: undefined,
|
|
489
|
+
liveState: undefined,
|
|
490
|
+
assets: projectEntry.assets,
|
|
491
|
+
command: projectEntry.command,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
prepared.push({ execution: disabled, cachePlan: projectPlan });
|
|
261
495
|
continue;
|
|
262
496
|
}
|
|
263
497
|
const liveSensor = parsed.kind !== 'legacy' ? live?.pack.sensors[name] : undefined;
|
|
@@ -294,6 +528,10 @@ async function runSensors(opts = {}) {
|
|
|
294
528
|
: entry.fast ?? false;
|
|
295
529
|
if (!shouldRun(fast, opts))
|
|
296
530
|
continue;
|
|
531
|
+
if (opts.slice && effectiveClosureOnly(packBoundSensor, liveSensor)) {
|
|
532
|
+
prepared.push({ execution: sliceSkipExecution(name, requestedScope), cachePlan: null });
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
297
535
|
const execution = parsed.kind !== 'legacy'
|
|
298
536
|
? (0, prepare_1.prepareV2Sensor)({
|
|
299
537
|
name,
|
|
@@ -309,14 +547,33 @@ async function runSensors(opts = {}) {
|
|
|
309
547
|
requestedScope,
|
|
310
548
|
changed: changed ?? undefined,
|
|
311
549
|
});
|
|
312
|
-
|
|
550
|
+
const ready = packBoundSensor.enabled === false
|
|
313
551
|
? { ...execution, command: undefined, syntheticStatus: 'skipped', syntheticReason: 'disabled' }
|
|
314
|
-
: execution
|
|
552
|
+
: execution;
|
|
553
|
+
// Measurement (and optional cache) when the sensor has authored inputs — even under
|
|
554
|
+
// `--no-cache`, so schema-2 receipts still carry digests (RF-4.3 / RF-2.9).
|
|
555
|
+
let cachePlan = null;
|
|
556
|
+
if (parsed.kind !== 'legacy') {
|
|
557
|
+
const variantId = packBoundSensor.variantId;
|
|
558
|
+
const variant = liveSensor?.variants.find((candidate) => candidate.id === variantId)
|
|
559
|
+
?? liveSensor?.variants.find((candidate) => candidate.id === liveState?.variantId);
|
|
560
|
+
const inputs = effectiveInputs(packBoundSensor.inputs, variant);
|
|
561
|
+
if (inputs) {
|
|
562
|
+
cachePlan = buildCachePlan({
|
|
563
|
+
gitRoot,
|
|
564
|
+
packageRoot: projectCwd,
|
|
565
|
+
manifestPath: configured.manifestPath,
|
|
566
|
+
sensorId: name,
|
|
567
|
+
inputGlobs: inputs,
|
|
568
|
+
variant,
|
|
569
|
+
liveState,
|
|
570
|
+
assets: packBoundSensor.assets ?? variant?.assets,
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
prepared.push({ execution: ready, cachePlan });
|
|
315
575
|
}
|
|
316
|
-
const results = await pooled(prepared.map(
|
|
317
|
-
const result = await (0, result_1.executePrepared)(entry, projectCwd);
|
|
318
|
-
return baseline ? (0, result_1.applyBaseline)(result, baseline[entry.name]) : result;
|
|
319
|
-
}), resolveConcurrency(activeManifest, prepared.length));
|
|
576
|
+
const results = await pooled(prepared.map(slot => async () => runPreparedSlot(slot, projectCwd, baseline, cacheEnabled, writeCache)), resolveConcurrency(activeManifest, prepared.length));
|
|
320
577
|
let overall = (0, verdict_1.reduceVerdict)(results);
|
|
321
578
|
// Legacy commands retain operational compatibility but their shell-backed
|
|
322
579
|
// contract cannot certify a green run.
|
|
@@ -15,6 +15,61 @@ const project_1 = require("./project");
|
|
|
15
15
|
const source_1 = require("./compatibility/source");
|
|
16
16
|
const registries_1 = require("../../core/registries");
|
|
17
17
|
const manifest_1 = require("./compatibility/manifest");
|
|
18
|
+
const UNCACHEABLE_DETAIL = 'not cacheable: no effective inputs';
|
|
19
|
+
function hasAuthoredInputs(inputs) {
|
|
20
|
+
return Array.isArray(inputs) && inputs.length > 0;
|
|
21
|
+
}
|
|
22
|
+
/** RF-1 effective inputs: project override ?? pack-variant inputs. */
|
|
23
|
+
function hasEffectiveInputs(manifestInputs, variantInputs) {
|
|
24
|
+
return hasAuthoredInputs(manifestInputs) || hasAuthoredInputs(variantInputs);
|
|
25
|
+
}
|
|
26
|
+
function uncacheableWarning(sensor) {
|
|
27
|
+
return { sensor, detail: UNCACHEABLE_DETAIL };
|
|
28
|
+
}
|
|
29
|
+
function withWarnings(result, warnings) {
|
|
30
|
+
return warnings.length === 0 ? result : { ...result, warnings };
|
|
31
|
+
}
|
|
32
|
+
function variantInputsFromPack(pack, sensorName, variantId) {
|
|
33
|
+
if (!pack || !variantId)
|
|
34
|
+
return undefined;
|
|
35
|
+
const sensor = pack.sensors[sensorName];
|
|
36
|
+
const variant = sensor?.variants.find(candidate => candidate.id === variantId);
|
|
37
|
+
return variant?.inputs;
|
|
38
|
+
}
|
|
39
|
+
function parsePackForInputs(source) {
|
|
40
|
+
try {
|
|
41
|
+
const parsed = (0, contract_1.parseSensorPack)(JSON.parse(source.content), source.path);
|
|
42
|
+
return parsed.kind === 'v2' ? parsed.pack : null;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Enabled v2/v3 sensors without effective inputs → RF-3.4 warnings (never blocks overall). */
|
|
49
|
+
function collectConfiguredInputsWarnings(sensors, pack = null) {
|
|
50
|
+
const warnings = [];
|
|
51
|
+
for (const [name, raw] of Object.entries(sensors)) {
|
|
52
|
+
if (raw.enabled === false)
|
|
53
|
+
continue;
|
|
54
|
+
if ((0, manifest_1.isProjectDeclaredSensor)(raw)) {
|
|
55
|
+
if (!hasAuthoredInputs(raw.inputs)) {
|
|
56
|
+
warnings.push(uncacheableWarning(name));
|
|
57
|
+
}
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const packBound = raw;
|
|
61
|
+
if (!hasEffectiveInputs(packBound.inputs, variantInputsFromPack(pack, name, packBound.variantId))) {
|
|
62
|
+
warnings.push(uncacheableWarning(name));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return warnings;
|
|
66
|
+
}
|
|
67
|
+
/** Legacy v1 sensors never author `inputs` — every enabled entry is uncacheable. */
|
|
68
|
+
function collectLegacyInputsWarnings(sensors) {
|
|
69
|
+
return Object.entries(sensors)
|
|
70
|
+
.filter(([, config]) => config?.enabled !== false)
|
|
71
|
+
.map(([name]) => uncacheableWarning(name));
|
|
72
|
+
}
|
|
18
73
|
/** First non-flag token after `npx` — the tool the command actually runs. */
|
|
19
74
|
function npxTool(parts) {
|
|
20
75
|
for (let i = 1; i < parts.length; i++) {
|
|
@@ -356,12 +411,12 @@ async function computeSensorStatus(cwd = process.cwd()) {
|
|
|
356
411
|
checks[name] = { ok: false, detail: 'pack-bound sensor requires a nonempty pack' };
|
|
357
412
|
}
|
|
358
413
|
}
|
|
359
|
-
return {
|
|
414
|
+
return withWarnings({
|
|
360
415
|
overall: Object.keys(checks).length > 0 && Object.values(checks).every(check => check.ok) ? 'READY' : 'DEGRADED',
|
|
361
416
|
pack: null,
|
|
362
417
|
checks,
|
|
363
418
|
...projectMetadata(project, 'project-sensors', 'project-declared-only'),
|
|
364
|
-
};
|
|
419
|
+
}, collectConfiguredInputsWarnings(parsed.pack.sensors));
|
|
365
420
|
}
|
|
366
421
|
if (parsed.kind === 'v2' || parsed.kind === 'v3') {
|
|
367
422
|
const parsedPack = parsed.pack;
|
|
@@ -378,6 +433,7 @@ async function computeSensorStatus(cwd = process.cwd()) {
|
|
|
378
433
|
return sourceFailure(project, packId ?? 'unknown', resolution);
|
|
379
434
|
const sourceMeta = resolvedSourceMetadata(resolution);
|
|
380
435
|
const liveIds = livePackSensorIds(resolution.source);
|
|
436
|
+
const packForInputs = parsePackForInputs(resolution.source);
|
|
381
437
|
const packManifest = packBoundOnly(parsedPack);
|
|
382
438
|
const checks = {};
|
|
383
439
|
let compatibility;
|
|
@@ -396,7 +452,10 @@ async function computeSensorStatus(cwd = process.cwd()) {
|
|
|
396
452
|
: { ok: false, detail };
|
|
397
453
|
}
|
|
398
454
|
}
|
|
399
|
-
return
|
|
455
|
+
return withWarnings({
|
|
456
|
+
overall: 'DEGRADED', pack: packId, checks,
|
|
457
|
+
...projectMetadata(project, 'project-sensors', sourceMeta.reason, { ...sourceMeta }),
|
|
458
|
+
}, collectConfiguredInputsWarnings(parsedPack.sensors, packForInputs));
|
|
400
459
|
}
|
|
401
460
|
for (const [name, sensor] of Object.entries(parsedPack.sensors)) {
|
|
402
461
|
if ((0, manifest_1.isProjectDeclaredSensor)(sensor)) {
|
|
@@ -408,12 +467,12 @@ async function computeSensorStatus(cwd = process.cwd()) {
|
|
|
408
467
|
: staticCompatibilityCheck(packSensor, compatibility[name]) ?? checkStructuredCommand(packSensor.command, packageRoot, packSensor.assets);
|
|
409
468
|
}
|
|
410
469
|
const uninitialized = uninitializedSensors(packManifest, compatibility);
|
|
411
|
-
return {
|
|
470
|
+
return withWarnings({
|
|
412
471
|
overall: Object.keys(checks).length > 0 && Object.values(checks).every(check => check.ok) ? 'READY' : 'DEGRADED',
|
|
413
472
|
pack: packId, checks,
|
|
414
473
|
...(uninitialized ? { uninitialized } : {}),
|
|
415
474
|
...projectMetadata(project, 'project-sensors', sourceMeta.reason, { ...sourceMeta }),
|
|
416
|
-
};
|
|
475
|
+
}, collectConfiguredInputsWarnings(parsedPack.sensors, packForInputs));
|
|
417
476
|
}
|
|
418
477
|
const manifest = parsed.pack;
|
|
419
478
|
const checks = {};
|
|
@@ -428,8 +487,8 @@ async function computeSensorStatus(cwd = process.cwd()) {
|
|
|
428
487
|
}
|
|
429
488
|
checks[name] = checkCmd(config.cmd, packageRoot);
|
|
430
489
|
}
|
|
431
|
-
return {
|
|
490
|
+
return withWarnings({
|
|
432
491
|
overall: Object.keys(manifest.sensors ?? {}).length > 0 && Object.values(checks).every(check => check.ok) ? 'READY' : 'DEGRADED',
|
|
433
492
|
pack: manifest.pack, checks, ...projectMetadata(project, 'project-sensors', 'legacy-v1'),
|
|
434
|
-
};
|
|
493
|
+
}, collectLegacyInputsWarnings(manifest.sensors ?? {}));
|
|
435
494
|
}
|
|
@@ -18,6 +18,7 @@ const lock_1 = require("./lock");
|
|
|
18
18
|
const validate_1 = require("../../core/plan/validate");
|
|
19
19
|
const identity_1 = require("../../core/plan/identity");
|
|
20
20
|
const native_bridge_1 = require("../../core/secure-fs/native-bridge");
|
|
21
|
+
const awm_gitignore_1 = require("../../core/awm-gitignore");
|
|
21
22
|
const MAX_VERIFIER_SCAN_DEPTH = 64;
|
|
22
23
|
const MAX_VERIFIER_SCAN_ENTRIES = 10000;
|
|
23
24
|
const MAX_PACKAGE_JSON_BYTES = 256 * 1024;
|
|
@@ -81,22 +82,9 @@ function detectRequiredVerifiers(repoRoot) {
|
|
|
81
82
|
visit(repoRoot, 0);
|
|
82
83
|
return ['test', 'sensors'].filter((kind) => kinds.has(kind));
|
|
83
84
|
}
|
|
84
|
-
/** El journal es gitignoreado (R1.1):
|
|
85
|
+
/** El journal es gitignoreado (R1.1): bloque gestionado D-024 / #218. */
|
|
85
86
|
function ensureJournalGitignored(repoRoot) {
|
|
86
|
-
|
|
87
|
-
let current = '';
|
|
88
|
-
try {
|
|
89
|
-
if (fs_1.default.lstatSync(gi).isSymbolicLink())
|
|
90
|
-
throw new Error('.gitignore must not be a symlink');
|
|
91
|
-
current = fs_1.default.readFileSync(gi, 'utf8');
|
|
92
|
-
}
|
|
93
|
-
catch (error) {
|
|
94
|
-
if (error.code !== 'ENOENT')
|
|
95
|
-
throw error;
|
|
96
|
-
}
|
|
97
|
-
if (!current.split('\n').some((l) => l.trim() === '.awm/' || l.trim() === '.awm')) {
|
|
98
|
-
fs_1.default.writeFileSync(gi, current.length > 0 && !current.endsWith('\n') ? `${current}\n.awm/\n` : `${current}.awm/\n`);
|
|
99
|
-
}
|
|
87
|
+
(0, awm_gitignore_1.upsertAwmGitignore)(repoRoot);
|
|
100
88
|
}
|
|
101
89
|
const TERMINAL_JOB_STATES = new Set(['exited', 'cancelled']);
|
|
102
90
|
function rebindBlocker(state) {
|