agentic-workflow-manager 9.21.0 → 9.22.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/change/index.js +16 -5
- package/dist/src/commands/init.js +8 -4
- package/dist/src/commands/preflight/checks.js +22 -0
- package/dist/src/commands/registry/add.js +9 -3
- 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/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/sync.js +11 -5
- package/dist/src/commands/update.js +23 -12
- package/dist/src/core/changes/derive.js +6 -3
- package/dist/src/core/changes/git.js +1 -2
- package/dist/src/core/changes/receipt.js +219 -9
- package/dist/src/core/changes/worktree.js +15 -3
- package/dist/src/core/init/steps.js +3 -8
- package/dist/src/core/registries.js +20 -15
- package/dist/src/core/registry-git-errors.js +107 -0
- package/dist/src/core/registry-git.js +93 -0
- package/dist/src/core/registry-sync-report.js +74 -0
- package/dist/src/core/versioning.js +3 -6
- package/dist/src/index.js +5 -8
- package/dist/tests/commands/change/new.test.js +104 -58
- package/dist/tests/commands/change/validate-unmeasured.test.js +246 -0
- package/dist/tests/commands/change/validate.test.js +164 -108
- package/dist/tests/commands/changes/list.test.js +41 -22
- package/dist/tests/commands/job/gate-reconcile.test.js +13 -2
- package/dist/tests/commands/preflight/sensors-inputs.test.js +110 -0
- package/dist/tests/commands/registry-sync-honesty.test.js +175 -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/exec.test.js +3 -1
- 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/update.test.js +8 -8
- package/dist/tests/commands/watch/supervisor-loop.test.js +21 -5
- package/dist/tests/core/changes/helpers.js +15 -3
- package/dist/tests/core/changes/list-paging.test.js +131 -110
- package/dist/tests/core/changes/list.test.js +133 -83
- package/dist/tests/core/changes/receipt.test.js +279 -8
- package/dist/tests/core/diagnostics/provider-checks.test.js +3 -0
- package/dist/tests/core/init/steps-registry-sync.test.js +6 -8
- package/dist/tests/core/registry-git.test.js +153 -0
- package/dist/tests/helpers/commit-seeded-registry.js +18 -0
- package/dist/tests/integration/codex-provider-isolated.test.js +7 -2
- package/dist/tests/integration/copilot-init-isolated.test.js +7 -4
- package/dist/tests/structural/active-documentation.test.js +0 -41
- package/dist/tests/structural/bundle-skill-reference-contract.test.js +0 -12
- package/dist/tests/structural/cli-reference-matches-commander.test.js +94 -0
- package/dist/tests/structural/native-release-artifacts.test.js +116 -107
- package/dist/tests/structural/sensor-documentation-contract.test.js +0 -47
- package/dist/tests/structural/sensor-portability-contract.test.js +0 -7
- package/dist/tests/structural/simple-git-v4-pin.test.js +49 -0
- package/package.json +3 -2
- 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
- package/dist/tests/structural/cloud-admission-docs-contract.test.js +0 -54
- package/dist/tests/structural/documentation-phase-is-mechanized.test.js +0 -29
- package/dist/tests/structural/operator-environments-docs-contract.test.js +0 -70
- package/dist/tests/structural/process-model-description-contract.test.js +0 -34
- package/dist/tests/structural/project-declared-sensors-docs-contract.test.js +0 -104
- package/dist/tests/structural/r4-plan-currentness-docs.test.js +0 -37
|
@@ -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
|
}
|
|
@@ -19,6 +19,7 @@ const picocolors_1 = __importDefault(require("picocolors"));
|
|
|
19
19
|
const paths_1 = require("../core/paths");
|
|
20
20
|
const profile_1 = require("../core/profile");
|
|
21
21
|
const registries_1 = require("../core/registries");
|
|
22
|
+
const registry_sync_report_1 = require("../core/registry-sync-report");
|
|
22
23
|
const bundles_1 = require("../core/bundles");
|
|
23
24
|
const bundle_install_1 = require("../core/bundle-install");
|
|
24
25
|
const profile_pins_1 = require("../core/profile-pins");
|
|
@@ -74,9 +75,14 @@ async function runSyncCore(options, deps = {}) {
|
|
|
74
75
|
}
|
|
75
76
|
const selectedAgents = resolved.targets;
|
|
76
77
|
const syncResults = await d.syncRegistries();
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
// SG-7: any sync/clone failure → non-zero exit. Keep later profile sync / link
|
|
79
|
+
// repair when usable content remains (audit A1) — aborting here left healthy
|
|
80
|
+
// registries advanced on disk without reconciling the project tree.
|
|
81
|
+
const syncHadErrors = (0, registry_sync_report_1.hardFailOnAnyRegistrySyncError)(syncResults);
|
|
82
|
+
if (syncHadErrors) {
|
|
83
|
+
for (const e of (0, registries_1.registrySyncErrors)(syncResults)) {
|
|
84
|
+
console.error(picocolors_1.default.red(` ✖ registry ${e.name}: ${e.error}`));
|
|
85
|
+
}
|
|
80
86
|
}
|
|
81
87
|
try {
|
|
82
88
|
(0, registries_1.assertSyncedRegistriesUsable)(syncResults);
|
|
@@ -126,7 +132,7 @@ async function runSyncCore(options, deps = {}) {
|
|
|
126
132
|
reportProjectLinkRepair(d.reconcileProjectSkillLinks(projectRoot, selectedAgents, (0, registries_2.contentRoots)()));
|
|
127
133
|
if (profile.extensions.length === 0) {
|
|
128
134
|
console.log(picocolors_1.default.yellow('No extensions in .awm/profile.json — nothing to sync. Use `awm add <bundle>` first.'));
|
|
129
|
-
return { code: 0, selectedAgents };
|
|
135
|
+
return { code: syncHadErrors ? 1 : 0, selectedAgents };
|
|
130
136
|
}
|
|
131
137
|
const method = options.method === 'copy' ? 'copy' : 'symlink';
|
|
132
138
|
let result;
|
|
@@ -150,5 +156,5 @@ async function runSyncCore(options, deps = {}) {
|
|
|
150
156
|
for (const id of result.transactionIds) {
|
|
151
157
|
console.log(picocolors_1.default.dim(` transaction ${id} — undo with \`awm backup restore ${id}\``));
|
|
152
158
|
}
|
|
153
|
-
return { code: 0, selectedAgents, result };
|
|
159
|
+
return { code: syncHadErrors ? 1 : 0, selectedAgents, result };
|
|
154
160
|
}
|
|
@@ -26,6 +26,7 @@ const paths_1 = require("../core/paths");
|
|
|
26
26
|
const config_1 = require("../utils/config");
|
|
27
27
|
const agent_targets_1 = require("../core/agent-targets");
|
|
28
28
|
const registries_1 = require("../core/registries");
|
|
29
|
+
const registry_sync_report_1 = require("../core/registry-sync-report");
|
|
29
30
|
const paths_2 = require("../core/paths");
|
|
30
31
|
const regenerate_1 = require("../core/context/regenerate");
|
|
31
32
|
const reconciliation_1 = require("../core/reconciliation");
|
|
@@ -59,11 +60,14 @@ function updateOutro(result) {
|
|
|
59
60
|
const { registries: reg } = result;
|
|
60
61
|
if (reg.configured === 0)
|
|
61
62
|
return 'Nothing updated — no registries configured on this machine.';
|
|
62
|
-
|
|
63
|
+
// SG-7: sync errors always yield code ≠ 0. When healthy registries still advanced,
|
|
64
|
+
// later stages ran — say so instead of the unreachable pre-SG-7 "stale content / exit 0".
|
|
65
|
+
if (result.code !== 0) {
|
|
66
|
+
if (reg.failed.length > 0 && reg.synced > 0) {
|
|
67
|
+
return (`⚠ Finished with registry sync errors — ${reg.synced}/${reg.configured} registries synced `
|
|
68
|
+
+ `(failed: ${reg.failed.join(', ')}); see errors above.`);
|
|
69
|
+
}
|
|
63
70
|
return 'Update failed — see errors above.';
|
|
64
|
-
if (reg.failed.length > 0) {
|
|
65
|
-
return `⚠ Updated with stale content — ${reg.synced}/${reg.configured} registries synced, `
|
|
66
|
-
+ `kept on-disk content for: ${reg.failed.join(', ')}.`;
|
|
67
71
|
}
|
|
68
72
|
return `✅ ${reg.synced} ${reg.synced === 1 ? 'registry' : 'registries'}, skills and hooks updated.`;
|
|
69
73
|
}
|
|
@@ -90,8 +94,10 @@ async function runUpdateCore(options = {}, deps = {}) {
|
|
|
90
94
|
const selectedAgents = resolved.targets;
|
|
91
95
|
const registryResults = await d.syncRegistries();
|
|
92
96
|
for (const r of registryResults) {
|
|
93
|
-
if (r.action === 'error')
|
|
94
|
-
|
|
97
|
+
if (r.action === 'error') {
|
|
98
|
+
// Printed below as a hard SG-7 failure — skip the stale warn path.
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
95
101
|
else if (r.disposition === 'regressed') {
|
|
96
102
|
const latest = r.availableVersion && r.availableVersion !== r.version
|
|
97
103
|
? ` Latest available: ${r.availableVersion}.`
|
|
@@ -131,11 +137,16 @@ async function runUpdateCore(options = {}, deps = {}) {
|
|
|
131
137
|
console.error(picocolors_1.default.dim(" This machine was never initialized: run 'awm init' first."));
|
|
132
138
|
return { code: 1, selectedAgents, registries };
|
|
133
139
|
}
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
|
|
140
|
+
// SG-7: any sync/clone failure → non-zero exit with classified message.
|
|
141
|
+
// Do NOT abort the later stages early when healthy registries already advanced
|
|
142
|
+
// on disk (audit A1): reconcile / hooks / self-update still run against usable
|
|
143
|
+
// content, then the command exits ≠ 0 naming the failed registry.
|
|
144
|
+
const syncHadErrors = (0, registry_sync_report_1.hardFailOnAnyRegistrySyncError)(registryResults);
|
|
145
|
+
if (syncHadErrors) {
|
|
146
|
+
for (const e of (0, registries_1.registrySyncErrors)(registryResults)) {
|
|
147
|
+
console.error(picocolors_1.default.red(` ✖ registry ${e.name}: ${e.error}`));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
139
150
|
try {
|
|
140
151
|
(0, registries_1.assertSyncedRegistriesUsable)(registryResults);
|
|
141
152
|
}
|
|
@@ -188,5 +199,5 @@ async function runUpdateCore(options = {}, deps = {}) {
|
|
|
188
199
|
// pasa `undefined` a propósito, para que la decisión la tome `defaultSelfUpdateMode()`
|
|
189
200
|
// según haya o no un humano en stdin — no este llamador.
|
|
190
201
|
await d.offerSelfUpdate(options.yes === true ? 'assume-yes' : undefined);
|
|
191
|
-
return { code: 0, selectedAgents, registries };
|
|
202
|
+
return { code: syncHadErrors ? 1 : 0, selectedAgents, registries };
|
|
192
203
|
}
|
|
@@ -96,13 +96,16 @@ const fail = (code, message) => ({ ok: false, diagnostic: { code, message: (0, t
|
|
|
96
96
|
* conventional commit does), while `semver` and `breaking` cover every commit.
|
|
97
97
|
* `mergeBaseSha` must be a full commit id (the output of `mergeBase`); anything else is `invalid-input`.
|
|
98
98
|
*/
|
|
99
|
-
function deriveFields(root, mergeBaseSha) {
|
|
99
|
+
function deriveFields(root, mergeBaseSha, pinnedHead) {
|
|
100
100
|
if (typeof root !== 'string' || root.length === 0)
|
|
101
101
|
return fail('invalid-input', 'repository root must be a non-empty string');
|
|
102
102
|
if (typeof mergeBaseSha !== 'string' || !types_1.COMMIT_ID.test(mergeBaseSha))
|
|
103
103
|
return fail('invalid-input', `merge-base must be a full commit id, got "${(0, text_safety_1.esc)(mergeBaseSha)}"`);
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
if (pinnedHead !== undefined && (typeof pinnedHead !== 'string' || !types_1.COMMIT_ID.test(pinnedHead)))
|
|
105
|
+
return fail('invalid-input', `pinned HEAD must be a full commit id, got "${(0, text_safety_1.esc)(pinnedHead)}"`);
|
|
106
|
+
// HEAD is read once: the log and the diff below both use this sha, so a HEAD that moves meanwhile cannot mix snapshots. A
|
|
107
|
+
// command that already pinned HEAD passes it (and checks `headMovedFailure` afterwards), saving one more `rev-parse`.
|
|
108
|
+
const head = pinnedHead ?? (0, git_1.headSha)(root);
|
|
106
109
|
if (head === null)
|
|
107
110
|
return fail('git-failed', 'git could not read HEAD');
|
|
108
111
|
const range = `${mergeBaseSha}..${head}`;
|
|
@@ -171,8 +171,7 @@ function isCommittedClean(root, rel) {
|
|
|
171
171
|
* Unknown state, or an unsafe exclusion, counts as dirty: an unreadable repository must never look clean.
|
|
172
172
|
*/
|
|
173
173
|
function isDirtyOutside(root, extraExcludes) {
|
|
174
|
-
|
|
175
|
-
return state.kind !== 'ok' || state.dirty;
|
|
174
|
+
return (0, worktree_1.treeDirty)(root, true, extraExcludes) !== false;
|
|
176
175
|
}
|
|
177
176
|
/** Repository root with native separators; null outside a repository. Only `rev-parse`: never a (slow, refreshing) `status`. */
|
|
178
177
|
function repoRoot(cwd) {
|