agentic-workflow-manager 9.11.4 → 9.11.6
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/sensors/compatibility/discovery.js +15 -1
- package/dist/src/commands/sensors/init.js +0 -10
- package/dist/src/commands/sensors/prepare.js +17 -1
- package/dist/src/commands/sensors/result.js +1 -0
- package/dist/src/commands/sensors/run.js +0 -1
- package/dist/src/commands/watch/supervisor.js +3 -0
- package/dist/src/core/admission/index.js +42 -20
- package/dist/src/core/artifact-name.js +0 -1
- package/dist/src/core/cli-version.js +21 -4
- package/dist/src/core/dashboard/render-html.js +0 -7
- package/dist/src/core/ledger/scan.js +0 -2
- package/dist/src/core/miro.js +0 -1
- package/dist/src/core/profile.js +0 -1
- package/dist/src/core/text.js +2 -2
- package/dist/src/core/tracks/teardown.js +25 -0
- package/dist/tests/commands/context.test.js +1 -1
- package/dist/tests/commands/multi-agent-targeting.test.js +1 -1
- package/dist/tests/commands/plan/index.test.js +7 -3
- package/dist/tests/commands/preflight/preflight.test.js +0 -1
- package/dist/tests/commands/process.test.js +1 -1
- package/dist/tests/commands/sensors/compatibility/discovery.test.js +63 -0
- package/dist/tests/commands/sensors/exec.test.js +26 -0
- package/dist/tests/commands/sensors/prepare.test.js +34 -0
- package/dist/tests/commands/track/supervisor-wrapper-cli.test.js +0 -3
- package/dist/tests/commands/watch/apply.test.js +1 -1
- package/dist/tests/core/admission/index.test.js +47 -0
- package/dist/tests/core/context/provider.test.js +2 -2
- package/dist/tests/core/context/strategies/config-instructions.test.js +1 -1
- package/dist/tests/core/dashboard/render-terminal.test.js +1 -1
- package/dist/tests/core/diagnostics/provider-checks.test.js +0 -1
- package/dist/tests/core/discovery.test.js +0 -1
- package/dist/tests/core/init/orchestrator.test.js +1 -1
- package/dist/tests/core/journal/process.test.js +1 -1
- package/dist/tests/core/orchestrators.test.js +4 -4
- package/dist/tests/core/secure-fs/native-bridge.test.js +0 -1
- package/dist/tests/core/text.test.js +1 -1
- package/dist/tests/structural/directory-fsync-is-single-source.test.js +1 -1
- package/dist/tests/structural/lint-covers-typescript.test.js +64 -5
- 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
|
@@ -202,12 +202,26 @@ function discoverProjectEvidence(cwd, pack, dependencies = {}) {
|
|
|
202
202
|
if (variant.command?.resolution === 'path' && variant.command.packageManager === variant.requirements.tool)
|
|
203
203
|
pathTools.add(variant.requirements.tool);
|
|
204
204
|
}
|
|
205
|
+
// Un proyecto que todavia no declaro su gestor —sin lockfile y sin campo
|
|
206
|
+
// `packageManager`— es el caso de un `package.json` recien creado, y era el
|
|
207
|
+
// unico caso en que un gestor presente en PATH quedaba estructuralmente
|
|
208
|
+
// invisible: sin lockfile, `packageManager` es null, la rama de PATH no se
|
|
209
|
+
// toma, y `npm` se buscaba en `node_modules/npm` (donde jamas esta). El
|
|
210
|
+
// sensor `test` del pack js-ts reportaba `tool-not-found` con `npm test`
|
|
211
|
+
// perfectamente ejecutable, y en desatendido eso bloquea la admision (#147).
|
|
212
|
+
//
|
|
213
|
+
// Cuando el proyecto NO declaro gestor se sondean todos los gestores del
|
|
214
|
+
// pack presentes en PATH y la prioridad de las variantes elige: no se
|
|
215
|
+
// adivina un gestor por defecto, se miden los que hay. Cuando SI lo declaro
|
|
216
|
+
// —lockfile o campo— solo se sondea ese: un proyecto pnpm nunca resuelve por
|
|
217
|
+
// npm. Un conflicto de lockfiles no habilita nada: sigue sin sondearse.
|
|
218
|
+
const undeterminedPackageManager = packageManager === null && lockManagers.size === 0;
|
|
205
219
|
const environment = pythonEnvironment(root);
|
|
206
220
|
const sitePackages = environment ? pythonSitePackages(root, environment.rootParts, targetPlatform) : [];
|
|
207
221
|
const toolVersions = {};
|
|
208
222
|
const toolProvenance = {};
|
|
209
223
|
for (const tool of [...tools].sort()) {
|
|
210
|
-
if (pathTools.has(tool) && packageManager === tool) {
|
|
224
|
+
if (pathTools.has(tool) && (packageManager === tool || undeterminedPackageManager)) {
|
|
211
225
|
// Discovery/status is read-only. A PATH executable is not inspected or
|
|
212
226
|
// executed here; callers that already possess trusted runtime evidence
|
|
213
227
|
// may inject it explicitly for an execution-time compatibility check.
|
|
@@ -139,16 +139,6 @@ function buildManifest(pack, existing, registryRoot, cwd = process.cwd()) {
|
|
|
139
139
|
}
|
|
140
140
|
return { pack, sensors };
|
|
141
141
|
}
|
|
142
|
-
/** Pack names present as directories under `<registryRoot>/sensor-packs/`, sorted. */
|
|
143
|
-
function availablePacks(registryRoot) {
|
|
144
|
-
const packsDir = path_1.default.join(registryRoot, 'sensor-packs');
|
|
145
|
-
if (!fs_1.default.existsSync(packsDir) || !fs_1.default.statSync(packsDir).isDirectory())
|
|
146
|
-
return [];
|
|
147
|
-
return fs_1.default.readdirSync(packsDir, { withFileTypes: true })
|
|
148
|
-
.filter(e => e.isDirectory())
|
|
149
|
-
.map(e => e.name)
|
|
150
|
-
.sort();
|
|
151
|
-
}
|
|
152
142
|
async function initSensors(opts = {}) {
|
|
153
143
|
const cwd = opts.cwd ?? process.cwd();
|
|
154
144
|
const detectionCwd = opts.packageRoot ? path_1.default.resolve(cwd, opts.packageRoot) : cwd;
|
|
@@ -116,8 +116,23 @@ function prepareV2Sensor(input) {
|
|
|
116
116
|
if (!input || typeof input !== 'object' || !input.sensor || typeof input.name !== 'string' || input.name === '')
|
|
117
117
|
throw new Error('v2 preparation input is invalid');
|
|
118
118
|
const requestedScope = resolveRequestedScope(input.requestedScope);
|
|
119
|
-
|
|
119
|
+
// Un sensor se ejecuta cuando la herramienta ESTA y funciona aca: `certified` es
|
|
120
|
+
// la version exacta que el registry congelo, y `compatible-unverified` es una que
|
|
121
|
+
// cae dentro del rango operativo declarado por el pack y cuyo probe coincidio
|
|
122
|
+
// (resolve.ts: 'operational-range-and-probe'). Antes solo corria la primera, asi
|
|
123
|
+
// que una maquina con npm 10.9.7 frente a un certifiedRange `=10.8.3` no ejecutaba
|
|
124
|
+
// el sensor: quedaba `inconclusive`, el veredicto global caia a `not_certified` y
|
|
125
|
+
// la admision desatendida bloqueaba. No se puede pinear el gestor de paquetes que
|
|
126
|
+
// viene con Node, asi que eso bloqueaba en cualquier maquina sin que nada estuviera
|
|
127
|
+
// mal — se saltaba la verificacion justamente donde mas hace falta.
|
|
128
|
+
//
|
|
129
|
+
// La diferencia no se pierde: viaja en `certification` hasta el resultado, asi que
|
|
130
|
+
// un veredicto producido por una version no congelada dice que lo fue. Lo que NO
|
|
131
|
+
// se ejecuta sigue sin ejecutarse: `incompatible` (fuera de rango), `missing-tool`
|
|
132
|
+
// (no esta), `unverifiable` (el probe no coincidio) y `not-applicable`.
|
|
133
|
+
if (!input.liveState || (input.liveState.state !== 'certified' && input.liveState.state !== 'compatible-unverified')) {
|
|
120
134
|
return v2Synthetic(input, input.liveState ? `${input.liveState.state}: ${input.liveState.reason}` : 'compatibility could not be revalidated');
|
|
135
|
+
}
|
|
121
136
|
if (input.liveState.variantId !== input.sensor.variantId)
|
|
122
137
|
return v2Synthetic(input, `variant-drift: manifest ${input.sensor.variantId}, live ${input.liveState.variantId ?? 'none'}; run \`awm sensors init\``);
|
|
123
138
|
const variant = input.liveSensor?.variants.find(candidate => candidate.id === input.sensor.variantId);
|
|
@@ -125,6 +140,7 @@ function prepareV2Sensor(input) {
|
|
|
125
140
|
return v2Synthetic(input, 'variant-drift: selected live variant has no command; run `awm sensors init`');
|
|
126
141
|
const common = {
|
|
127
142
|
name: input.name,
|
|
143
|
+
certification: input.liveState.state === 'certified' ? 'certified' : 'operational-unverified',
|
|
128
144
|
...(variant.formatter ? { formatter: variant.formatter } : {}),
|
|
129
145
|
...timeout(input.projectTimeout ?? input.sensor.timeout, input.packTimeout ?? input.liveSensor?.timeout, input.sensor.fast ?? input.liveSensor?.fast ?? false),
|
|
130
146
|
requestedScope,
|
|
@@ -77,6 +77,7 @@ function interpretResult(prepared, raw) {
|
|
|
77
77
|
const execution = executionEvidence(prepared, raw.elapsedMs);
|
|
78
78
|
const withEvidence = (result) => ({
|
|
79
79
|
...result,
|
|
80
|
+
...(prepared.certification ? { certification: prepared.certification } : {}),
|
|
80
81
|
...(prepared.effectiveScope === 'changed' ? { scope: 'changed' } : {}),
|
|
81
82
|
execution,
|
|
82
83
|
});
|
|
@@ -29,7 +29,6 @@ const project_1 = require("./project");
|
|
|
29
29
|
// Sensor JSON output can be several MB on large repos (e.g. `eslint --format json`
|
|
30
30
|
// with thousands of findings). A 1MB cap killed the child with SIGTERM when
|
|
31
31
|
// exceeded — which previously surfaced as a false "timeout".
|
|
32
|
-
const MAX_BUFFER = 64 * 1024 * 1024;
|
|
33
32
|
/** Hard ceiling on parallel sensors: past this, they only contend for the same cores. */
|
|
34
33
|
const MAX_CONCURRENCY = 4;
|
|
35
34
|
const READ_ONLY_MAX_ENTRIES = 250_000;
|
|
@@ -5,6 +5,9 @@ exports.routingFacts = routingFacts;
|
|
|
5
5
|
exports.defaultDispatchAdmission = defaultDispatchAdmission;
|
|
6
6
|
exports.admissionForConfig = admissionForConfig;
|
|
7
7
|
exports.runSupervisorLoop = runSupervisorLoop;
|
|
8
|
+
// Loop foreground (R4.4/R4.5): tick = apply -> collect/spawn -> stall -> gate.
|
|
9
|
+
// COMPLETE exige gate verde (que exige cero vivos): drenaje ANTES de declarar.
|
|
10
|
+
// Custodia BLOCKED: el loop sigue, el lock NO se libera, nada se mata.
|
|
8
11
|
const store_1 = require("../../core/journal/store");
|
|
9
12
|
const fingerprint_1 = require("../../core/journal/fingerprint");
|
|
10
13
|
const validate_1 = require("../../core/plan/validate");
|
|
@@ -66,18 +66,35 @@ function routingForecast(plan, routing) {
|
|
|
66
66
|
throw new Error('routing controllerCount must be 0 or 1');
|
|
67
67
|
return { kind: 'routing-v1', slices: plan.manifest.slices.length, implementerProfiles: profiles, roles: { 'specification-reviewer': plan.manifest.slices.length, 'code-quality-reviewer': plan.manifest.slices.length, 'final-reviewer': 1, 'track-a-qa': 1, documentation: 1, retro: 1, finishing: 1 }, trackB: qa === undefined ? { state: 'unavailable', lowerBound: 1 } : { state: 'known', count: qa.length }, controller: routing.controllerCount === undefined ? { state: 'unavailable', lowerBound: 0 } : { state: 'known', count: routing.controllerCount } };
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* El gate mide los CONTRATOS CONSUMIDOS: los registries de donde salen las skills
|
|
71
|
+
* y los packs que el plan va a ejecutar. Uno atrasado sí invalida el ciclo, porque
|
|
72
|
+
* el plan se validó contra otra versión de ese contrato.
|
|
73
|
+
*
|
|
74
|
+
* El CLI propio queda FUERA del bloqueo y se reporta en `cliCurrentness`. Estaba
|
|
75
|
+
* adentro, y como cada merge a `main` publica, cualquier máquina quedaba bloqueada
|
|
76
|
+
* minutos después de un release: `ADMISSION_CURRENTNESS_BLOCKED: stale: cli` con
|
|
77
|
+
* todo lo demás en verde. Eso no protegía nada — el binario que corre es el que
|
|
78
|
+
* corre, y que npm tenga uno más nuevo no cambia lo que este proceso ya es — y
|
|
79
|
+
* convertía un aviso ("hay update") en una detención del ciclo. El aviso sigue
|
|
80
|
+
* existiendo: lo imprime el update-check y ahora viaja también en el reporte.
|
|
81
|
+
*/
|
|
69
82
|
function currentness(report, consumedRegistryComponents) {
|
|
70
|
-
const consumed = new Set(
|
|
71
|
-
const present = new
|
|
83
|
+
const consumed = new Set(consumedRegistryComponents);
|
|
84
|
+
const present = new Map(report.components.map(component => [component.component, component.status]));
|
|
85
|
+
const cliStatus = present.get('cli');
|
|
86
|
+
const cli = cliStatus === 'current' ? 'current'
|
|
87
|
+
: cliStatus === 'stale' || cliStatus === 'pinned-behind' ? 'stale'
|
|
88
|
+
: 'unverifiable';
|
|
72
89
|
const missing = [...consumed].filter(component => !present.has(component));
|
|
73
90
|
if (missing.length > 0)
|
|
74
|
-
return { status: 'unverifiable', diagnostics: [diagnostic('ADMISSION_CURRENTNESS_MISSING_COMPONENT', `Currentness evidence is missing consumed components: ${missing.join(', ')}.`)] };
|
|
91
|
+
return { status: 'unverifiable', cli, diagnostics: [diagnostic('ADMISSION_CURRENTNESS_MISSING_COMPONENT', `Currentness evidence is missing consumed components: ${missing.join(', ')}.`)] };
|
|
75
92
|
const failed = report.components.filter(component => consumed.has(component.component) && component.status !== 'current');
|
|
76
93
|
if (failed.length === 0)
|
|
77
|
-
return { status: 'current', diagnostics: [] };
|
|
94
|
+
return { status: 'current', cli, diagnostics: [] };
|
|
78
95
|
const names = failed.map(component => component.component).join(', ');
|
|
79
96
|
const status = failed.some(component => component.status === 'unverifiable') ? 'unverifiable' : 'stale';
|
|
80
|
-
return { status, diagnostics: [diagnostic('ADMISSION_CURRENTNESS_BLOCKED', `Consumed contract currentness is ${status}: ${names}.`)] };
|
|
97
|
+
return { status, cli, diagnostics: [diagnostic('ADMISSION_CURRENTNESS_BLOCKED', `Consumed contract currentness is ${status}: ${names}.`)] };
|
|
81
98
|
}
|
|
82
99
|
function sensorVerdict(report) { return report.overall === 'not_certified' ? 'not-certified' : report.overall === 'pass' ? 'pass' : 'fail'; }
|
|
83
100
|
function journalStatus(input, plan) {
|
|
@@ -102,6 +119,7 @@ function journalStatus(input, plan) {
|
|
|
102
119
|
const PLAN_STATES = new Set(['valid', 'migration-required', 'invalid', 'unsupported']);
|
|
103
120
|
const JOURNALS = new Set(['not-required', 'current', 'missing', 'corrupt', 'stale']);
|
|
104
121
|
const CURRENTNESS = new Set(['current', 'stale', 'unverifiable', 'not-checked']);
|
|
122
|
+
const CLI_CURRENTNESS = new Set(['current', 'stale', 'unverifiable', 'not-checked']);
|
|
105
123
|
const SENSORS = new Set(['pass', 'fail', 'not-certified', 'not-required']);
|
|
106
124
|
const CAPABILITY = new Set(['supported', 'unsupported', 'unverified']);
|
|
107
125
|
function invalidAdmission() { throw new Error('admission returned an invalid report'); }
|
|
@@ -146,6 +164,8 @@ function sanitizeAdmissionReport(report) {
|
|
|
146
164
|
const value = report;
|
|
147
165
|
if ((value.state !== 'admitted' && value.state !== 'blocked') || !PLAN_STATES.has(value.planState) || !JOURNALS.has(value.journal) || !CURRENTNESS.has(value.currentness) || !SENSORS.has(value.sensors) || !validDiagnostics(value.diagnostics))
|
|
148
166
|
invalidAdmission();
|
|
167
|
+
if (value.cliCurrentness !== undefined && !CLI_CURRENTNESS.has(value.cliCurrentness))
|
|
168
|
+
invalidAdmission();
|
|
149
169
|
if (value.planDigest !== undefined && (typeof value.planDigest !== 'string' || !/^[a-f0-9]{64}$/.test(value.planDigest)))
|
|
150
170
|
invalidAdmission();
|
|
151
171
|
if (value.executionMode !== undefined && value.executionMode !== 'interactivo' && value.executionMode !== 'desatendido')
|
|
@@ -190,28 +210,30 @@ async function admitPlan(input) {
|
|
|
190
210
|
const mode = input.executionMode ?? (manifestMode === 'desatendido' ? 'desatendido' : 'interactivo');
|
|
191
211
|
if (mode !== 'interactivo' && mode !== 'desatendido')
|
|
192
212
|
return blocked(input, [diagnostic('ADMISSION_EXECUTION_MODE_INVALID', 'Execution mode must be interactivo or desatendido.')], { planDigest: plan.planDigest, provider, executionMode: 'interactivo' });
|
|
213
|
+
let cliCurrentness = 'not-checked';
|
|
193
214
|
if (input.requireCurrent) {
|
|
194
215
|
if (input.provenance !== 'proven')
|
|
195
216
|
return blocked(input, [diagnostic('ADMISSION_CURRENTNESS_PROVENANCE_REQUIRED', 'Consumed registry contracts cannot be proven from validated plan sources.')], { planDigest: plan.planDigest, provider, executionMode: mode, currentness: 'unverifiable' });
|
|
196
217
|
if (!input.currentness)
|
|
197
218
|
return blocked(input, [diagnostic('ADMISSION_CURRENTNESS_REQUIRED', 'Currentness evidence was required but not supplied.')], { planDigest: plan.planDigest, provider, executionMode: mode });
|
|
198
219
|
const gate = currentness(input.currentness, input.consumedRegistryComponents ?? []);
|
|
220
|
+
cliCurrentness = gate.cli;
|
|
199
221
|
if (gate.diagnostics.length)
|
|
200
|
-
return blocked(input, gate.diagnostics, { planDigest: plan.planDigest, provider, executionMode: mode, currentness: gate.status });
|
|
222
|
+
return blocked(input, gate.diagnostics, { planDigest: plan.planDigest, provider, executionMode: mode, currentness: gate.status, cliCurrentness });
|
|
201
223
|
if (input.compatibilityDiagnostics?.length)
|
|
202
|
-
return blocked(input, input.compatibilityDiagnostics, { planDigest: plan.planDigest, provider, executionMode: mode, currentness: 'unverifiable' });
|
|
224
|
+
return blocked(input, input.compatibilityDiagnostics, { planDigest: plan.planDigest, provider, executionMode: mode, currentness: 'unverifiable', cliCurrentness });
|
|
203
225
|
}
|
|
204
226
|
if (input.verifySensors) {
|
|
205
227
|
if (!input.sensors)
|
|
206
|
-
return blocked(input, [diagnostic('ADMISSION_SENSORS_REQUIRED', 'Sensor evidence was required but not supplied.')], { planDigest: plan.planDigest, provider, executionMode: mode, currentness: input.requireCurrent ? 'current' : 'not-checked' });
|
|
228
|
+
return blocked(input, [diagnostic('ADMISSION_SENSORS_REQUIRED', 'Sensor evidence was required but not supplied.')], { planDigest: plan.planDigest, provider, executionMode: mode, currentness: input.requireCurrent ? 'current' : 'not-checked', cliCurrentness });
|
|
207
229
|
const sensors = sensorVerdict(input.sensors);
|
|
208
230
|
if (sensors !== 'pass')
|
|
209
|
-
return blocked(input, [diagnostic('ADMISSION_SENSORS_BLOCKED', `Sensor verdict is ${sensors}.`)], { planDigest: plan.planDigest, provider, executionMode: mode, currentness: input.requireCurrent ? 'current' : 'not-checked', sensors });
|
|
231
|
+
return blocked(input, [diagnostic('ADMISSION_SENSORS_BLOCKED', `Sensor verdict is ${sensors}.`)], { planDigest: plan.planDigest, provider, executionMode: mode, currentness: input.requireCurrent ? 'current' : 'not-checked', cliCurrentness, sensors });
|
|
210
232
|
}
|
|
211
233
|
if (mode === 'desatendido') {
|
|
212
234
|
const journal = journalStatus(input, plan);
|
|
213
235
|
if (journal !== 'current')
|
|
214
|
-
return blocked(input, [diagnostic('ADMISSION_JOURNAL_BINDING_REQUIRED', 'Unattended execution requires a healthy schema-2 journal binding for this exact plan; run watch --init --plan.')], { planDigest: plan.planDigest, provider, executionMode: mode, journal, currentness: input.requireCurrent ? 'current' : 'not-checked', sensors: input.verifySensors ? 'pass' : 'not-required' });
|
|
236
|
+
return blocked(input, [diagnostic('ADMISSION_JOURNAL_BINDING_REQUIRED', 'Unattended execution requires a healthy schema-2 journal binding for this exact plan; run watch --init --plan.')], { planDigest: plan.planDigest, provider, executionMode: mode, journal, currentness: input.requireCurrent ? 'current' : 'not-checked', cliCurrentness, sensors: input.verifySensors ? 'pass' : 'not-required' });
|
|
215
237
|
const capabilities = unattendedCapabilities(provider, input.controllerAutonomy);
|
|
216
238
|
const resolution = { outcome: capabilities.unattendedController === 'supported' ? 'native' : 'blocked', provider, capabilities, evidenceVersion: 'r1-v1', diagnostics: [] };
|
|
217
239
|
if (resolution.outcome === 'blocked') {
|
|
@@ -222,38 +244,38 @@ async function admitPlan(input) {
|
|
|
222
244
|
const reason = exports.PROVIDER_EXECUTION_CAPABILITIES[provider].unattendedController !== 'supported'
|
|
223
245
|
? diagnostic('ADMISSION_CAPABILITY_UNVERIFIED', `Provider ${provider} has no verified unattended execution capability.`)
|
|
224
246
|
: diagnostic('ADMISSION_CONTROLLER_AUTONOMY_REQUIRED', `Unattended dispatch on ${provider} requires an explicit controller autonomy posture; without it the controller blocks on its first tool call.`);
|
|
225
|
-
return blocked(input, [reason], { planDigest: plan.planDigest, provider, executionMode: mode, journal, currentness: input.requireCurrent ? 'current' : 'not-checked', sensors: input.verifySensors ? 'pass' : 'not-required', capabilityResolution: resolution });
|
|
247
|
+
return blocked(input, [reason], { planDigest: plan.planDigest, provider, executionMode: mode, journal, currentness: input.requireCurrent ? 'current' : 'not-checked', cliCurrentness, sensors: input.verifySensors ? 'pass' : 'not-required', capabilityResolution: resolution });
|
|
226
248
|
}
|
|
227
|
-
return completeAdmission(input, plan, provider, mode, journal, input.requireCurrent ? 'current' : 'not-checked', input.verifySensors ? 'pass' : 'not-required', resolution);
|
|
249
|
+
return completeAdmission(input, plan, provider, mode, journal, input.requireCurrent ? 'current' : 'not-checked', input.verifySensors ? 'pass' : 'not-required', resolution, cliCurrentness);
|
|
228
250
|
}
|
|
229
251
|
const capabilities = exports.PROVIDER_EXECUTION_CAPABILITIES[provider];
|
|
230
252
|
const resolution = { outcome: capabilities.interactiveExecution === 'supported' ? 'native' : 'blocked', provider, capabilities, evidenceVersion: 'r1-v1', diagnostics: [] };
|
|
231
253
|
if (resolution.outcome === 'blocked')
|
|
232
254
|
return blocked(input, [diagnostic('ADMISSION_CAPABILITY_UNVERIFIED', `Provider ${provider} has no verified interactive execution capability.`)], { planDigest: plan.planDigest, provider, executionMode: mode, capabilityResolution: resolution });
|
|
233
|
-
return completeAdmission(input, plan, provider, mode, 'not-required', input.requireCurrent ? 'current' : 'not-checked', input.verifySensors ? 'pass' : 'not-required', resolution);
|
|
255
|
+
return completeAdmission(input, plan, provider, mode, 'not-required', input.requireCurrent ? 'current' : 'not-checked', input.verifySensors ? 'pass' : 'not-required', resolution, cliCurrentness);
|
|
234
256
|
}
|
|
235
|
-
function completeAdmission(input, plan, provider, executionMode, journal, currentness, sensors, capabilityResolution) {
|
|
257
|
+
function completeAdmission(input, plan, provider, executionMode, journal, currentness, sensors, capabilityResolution, cliCurrentness = 'not-checked') {
|
|
236
258
|
if (plan.schema === 'compact-slices/v1')
|
|
237
|
-
return { state: 'admitted', planState: 'valid', planDigest: plan.planDigest, provider, executionMode, journal, currentness, sensors, capabilityResolution, forecast: forecast(plan.manifest.slices.length), diagnostics: [] };
|
|
259
|
+
return { state: 'admitted', planState: 'valid', planDigest: plan.planDigest, provider, executionMode, journal, currentness, cliCurrentness, sensors, capabilityResolution, forecast: forecast(plan.manifest.slices.length), diagnostics: [] };
|
|
238
260
|
const routing = input.routing ?? input.routingReader?.();
|
|
239
261
|
if (!routing?.runtime || !routing.policy || !routing.capabilities)
|
|
240
|
-
return blocked(input, [diagnostic('ADMISSION_ROUTING_FACTS_REQUIRED', 'Compact v2 requires an approved policy, current capability receipt, and runtime identity.')], { planDigest: plan.planDigest, provider, executionMode, journal, currentness, sensors, capabilityResolution });
|
|
262
|
+
return blocked(input, [diagnostic('ADMISSION_ROUTING_FACTS_REQUIRED', 'Compact v2 requires an approved policy, current capability receipt, and runtime identity.')], { planDigest: plan.planDigest, provider, executionMode, journal, currentness, cliCurrentness, sensors, capabilityResolution });
|
|
241
263
|
let runtime;
|
|
242
264
|
try {
|
|
243
265
|
runtime = (0, capabilities_1.validateRuntimeKey)(routing.runtime);
|
|
244
266
|
}
|
|
245
267
|
catch {
|
|
246
|
-
return blocked(input, [diagnostic('ADMISSION_ROUTING_RUNTIME_INVALID', 'Compact v2 routing runtime identity is invalid.')], { planDigest: plan.planDigest, provider, executionMode, journal, currentness, sensors, capabilityResolution });
|
|
268
|
+
return blocked(input, [diagnostic('ADMISSION_ROUTING_RUNTIME_INVALID', 'Compact v2 routing runtime identity is invalid.')], { planDigest: plan.planDigest, provider, executionMode, journal, currentness, cliCurrentness, sensors, capabilityResolution });
|
|
247
269
|
}
|
|
248
270
|
if (runtime.target !== provider)
|
|
249
|
-
return blocked(input, [diagnostic('ADMISSION_ROUTING_PROVIDER_MISMATCH', 'Compact v2 routing runtime target must exactly match the admitted provider.')], { planDigest: plan.planDigest, provider, executionMode, journal, currentness, sensors, capabilityResolution });
|
|
271
|
+
return blocked(input, [diagnostic('ADMISSION_ROUTING_PROVIDER_MISMATCH', 'Compact v2 routing runtime target must exactly match the admitted provider.')], { planDigest: plan.planDigest, provider, executionMode, journal, currentness, cliCurrentness, sensors, capabilityResolution });
|
|
250
272
|
const now = routing.now ?? new Date();
|
|
251
273
|
for (const slice of plan.manifest.slices) {
|
|
252
274
|
const resolved = (0, resolve_1.resolveSelection)({ role: 'implementer', requestedProfile: slice.implementerProfile, policy: routing.policy, capabilities: routing.capabilities, runtime, now });
|
|
253
275
|
if (resolved.state === 'blocked')
|
|
254
|
-
return blocked(input, resolved.diagnostics, { planDigest: plan.planDigest, provider, executionMode, journal, currentness, sensors, capabilityResolution });
|
|
276
|
+
return blocked(input, resolved.diagnostics, { planDigest: plan.planDigest, provider, executionMode, journal, currentness, cliCurrentness, sensors, capabilityResolution });
|
|
255
277
|
}
|
|
256
|
-
return { state: 'admitted', planState: 'valid', planDigest: plan.planDigest, provider, executionMode, journal, currentness, sensors, capabilityResolution, forecast: forecast(plan.manifest.slices.length), routingForecast: routingForecast(plan, routing), diagnostics: [] };
|
|
278
|
+
return { state: 'admitted', planState: 'valid', planDigest: plan.planDigest, provider, executionMode, journal, currentness, cliCurrentness, sensors, capabilityResolution, forecast: forecast(plan.manifest.slices.length), routingForecast: routingForecast(plan, routing), diagnostics: [] };
|
|
257
279
|
}
|
|
258
280
|
// Structural exhaustiveness guard: adding an AgentTarget requires an explicit entry above.
|
|
259
281
|
const capabilityTargets = providers_1.AGENT_TARGETS;
|
|
@@ -50,7 +50,6 @@ function isSafeArtifactName(name) {
|
|
|
50
50
|
if (/[. ]$/.test(value))
|
|
51
51
|
return false;
|
|
52
52
|
// Bytes de control y NUL: truncan la ruta a nivel syscall en algunos SO.
|
|
53
|
-
// eslint-disable-next-line no-control-regex
|
|
54
53
|
if (/[\u0000-\u001f\u007f]/.test(value))
|
|
55
54
|
return false;
|
|
56
55
|
// Cualquier separador (de ambas plataformas) convierte esto en una ruta,
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.CLI_PACKAGE_NAME = void 0;
|
|
7
|
+
exports.packageRoot = packageRoot;
|
|
7
8
|
exports.cliVersion = cliVersion;
|
|
8
9
|
// src/core/cli-version.ts
|
|
9
10
|
//
|
|
@@ -12,19 +13,35 @@ exports.cliVersion = cliVersion;
|
|
|
12
13
|
const fs_1 = __importDefault(require("fs"));
|
|
13
14
|
const path_1 = __importDefault(require("path"));
|
|
14
15
|
exports.CLI_PACKAGE_NAME = 'agentic-workflow-manager';
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Directorio raíz del paquete del CLI — el que contiene SU package.json, no el
|
|
18
|
+
* del proyecto del usuario. Devuelve null si no aparece en la cadena de padres.
|
|
19
|
+
*/
|
|
20
|
+
function packageRoot(from = __dirname) {
|
|
21
|
+
let dir = from;
|
|
17
22
|
for (let i = 0; i < 6; i++) {
|
|
18
23
|
const p = path_1.default.join(dir, 'package.json');
|
|
19
24
|
if (fs_1.default.existsSync(p)) {
|
|
20
25
|
try {
|
|
21
26
|
const pkg = JSON.parse(fs_1.default.readFileSync(p, 'utf-8'));
|
|
22
|
-
if (pkg.name === exports.CLI_PACKAGE_NAME
|
|
23
|
-
return
|
|
27
|
+
if (pkg.name === exports.CLI_PACKAGE_NAME)
|
|
28
|
+
return dir;
|
|
24
29
|
}
|
|
25
30
|
catch { /* package.json ajeno o ilegible — seguir subiendo */ }
|
|
26
31
|
}
|
|
27
32
|
dir = path_1.default.dirname(dir);
|
|
28
33
|
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
function cliVersion() {
|
|
37
|
+
const root = packageRoot();
|
|
38
|
+
if (!root)
|
|
39
|
+
return '0.0.0';
|
|
40
|
+
try {
|
|
41
|
+
const pkg = JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, 'package.json'), 'utf-8'));
|
|
42
|
+
if (typeof pkg.version === 'string')
|
|
43
|
+
return pkg.version;
|
|
44
|
+
}
|
|
45
|
+
catch { /* ilegible entre el walk y la lectura */ }
|
|
29
46
|
return '0.0.0';
|
|
30
47
|
}
|
|
@@ -14,13 +14,6 @@ function sectionHtml(section, supplement = '') {
|
|
|
14
14
|
const rows = section.items.length === 0 ? '<p class="empty">No observations reported.</p>' : `<table><thead><tr><th scope="col">Observation</th><th scope="col">State</th><th scope="col">Detail</th><th scope="col">Remediation</th></tr></thead><tbody>${section.items.map((item) => `<tr><td data-label="Observation">${escapeHtml(item.label)}</td><td data-label="State"><span class="state ${item.state}">${STATE_GLYPH[item.state]} ${STATE_TEXT[item.state]}</span></td><td data-label="Detail">${item.detail ? escapeHtml(item.detail) : '—'}</td><td data-label="Remediation">${item.remediation ? `<code>${escapeHtml(item.remediation)}</code>` : '—'}</td></tr>`).join('')}</tbody></table>`;
|
|
15
15
|
return `<section id="${section.id}" aria-label="${title}"><header><h2>${title}</h2><span class="eyebrow">${escapeHtml(section.availability.replace('_', ' '))}</span></header><div class="section-body">${availability}${supplement}${rows}</div></section>`;
|
|
16
16
|
}
|
|
17
|
-
function diagnosticCards(items, attribute) {
|
|
18
|
-
const labels = ['installation', 'sensors', 'permissions'];
|
|
19
|
-
const cards = items.slice(0, 3).map((item, index) => `<li data-diagnostic-card="${labels[index] ?? 'diagnostic'}"><span class="state ${item.state}">${STATE_GLYPH[item.state]} ${STATE_TEXT[item.state]}</span><strong>${escapeHtml(item.label)}</strong><span>${item.detail ? escapeHtml(item.detail) : 'No additional detail'}</span></li>`).join('') || '<li><span class="state not_applicable">— Not applicable</span><strong>No machine observations</strong><span>Machine diagnostics are not available.</span></li>';
|
|
20
|
-
if (attribute === 'data-machine-diagnostics')
|
|
21
|
-
return `<section data-machine-diagnostics aria-labelledby="machine-diagnostics-heading"><header><h2 id="machine-diagnostics-heading">Machine diagnostics</h2></header><div class="section-body"><ul class="diagnostic-grid">${cards}</ul></div></section>`;
|
|
22
|
-
return `<div class="machine-preparation-strip" data-machine-preparation role="group" aria-labelledby="machine-preparation-heading"><h3 id="machine-preparation-heading">Preparación de máquina</h3><ul class="diagnostic-grid">${cards}</ul></div>`;
|
|
23
|
-
}
|
|
24
17
|
function machineBento(items) {
|
|
25
18
|
const names = ['Instalación', 'Sensores globales', 'Persistencia'];
|
|
26
19
|
return `<div class="machine-bento" data-machine-bento>${names.map((name, index) => {
|
|
@@ -136,7 +136,6 @@ function scanProjectLedgers(projectRoot, overrides = {}) {
|
|
|
136
136
|
throw new Error('ledger archive directory escapes project root');
|
|
137
137
|
directories.push({ directory: archive, archive: true });
|
|
138
138
|
}
|
|
139
|
-
let filesSeen = 0;
|
|
140
139
|
let linesSeen = 0;
|
|
141
140
|
let entryLimitReached = false;
|
|
142
141
|
let omittedEvidenceRefs = 0;
|
|
@@ -145,7 +144,6 @@ function scanProjectLedgers(projectRoot, overrides = {}) {
|
|
|
145
144
|
for (const { isArchive, target } of collected.candidates.slice(0, limits.maxFiles)) {
|
|
146
145
|
if (entryLimitReached)
|
|
147
146
|
break;
|
|
148
|
-
filesSeen += 1;
|
|
149
147
|
const stat = fs_1.default.lstatSync(target);
|
|
150
148
|
if (stat.isSymbolicLink()) {
|
|
151
149
|
skip('symlink-entry');
|
package/dist/src/core/miro.js
CHANGED
|
@@ -85,7 +85,6 @@ function computeLayout(storyMap) {
|
|
|
85
85
|
let groupLeftX = PADDING; // running X from frame left edge
|
|
86
86
|
for (let ai = 0; ai < activities.length; ai++) {
|
|
87
87
|
const activity = activities[ai];
|
|
88
|
-
const numTaskCols = Math.max(1, activity.tasks.length);
|
|
89
88
|
const groupWidth = activityGroupWidths[ai];
|
|
90
89
|
// Activity card — spans its task columns
|
|
91
90
|
const activityX = frameLeft + groupLeftX + groupWidth / 2;
|
package/dist/src/core/profile.js
CHANGED
|
@@ -27,7 +27,6 @@ function findProjectRoot(startDir) {
|
|
|
27
27
|
catch {
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
|
-
// eslint-disable-next-line no-constant-condition
|
|
31
30
|
while (true) {
|
|
32
31
|
if (fs_1.default.existsSync(path_1.default.join(dir, '.git')) ||
|
|
33
32
|
fs_1.default.existsSync(path_1.default.join(dir, 'package.json')) ||
|
package/dist/src/core/text.js
CHANGED
|
@@ -14,13 +14,13 @@ exports.sanitizeDeclaredField = sanitizeDeclaredField;
|
|
|
14
14
|
* `AGENTS.md` documenta bajo `defensive-guard-consistency`: endurecer una copia
|
|
15
15
|
* y dejar la otra atrás. */
|
|
16
16
|
function stripControlChars(text) {
|
|
17
|
-
//
|
|
17
|
+
// Necesitamos matchear C0 deliberadamente.
|
|
18
18
|
return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
|
|
19
19
|
}
|
|
20
20
|
/** Keeps an untrusted diagnostic field on one terminal line, without changing
|
|
21
21
|
* the whitespace policy for multiline public text or composed markdown. */
|
|
22
22
|
function sanitizeDiagnosticText(text) {
|
|
23
|
-
//
|
|
23
|
+
// Remove all C0/C1 controls, including terminal escape bytes.
|
|
24
24
|
return text.replace(/\r\n|[\r\n\t\u2028\u2029]/g, ' ').replace(/[\x00-\x1f\x7f-\x9f]/g, '');
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.decideTeardown = void 0;
|
|
4
4
|
exports.worktreeOwnershipProven = worktreeOwnershipProven;
|
|
5
|
+
// Task 13 (R4.2/R4.3/R4.6/R4.10/C2/C9): `decideTeardown` YA vive en
|
|
6
|
+
// `protocol.ts` (autoridad única de decisión) — este módulo NO crea una
|
|
7
|
+
// segunda copia (mismo criterio que `join.ts` con `decideJoinReconciliation`,
|
|
8
|
+
// T11). `teardown.test.ts` prueba la matriz completa IMPORTANDO este
|
|
9
|
+
// re-export, así que cualquier fix que descubra tiene que vivir en
|
|
10
|
+
// `protocol.ts` (y volver a correr la exploración de T1 en el mismo commit)
|
|
11
|
+
// para que el gate siga significando algo.
|
|
12
|
+
//
|
|
13
|
+
// Lo que SÍ aporta este archivo es la única prueba de OWNERSHIP real que
|
|
14
|
+
// `tracks.ts` necesita antes de siquiera considerar `remove-owned-worktree`
|
|
15
|
+
// como decisión segura: cuatro condiciones simultáneas (R4.10) —
|
|
16
|
+
// 1. `teardownIntent.worktreePath` (snapshot durable tomado en
|
|
17
|
+
// `TEARDOWN_REQUESTED` -> `TEARDOWN_INTENT`) resuelve al MISMO target
|
|
18
|
+
// canónico que `ref.worktreePath` (el dato vivo del journal).
|
|
19
|
+
// 2. `teardownIntent.branch === ref.branch`.
|
|
20
|
+
// 3. `.awm/track.json` del worktree (`readDescriptor`) coincide en
|
|
21
|
+
// `planJournalId`/`trackId`/`fencingToken` — la identidad completa que
|
|
22
|
+
// el propio track escribió al armarse (Task 9), nunca reconstruida a
|
|
23
|
+
// mano.
|
|
24
|
+
// 4. `git worktree list --porcelain` asocia ESE path exactamente con la
|
|
25
|
+
// branch determinista del track (`ownedWorktreeExists`, ya construida
|
|
26
|
+
// en Task 9 sobre el mismo criterio).
|
|
27
|
+
// Cualquier chequeo indemostrable (no existe, no legible, mismatch, fallo de
|
|
28
|
+
// git) es "ownership NO probado" — fail-closed (R4.6): el caller nunca
|
|
29
|
+
// adopta ni borra lo que no pudo probar.
|
|
5
30
|
const descriptor_1 = require("./descriptor");
|
|
6
31
|
const paths_1 = require("../paths");
|
|
7
32
|
const git_1 = require("./git");
|
|
@@ -66,7 +66,7 @@ describe('awm context orchestrators', () => {
|
|
|
66
66
|
it('sanea bytes de control antes de escribir a la terminal', () => {
|
|
67
67
|
const hostil = { name: 'x\x1b[31m', appliesWhen: 'w\x07', terminatesTo: 't\x00' };
|
|
68
68
|
const r = (0, context_1.runContextOrchestrators)(collected([hostil]), { json: false });
|
|
69
|
-
//
|
|
69
|
+
// Verificamos la ausencia deliberada de C0.
|
|
70
70
|
expect(r.stdout).not.toMatch(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/);
|
|
71
71
|
});
|
|
72
72
|
});
|
|
@@ -180,7 +180,7 @@ describe('multi-agent targeting (add/remove/sync/update/doctor)', () => {
|
|
|
180
180
|
fs_1.default.writeFileSync(path_1.default.join(process.env.AWM_HOME, 'registries.json'), JSON.stringify([{ name: 'baseline', remote: 'https://example.com/baseline.git' }], null, 2));
|
|
181
181
|
const { runUpdateCore } = require('../../src/commands/update');
|
|
182
182
|
const syncRegistries = jest.fn(async () => [SYNCED_REGISTRY]);
|
|
183
|
-
const planReconciliation = jest.fn((
|
|
183
|
+
const planReconciliation = jest.fn((_operations) => ({ operations: [], records: [], reports: [] }));
|
|
184
184
|
const applyInstallPlan = jest.fn(() => ({ installed: [], skipped: [], transactionId: 'tx', modifiedFiles: [] }));
|
|
185
185
|
const resyncInstalledHooks = jest.fn(() => []);
|
|
186
186
|
const outcome = await runUpdateCore({}, {
|
|
@@ -637,7 +637,11 @@ describe('plan admit Commander wiring', () => {
|
|
|
637
637
|
process.exitCode = undefined;
|
|
638
638
|
}
|
|
639
639
|
});
|
|
640
|
-
|
|
640
|
+
// Antes esta prueba afirmaba que un CLI stale bloqueaba y por eso los sensores no
|
|
641
|
+
// corrian. Eso era el defecto: cada publicacion de npm detenia cualquier maquina.
|
|
642
|
+
// La propiedad de orden —si el gate de contratos bloquea, los sensores no corren—
|
|
643
|
+
// sigue cubierta arriba con un registry consumido stale.
|
|
644
|
+
it('con el CLI stale admite y SI corre los sensores: la version publicada en npm no detiene el ciclo', async () => {
|
|
641
645
|
const output = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
642
646
|
const sensors = jest.fn();
|
|
643
647
|
const program = new commander_1.Command();
|
|
@@ -651,8 +655,8 @@ describe('plan admit Commander wiring', () => {
|
|
|
651
655
|
});
|
|
652
656
|
try {
|
|
653
657
|
await program.parseAsync(['node', 'awm', 'plan', 'admit', 'plan.md', '--provider', 'codex', '--cwd', repositoryRoot, '--require-current', '--verify-sensors', '--json']);
|
|
654
|
-
expect(sensors).
|
|
655
|
-
expect(JSON.parse(String(output.mock.calls[0][0]))).toMatchObject({
|
|
658
|
+
expect(sensors).toHaveBeenCalled();
|
|
659
|
+
expect(JSON.parse(String(output.mock.calls[0][0]))).toMatchObject({ currentness: 'current', cliCurrentness: 'stale' });
|
|
656
660
|
}
|
|
657
661
|
finally {
|
|
658
662
|
output.mockRestore();
|
|
@@ -25,7 +25,6 @@ jest.mock('../../../src/commands/sensors/run', () => ({
|
|
|
25
25
|
jest.mock('../../../src/core/currentness/check', () => ({
|
|
26
26
|
checkCurrentness: jest.fn(),
|
|
27
27
|
}));
|
|
28
|
-
const mockExecSync = child_process_1.execSync;
|
|
29
28
|
const mockRunSensors = run_1.runSensors;
|
|
30
29
|
const mockCheckCurrentness = check_1.checkCurrentness;
|
|
31
30
|
/** Turn a tmpdir into a real git repo with (optionally) an `origin` remote. */
|
|
@@ -79,7 +79,7 @@ describe('awm process show (texto)', () => {
|
|
|
79
79
|
});
|
|
80
80
|
const r = (0, process_1.runProcessShow)({ models: [hostile], diagnostics: [] }, 'mi-proceso', false);
|
|
81
81
|
expect(r.code).toBe(0);
|
|
82
|
-
//
|
|
82
|
+
// Necesitamos verificar la ausencia deliberada de C0.
|
|
83
83
|
expect(r.stdout).not.toMatch(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/);
|
|
84
84
|
expect(r.stdout).toContain('Objetivo[31mmalicioso[0m.');
|
|
85
85
|
expect(r.stdout).toContain('Uno]0;pwned');
|
|
@@ -31,6 +31,69 @@ describe('discoverProjectEvidence', () => {
|
|
|
31
31
|
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
32
32
|
}
|
|
33
33
|
});
|
|
34
|
+
// #147: el gestor de paquetes en PATH era invisible sin lockfile, asi que el sensor
|
|
35
|
+
// `test` del pack js-ts reportaba tool-not-found con `npm test` ejecutable, y en
|
|
36
|
+
// desatendido eso bloquea la admision por sensores.
|
|
37
|
+
const packageManagerPack = {
|
|
38
|
+
schemaVersion: 2,
|
|
39
|
+
name: 'js-ts',
|
|
40
|
+
detects: ['package.json'],
|
|
41
|
+
sensors: {
|
|
42
|
+
test: {
|
|
43
|
+
applicability: { allFiles: ['package.json'] },
|
|
44
|
+
variants: [
|
|
45
|
+
{ id: 'npm-script', requirements: { tool: 'npm', configFiles: [] }, command: { executable: 'npm', resolution: 'path', packageManager: 'npm' } },
|
|
46
|
+
{ id: 'pnpm-script', requirements: { tool: 'pnpm', configFiles: [] }, command: { executable: 'pnpm', resolution: 'path', packageManager: 'pnpm' } },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
it('probes the package managers on PATH when the project declares none', () => {
|
|
52
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-discovery-pm-undeclared-'));
|
|
53
|
+
try {
|
|
54
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), JSON.stringify({ scripts: { test: 'node -e ""' } }));
|
|
55
|
+
const evidence = (0, discovery_1.discoverProjectEvidence)(root, packageManagerPack, { pathToolVersion: tool => (tool === 'npm' ? '10.9.7' : null) });
|
|
56
|
+
expect(evidence.packageManager).toBeNull();
|
|
57
|
+
expect(evidence.toolVersions.npm).toBe('10.9.7');
|
|
58
|
+
expect(evidence.toolProvenance.npm).toBe('path');
|
|
59
|
+
// pnpm no esta en PATH: se sondeo y no se encontro, que es un hecho medido.
|
|
60
|
+
expect(evidence.toolVersions.pnpm).toBeNull();
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
it('probes only the declared package manager, so a pnpm project never resolves through npm', () => {
|
|
67
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-discovery-pm-declared-'));
|
|
68
|
+
try {
|
|
69
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), JSON.stringify({ scripts: { test: 'node -e ""' } }));
|
|
70
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'pnpm-lock.yaml'), 'lockfileVersion: 9');
|
|
71
|
+
const probed = [];
|
|
72
|
+
const evidence = (0, discovery_1.discoverProjectEvidence)(root, packageManagerPack, { pathToolVersion: tool => { probed.push(tool); return '10.9.7'; } });
|
|
73
|
+
expect(evidence.packageManager).toBe('pnpm');
|
|
74
|
+
expect(probed).toEqual(['pnpm']);
|
|
75
|
+
expect(evidence.toolVersions.npm).toBeNull();
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
it('probes nothing when the lockfiles conflict: an undecided project is not an undeclared one', () => {
|
|
82
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-discovery-pm-conflict-'));
|
|
83
|
+
try {
|
|
84
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), JSON.stringify({ scripts: { test: 'node -e ""' } }));
|
|
85
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'package-lock.json'), '{}');
|
|
86
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'pnpm-lock.yaml'), 'lockfileVersion: 9');
|
|
87
|
+
const probed = [];
|
|
88
|
+
const evidence = (0, discovery_1.discoverProjectEvidence)(root, packageManagerPack, { pathToolVersion: tool => { probed.push(tool); return '10.9.7'; } });
|
|
89
|
+
expect(evidence.packageManagerConflict).toBe(true);
|
|
90
|
+
expect(probed).toEqual([]);
|
|
91
|
+
expect(evidence.toolVersions.npm).toBeNull();
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
95
|
+
}
|
|
96
|
+
});
|
|
34
97
|
it('includes pack and applicability markers when deciding whether a sensor applies', () => {
|
|
35
98
|
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-discovery-markers-'));
|
|
36
99
|
try {
|
|
@@ -455,3 +455,29 @@ onPosix('runStructuredCommand — Python environments', () => {
|
|
|
455
455
|
}
|
|
456
456
|
});
|
|
457
457
|
});
|
|
458
|
+
// Ejecutar con una version que el registry no congelo es correcto — el sensor mide de
|
|
459
|
+
// verdad — pero el resultado no puede presentarse como si la version estuviera
|
|
460
|
+
// certificada. La distincion viaja hasta el veredicto o se pierde en silencio.
|
|
461
|
+
const raw = (overrides = {}) => ({
|
|
462
|
+
stdout: '', stderr: '', code: 0, signal: null, timedOut: false, overflowed: false, elapsedMs: 5, ...overrides,
|
|
463
|
+
});
|
|
464
|
+
describe('interpretResult: procedencia de la version que produjo el veredicto', () => {
|
|
465
|
+
it('propaga operational-unverified a un pass', () => {
|
|
466
|
+
const result = (0, result_1.interpretResult)(sensor({ certification: 'operational-unverified' }), raw({ code: 0, elapsedMs: 12 }));
|
|
467
|
+
expect(result.status).toBe('pass');
|
|
468
|
+
expect(result.certification).toBe('operational-unverified');
|
|
469
|
+
});
|
|
470
|
+
it('propaga certified cuando corrio la version congelada', () => {
|
|
471
|
+
const result = (0, result_1.interpretResult)(sensor({ certification: 'certified' }), raw({ code: 0, elapsedMs: 12 }));
|
|
472
|
+
expect(result.certification).toBe('certified');
|
|
473
|
+
});
|
|
474
|
+
it('propaga la procedencia tambien cuando el sensor falla', () => {
|
|
475
|
+
const result = (0, result_1.interpretResult)(sensor({ certification: 'operational-unverified' }), raw({ code: 1, stdout: 'boom', elapsedMs: 9 }));
|
|
476
|
+
expect(result.status).not.toBe('pass');
|
|
477
|
+
expect(result.certification).toBe('operational-unverified');
|
|
478
|
+
});
|
|
479
|
+
it('no inventa procedencia cuando la preparacion no la declara', () => {
|
|
480
|
+
const result = (0, result_1.interpretResult)(sensor(), raw({ code: 0, elapsedMs: 3 }));
|
|
481
|
+
expect(result.certification).toBeUndefined();
|
|
482
|
+
});
|
|
483
|
+
});
|
|
@@ -86,3 +86,37 @@ describe('validateRunOptions', () => {
|
|
|
86
86
|
.toThrow(/refusing to combine --changed with a baseline capture/);
|
|
87
87
|
});
|
|
88
88
|
});
|
|
89
|
+
// El bloqueo que esto arregla: `compatible-unverified` significa que la herramienta esta,
|
|
90
|
+
// cae en el rango operativo declarado por el pack y su probe coincidio — solo la version
|
|
91
|
+
// exacta no es la que el registry congelo. Saltearla dejaba el veredicto en
|
|
92
|
+
// `not_certified` y la admision desatendida bloqueada en cualquier maquina cuya version
|
|
93
|
+
// no fuera el pin, que para el gestor que viene con Node es siempre.
|
|
94
|
+
describe('prepareV2Sensor: que versiones ejecutan', () => {
|
|
95
|
+
const unverified = { state: 'compatible-unverified', reason: 'operational-range-and-probe', variantId: 'eslint-live', toolVersion: '10.9.7', runtimeVersion: '22.0.0', certifiedRange: '=10.8.3', evidence: [] };
|
|
96
|
+
it('ejecuta una version operativa que el registry no congelo, y lo declara', () => {
|
|
97
|
+
const prepared = (0, prepare_1.prepareV2Sensor)(v2Input({ liveState: unverified, requestedScope: 'full' }));
|
|
98
|
+
expect(prepared.syntheticStatus).toBeUndefined();
|
|
99
|
+
expect(prepared.command).toEqual({ kind: 'structured', value: { executable: 'live-eslint', resolution: 'path', args: ['.'] } });
|
|
100
|
+
expect(prepared.certification).toBe('operational-unverified');
|
|
101
|
+
});
|
|
102
|
+
it('marca como certificada la ejecucion con la version congelada', () => {
|
|
103
|
+
const prepared = (0, prepare_1.prepareV2Sensor)(v2Input({ requestedScope: 'full' }));
|
|
104
|
+
expect(prepared.syntheticStatus).toBeUndefined();
|
|
105
|
+
expect(prepared.certification).toBe('certified');
|
|
106
|
+
});
|
|
107
|
+
it.each([
|
|
108
|
+
['incompatible', 'no-operational-variant'],
|
|
109
|
+
['missing-tool', 'tool-not-found'],
|
|
110
|
+
['unverifiable', 'probe-not-matched'],
|
|
111
|
+
['not-applicable', 'sensor-not-applicable'],
|
|
112
|
+
])('no ejecuta un sensor %s: sin herramienta o sin probe no hay nada que medir', (state, reason) => {
|
|
113
|
+
const prepared = (0, prepare_1.prepareV2Sensor)(v2Input({
|
|
114
|
+
liveState: { ...unverified, state, reason },
|
|
115
|
+
requestedScope: 'full',
|
|
116
|
+
}));
|
|
117
|
+
expect(prepared.syntheticStatus).toBe('inconclusive');
|
|
118
|
+
expect(prepared.syntheticReason).toBe(`${state}: ${reason}`);
|
|
119
|
+
expect(prepared.command).toBeUndefined();
|
|
120
|
+
expect(prepared.certification).toBeUndefined();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -25,9 +25,6 @@ class ExitSignal extends Error {
|
|
|
25
25
|
this.code = code;
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
-
function git(repo, args) {
|
|
29
|
-
return (0, child_process_1.execFileSync)('git', args, { cwd: repo, encoding: 'utf8' }).trim();
|
|
30
|
-
}
|
|
31
28
|
function gitInit(repo, branch) {
|
|
32
29
|
(0, child_process_1.execFileSync)('git', ['-c', 'user.email=t@t.t', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', 'init', '-q', '-b', branch], { cwd: repo });
|
|
33
30
|
fs_1.default.writeFileSync(path_1.default.join(repo, 'f.txt'), 'x');
|
|
@@ -126,7 +126,7 @@ describe('aplicacion transaccional de requests', () => {
|
|
|
126
126
|
expect(Object.keys((0, store_1.readJournal)(repo, 'rama').state.jobs)).toHaveLength(1); // sin duplicados
|
|
127
127
|
});
|
|
128
128
|
test('register-entity CREA task con VerificationPlan y ReviewObligations; cycle-plan y dispatch (R1.4/R1.4b)', () => {
|
|
129
|
-
|
|
129
|
+
(0, requests_1.emitRequest)(repo, 'rama', {
|
|
130
130
|
kind: 'register-entity', generationToken: 'g1', idempotencyKey: 'e1',
|
|
131
131
|
payload: {
|
|
132
132
|
entity: 'task', taskId: 'T1', title: 'implementar',
|
|
@@ -170,3 +170,50 @@ describe('admitPlan', () => {
|
|
|
170
170
|
});
|
|
171
171
|
});
|
|
172
172
|
});
|
|
173
|
+
// Un CLI atrasado detenia el ciclo entero: como cada merge a main publica, cualquier
|
|
174
|
+
// maquina quedaba bloqueada minutos despues de un release con todo lo demas en verde.
|
|
175
|
+
// El gate ahora mide los contratos consumidos (los registries, de donde salen las
|
|
176
|
+
// skills y los packs que el plan ejecuta) y reporta el CLI sin bloquear.
|
|
177
|
+
describe('admitPlan: el CLI atrasado avisa, los contratos consumidos bloquean', () => {
|
|
178
|
+
const component = (name, status) => ({
|
|
179
|
+
component: name, installed: '1.0.0', latest: status === 'current' ? '1.0.0' : '2.0.0',
|
|
180
|
+
channel: 'stable', source: 'npm', checkedAt: 'x', status, detail: 'd', remedy: 'r',
|
|
181
|
+
});
|
|
182
|
+
const admit = (components, consumed = ['registry:baseline']) => (0, admission_1.admitPlan)({
|
|
183
|
+
plan: valid, provider: 'codex', cwd: process.cwd(), enabledAgents: ['codex'],
|
|
184
|
+
requireCurrent: true, provenance: 'proven', consumedRegistryComponents: consumed,
|
|
185
|
+
currentness: { checkedAt: 'x', compatibility: { status: 'not-checked' }, components },
|
|
186
|
+
});
|
|
187
|
+
it('admite con el CLI stale cuando el registry consumido esta current', async () => {
|
|
188
|
+
const report = await admit([component('cli', 'stale'), component('registry:baseline', 'current')]);
|
|
189
|
+
expect(report.state).toBe('admitted');
|
|
190
|
+
expect(report.currentness).toBe('current');
|
|
191
|
+
expect(report.cliCurrentness).toBe('stale');
|
|
192
|
+
});
|
|
193
|
+
it('admite con el CLI inverificable: no haber podido consultar npm no es un defecto del ciclo', async () => {
|
|
194
|
+
const report = await admit([component('cli', 'unverifiable'), component('registry:baseline', 'current')]);
|
|
195
|
+
expect(report.state).toBe('admitted');
|
|
196
|
+
expect(report.cliCurrentness).toBe('unverifiable');
|
|
197
|
+
});
|
|
198
|
+
it('bloquea con un registry consumido stale, y ahi el CLI no lo salva', async () => {
|
|
199
|
+
const report = await admit([component('cli', 'current'), component('registry:baseline', 'stale')]);
|
|
200
|
+
expect(report.state).toBe('blocked');
|
|
201
|
+
expect(report.currentness).toBe('stale');
|
|
202
|
+
expect(report.diagnostics[0].code).toBe('ADMISSION_CURRENTNESS_BLOCKED');
|
|
203
|
+
expect(report.cliCurrentness).toBe('current');
|
|
204
|
+
});
|
|
205
|
+
it('bloquea cuando falta la evidencia de un registry consumido', async () => {
|
|
206
|
+
const report = await admit([component('cli', 'current')]);
|
|
207
|
+
expect(report.state).toBe('blocked');
|
|
208
|
+
expect(report.diagnostics[0].code).toBe('ADMISSION_CURRENTNESS_MISSING_COMPONENT');
|
|
209
|
+
});
|
|
210
|
+
it('reporta el CLI tambien cuando el bloqueo lo produjo otro gate', async () => {
|
|
211
|
+
const report = await (0, admission_1.admitPlan)({
|
|
212
|
+
plan: valid, provider: 'codex', cwd: process.cwd(), enabledAgents: ['codex'],
|
|
213
|
+
requireCurrent: true, provenance: 'proven', consumedRegistryComponents: [],
|
|
214
|
+
currentness: { checkedAt: 'x', compatibility: { status: 'not-checked' }, components: [component('cli', 'stale')] },
|
|
215
|
+
verifySensors: true, sensors: { overall: 'fail', sensors: [] },
|
|
216
|
+
});
|
|
217
|
+
expect(report).toMatchObject({ state: 'blocked', sensors: 'fail', cliCurrentness: 'stale' });
|
|
218
|
+
});
|
|
219
|
+
});
|
|
@@ -201,7 +201,7 @@ describe('composedOrchestrators', () => {
|
|
|
201
201
|
{ name: 'a\x1bx', appliesWhen: 'w\x07', terminatesTo: 't\x00' },
|
|
202
202
|
]);
|
|
203
203
|
expect(out[0].name).toBe('ax');
|
|
204
|
-
//
|
|
204
|
+
// Verificamos la ausencia deliberada de C0.
|
|
205
205
|
const controlCharPattern = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/;
|
|
206
206
|
expect(out[0].name).not.toMatch(controlCharPattern);
|
|
207
207
|
expect(out[0].appliesWhen).not.toMatch(controlCharPattern);
|
|
@@ -216,7 +216,7 @@ describe('composedOrchestrators', () => {
|
|
|
216
216
|
{ name: 'evil\x1b[31m', appliesWhen: 'w', terminatesTo: 't' },
|
|
217
217
|
],
|
|
218
218
|
});
|
|
219
|
-
//
|
|
219
|
+
// Verificamos la ausencia deliberada de C0.
|
|
220
220
|
expect(ctx.markdown).not.toMatch(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/);
|
|
221
221
|
});
|
|
222
222
|
it('lo que renderiza el payload sale de esta misma funcion', () => {
|
|
@@ -47,7 +47,7 @@ describe('ConfigInstructionsStrategy.inject', () => {
|
|
|
47
47
|
});
|
|
48
48
|
describe('ConfigInstructionsStrategy.remove', () => {
|
|
49
49
|
it('removes only the sentinel, preserving user entries', () => {
|
|
50
|
-
const { configPath,
|
|
50
|
+
const { configPath, provider, input } = setup({ instructions: ['docs/rules.md'] });
|
|
51
51
|
strat.inject(input, provider);
|
|
52
52
|
strat.remove(input, provider);
|
|
53
53
|
const cfg = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
@@ -26,7 +26,7 @@ describe('renderFullTerminal', () => {
|
|
|
26
26
|
it('renders all lifecycle sections in canonical order with status and remediation', () => {
|
|
27
27
|
const output = (0, render_terminal_1.renderFullTerminal)((0, validate_1.validateDashboardSnapshotV1)(completeSnapshot()));
|
|
28
28
|
const headings = ['Machine / install', 'Project readiness', 'Design / planning', 'Execution', 'QA', 'Docs', 'Retro', 'Final / history', 'Processes'];
|
|
29
|
-
expect(headings.map((heading) => output.indexOf(heading))).toEqual(
|
|
29
|
+
expect(headings.map((heading) => output.indexOf(heading))).toEqual(headings.map(() => expect.any(Number)));
|
|
30
30
|
for (let index = 1; index < headings.length; index++)
|
|
31
31
|
expect(output.indexOf(headings[index])).toBeGreaterThan(output.indexOf(headings[index - 1]));
|
|
32
32
|
expect(output).toContain('⚠ Sensors [project.sensors] — 2 stale');
|
|
@@ -191,7 +191,6 @@ describe('gatherProviderChecks — shared skills.global does not mask broken lin
|
|
|
191
191
|
});
|
|
192
192
|
it('still reports shared (healthy, non-degrading) when the shared dir has no broken links', () => {
|
|
193
193
|
const { gatherContext } = require('../../../src/core/diagnostics/context');
|
|
194
|
-
const { computeProviderOverall } = require('../../../src/core/diagnostics/checks');
|
|
195
194
|
const scanSkills = jest.fn(() => ({ valid: ['ok-skill'], repairable: [], dead: [], usurped: [] }));
|
|
196
195
|
const ctx = gatherContext({ cwd: tmpHome, bundles: [], agents: ['opencode', 'codex'], scanSkills });
|
|
197
196
|
for (const provider of ctx.providers) {
|
|
@@ -14,7 +14,6 @@ const SKILLS_ROOT = CONTENT_ROOT;
|
|
|
14
14
|
const WORKFLOWS_ROOT = CONTENT_ROOT;
|
|
15
15
|
// For backward-compat with mock expectations that check SKILLS_DIR-style paths:
|
|
16
16
|
const SKILLS_DIR = path_1.default.join(CONTENT_ROOT, 'skills');
|
|
17
|
-
const WORKFLOWS_DIR = path_1.default.join(CONTENT_ROOT, 'workflows');
|
|
18
17
|
const directoryStat = { dev: 1n, ino: 3n, isDirectory: () => true, isSymbolicLink: () => false };
|
|
19
18
|
const skillStat = { dev: 1n, ino: 2n, size: 0n, isFile: () => true, isSymbolicLink: () => false };
|
|
20
19
|
describe('Artifact Discovery', () => {
|
|
@@ -62,7 +62,7 @@ describe('runInitSteps — orchestrator', () => {
|
|
|
62
62
|
function buildDeps(cwd) {
|
|
63
63
|
const { gatherContext } = require('../../../src/core/diagnostics/context');
|
|
64
64
|
const { discoverAllBundles } = require('../../../src/core/bundles');
|
|
65
|
-
const { contentRoots
|
|
65
|
+
const { contentRoots } = require('../../../src/core/registries');
|
|
66
66
|
const { defaultActions } = require('../../../src/core/init/steps');
|
|
67
67
|
// Seed content at ~/.awm/registries/baseline (content root IS the registry root).
|
|
68
68
|
const contentRoot = path_1.default.join(process.env.AWM_HOME, 'registries', 'baseline');
|
|
@@ -222,7 +222,7 @@ describe('process identity', () => {
|
|
|
222
222
|
});
|
|
223
223
|
test('captureRefFor degrada a unknown si ps falla, nunca crashea al capturar identidad (R2.1)', () => {
|
|
224
224
|
const cp = require('child_process');
|
|
225
|
-
const spy = jest.spyOn(cp, 'execFileSync').mockImplementation((...
|
|
225
|
+
const spy = jest.spyOn(cp, 'execFileSync').mockImplementation((..._args) => {
|
|
226
226
|
const err = new Error('spawn ps ENOENT');
|
|
227
227
|
err.code = 'ENOENT';
|
|
228
228
|
throw err;
|
|
@@ -197,7 +197,7 @@ describe('readDeclaredOrchestrators', () => {
|
|
|
197
197
|
stderrBoundary(result.diagnostics);
|
|
198
198
|
expect(stderrBoundary.mock.calls[0][0].join('')).not.toMatch(/[\u2028\u0085]/);
|
|
199
199
|
expect(stderrBoundary.mock.results[0].value).not.toMatch(/[\u2028\u0085]/);
|
|
200
|
-
//
|
|
200
|
+
// Reject all terminal controls and line separators.
|
|
201
201
|
expect(result.diagnostics[0]).not.toMatch(/[\x00-\x1f\x7f-\x9f\u2028\u2029]/);
|
|
202
202
|
});
|
|
203
203
|
it.each(['inspection', 'read', 'parse', 'shape', 'fields'])('sanitizes %s diagnostics, including paths and error text', (branch) => {
|
|
@@ -224,7 +224,7 @@ describe('readDeclaredOrchestrators', () => {
|
|
|
224
224
|
expect(result.diagnostics[0]).toContain('awm-registry.json');
|
|
225
225
|
expect(result.diagnostics[0]).toContain('before');
|
|
226
226
|
expect(result.diagnostics[0]).toContain('after');
|
|
227
|
-
//
|
|
227
|
+
// Reject all terminal controls and line separators.
|
|
228
228
|
expect(result.diagnostics[0]).not.toMatch(/[\x00-\x1f\x7f-\x9f\u2028\u2029]/);
|
|
229
229
|
});
|
|
230
230
|
it('preserves valid raw fields and structural parsing without skill discovery', () => {
|
|
@@ -290,7 +290,7 @@ describe('collectDeclaredOrchestrators', () => {
|
|
|
290
290
|
writeSkill('first', 'real-skill');
|
|
291
291
|
const { diagnostics } = (0, orchestrators_1.collectDeclaredOrchestrators)();
|
|
292
292
|
expect(diagnostics).toHaveLength(1);
|
|
293
|
-
//
|
|
293
|
+
// Diagnostic must contain no terminal controls or line separators.
|
|
294
294
|
expect(diagnostics[0]).not.toMatch(/[\x00-\x1f\x7f-\x9f\u2028\u2029]/);
|
|
295
295
|
expect(diagnostics[0]).toContain('shared');
|
|
296
296
|
expect(diagnostics[0]).toContain('forged');
|
|
@@ -304,7 +304,7 @@ describe('collectDeclaredOrchestrators', () => {
|
|
|
304
304
|
expect(diagnostics).toHaveLength(1);
|
|
305
305
|
expect(diagnostics[0]).toContain('discovery unavailable');
|
|
306
306
|
expect(diagnostics[0]).toContain('forged');
|
|
307
|
-
//
|
|
307
|
+
// Diagnostic must contain no terminal controls or line separators.
|
|
308
308
|
expect(diagnostics[0]).not.toMatch(/[\x00-\x1f\x7f-\x9f\u2028\u2029]/);
|
|
309
309
|
});
|
|
310
310
|
it('excludes a later colliding root without losing earlier or subsequent healthy roots', () => {
|
|
@@ -676,7 +676,6 @@ windowsOnly('native secure-fs Windows handle fixtures', () => {
|
|
|
676
676
|
const junction = path_1.default.join(root, 'junction');
|
|
677
677
|
fs_1.default.mkdirSync(outside);
|
|
678
678
|
fs_1.default.symlinkSync(outside, junction, 'junction');
|
|
679
|
-
const target = path_1.default.join(junction, 'sensors.json');
|
|
680
679
|
expect(() => binding.writeProjectTransaction(root, 'junction/sensors.json', Buffer.from('new bytes'), { mode: 'create', createParents: false })).toThrow(/rejected path ancestor/i);
|
|
681
680
|
expect(fs_1.default.existsSync(path_1.default.join(outside, 'sensors.json'))).toBe(false);
|
|
682
681
|
});
|
|
@@ -28,7 +28,7 @@ describe('sanitizeDeclaredField', () => {
|
|
|
28
28
|
expect((0, text_1.sanitizeDeclaredField)('a\n## Forjado *b* `c` <d>')).toBe('a Forjado b c d');
|
|
29
29
|
});
|
|
30
30
|
it('no deja ESC ni otros C0 sobrevivir aunque el saneo markdown no los toque', () => {
|
|
31
|
-
//
|
|
31
|
+
// Verificamos la ausencia deliberada de C0.
|
|
32
32
|
expect((0, text_1.sanitizeDeclaredField)('name\x1b[31mred\x07')).not.toMatch(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/);
|
|
33
33
|
});
|
|
34
34
|
it('es idempotente sobre texto ya limpio', () => {
|
|
@@ -86,7 +86,7 @@ describe('directory fsync has exactly one implementation', () => {
|
|
|
86
86
|
];
|
|
87
87
|
expect(rejects(regression, 1)).toBe(true);
|
|
88
88
|
// The sanctioned replacement carries no directory open at all.
|
|
89
|
-
expect(
|
|
89
|
+
expect(rejects(['fsyncDirSync(path.dirname(file));'], 0)).toBe(false);
|
|
90
90
|
// Flushing a FILE descriptor stays legal — that is a real, portable fsync.
|
|
91
91
|
const fileFlush = ["const fd = fs.openSync(temporary, 'wx', 0o600);", 'fs.fsyncSync(fd);'];
|
|
92
92
|
expect(rejects(fileFlush, 0)).toBe(false);
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const child_process_1 = require("child_process");
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
8
|
const path_1 = __importDefault(require("path"));
|
|
8
9
|
// #171: the lint gate linted 14 files — all js/mjs — and exited 0 on a
|
|
9
10
|
// TypeScript CLI. `eslint.config.awm.mjs` imports a project config that does
|
|
@@ -23,12 +24,37 @@ const CLI_ROOT = path_1.default.resolve(__dirname, '../..');
|
|
|
23
24
|
* repo already documents in tests/integration/published-doctor-evidence.e2e.test.ts,
|
|
24
25
|
* and the one this test hit on both Windows legs of its first CI run. Going
|
|
25
26
|
* through `process.execPath` needs no shell, so no argument escaping either. */
|
|
26
|
-
function lint() {
|
|
27
|
-
const
|
|
27
|
+
function lint(target = '.') {
|
|
28
|
+
const args = [
|
|
28
29
|
path_1.default.join(CLI_ROOT, 'node_modules', 'eslint', 'bin', 'eslint.js'),
|
|
29
|
-
|
|
30
|
-
]
|
|
31
|
-
|
|
30
|
+
target, '--config', 'eslint.config.awm.mjs', '--format', 'json',
|
|
31
|
+
];
|
|
32
|
+
try {
|
|
33
|
+
const raw = (0, child_process_1.execFileSync)(process.execPath, args, { cwd: CLI_ROOT, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
34
|
+
return JSON.parse(raw);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
// eslint exits non-zero when it reports an error, and the JSON report is
|
|
38
|
+
// still on stdout. A probe that EXPECTS a finding must read it, not die.
|
|
39
|
+
const stdout = error.stdout;
|
|
40
|
+
if (typeof stdout !== 'string' || stdout.trim() === '')
|
|
41
|
+
throw error;
|
|
42
|
+
return JSON.parse(stdout);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Lint one throwaway `.ts` file inside the repo, so the real config's `.ts`
|
|
46
|
+
* block applies to it, and report what the gate said about it. */
|
|
47
|
+
function probe(body) {
|
|
48
|
+
const directory = fs_1.default.mkdtempSync(path_1.default.join(CLI_ROOT, 'tests', '.lint-probe-'));
|
|
49
|
+
const file = path_1.default.join(directory, 'probe.ts');
|
|
50
|
+
try {
|
|
51
|
+
fs_1.default.writeFileSync(file, body);
|
|
52
|
+
const results = lint(path_1.default.relative(CLI_ROOT, file).split(path_1.default.sep).join('/'));
|
|
53
|
+
return results.flatMap(entry => entry.messages);
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
fs_1.default.rmSync(directory, { recursive: true, force: true });
|
|
57
|
+
}
|
|
32
58
|
}
|
|
33
59
|
describe('the lint gate covers TypeScript (#171)', () => {
|
|
34
60
|
jest.setTimeout(10 * 60_000);
|
|
@@ -53,4 +79,37 @@ describe('the lint gate covers TypeScript (#171)', () => {
|
|
|
53
79
|
.map(m => `${entry.filePath}: ${m.message}`));
|
|
54
80
|
expect(unresolved).toEqual([]);
|
|
55
81
|
});
|
|
82
|
+
// The gate examining TypeScript is worth nothing if it cannot fail on it.
|
|
83
|
+
// #171 left the rule at 'warn' with a backlog behind it; these assert the
|
|
84
|
+
// gate now BLOCKS, and that the tree it blocks on is clean.
|
|
85
|
+
test('the tree it lints is clean, with nothing left to report', () => {
|
|
86
|
+
const findings = results.flatMap(entry => entry.messages.map(m => `${entry.filePath}: ${m.ruleId ?? 'directive'} ${m.message}`));
|
|
87
|
+
expect(findings).toEqual([]);
|
|
88
|
+
});
|
|
89
|
+
test('an unused variable in TypeScript is an error, not a warning', () => {
|
|
90
|
+
const messages = probe('const neverUsed = 1;\nexport const used = 2;\n');
|
|
91
|
+
const unused = messages.filter(m => m.ruleId === '@typescript-eslint/no-unused-vars');
|
|
92
|
+
expect(unused).toHaveLength(1);
|
|
93
|
+
expect(unused[0].severity).toBe(2);
|
|
94
|
+
expect(unused[0].message).toContain('neverUsed');
|
|
95
|
+
});
|
|
96
|
+
test('an underscore-prefixed unused binding stays deliberate and unreported', () => {
|
|
97
|
+
expect(probe('const _deliberatelyUnused = 1;\nexport const used = 2;\n')).toEqual([]);
|
|
98
|
+
});
|
|
99
|
+
// The reason the rule had to become the TypeScript-aware one rather than the
|
|
100
|
+
// base rule: eslint's own `no-unused-vars` does not understand type
|
|
101
|
+
// annotations, so it flagged every parameter NAME inside a function TYPE.
|
|
102
|
+
// That was 195 of the 259 findings #171 exposed. Renaming those to `_x` would
|
|
103
|
+
// have made the types less readable to silence noise.
|
|
104
|
+
test('parameter names inside a function type are not reported as unused', () => {
|
|
105
|
+
const messages = probe('export type Fingerprint = (argv: string[], paths: string[], cwd: string) => string | null;\n');
|
|
106
|
+
expect(messages).toEqual([]);
|
|
107
|
+
});
|
|
108
|
+
test('a caught error that is never inspected is reported too', () => {
|
|
109
|
+
const messages = probe('export function f(value: number): number {\n try { return value; } catch (error) { return 0; }\n}\n');
|
|
110
|
+
const unused = messages.filter(m => m.ruleId === '@typescript-eslint/no-unused-vars');
|
|
111
|
+
expect(unused).toHaveLength(1);
|
|
112
|
+
expect(unused[0].severity).toBe(2);
|
|
113
|
+
expect(unused[0].message).toContain('error');
|
|
114
|
+
});
|
|
56
115
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-workflow-manager",
|
|
3
|
-
"version": "9.11.
|
|
3
|
+
"version": "9.11.6",
|
|
4
4
|
"main": "dist/src/index.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"awm": "./dist/src/index.js"
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"@types/js-yaml": "4.0.9",
|
|
60
60
|
"@types/node": "^25.3.0",
|
|
61
61
|
"@types/semver": "^7.7.1",
|
|
62
|
+
"@typescript-eslint/eslint-plugin": "^8.70.0",
|
|
62
63
|
"@typescript-eslint/parser": "^8.70.0",
|
|
63
64
|
"dependency-cruiser": "16.10.4",
|
|
64
65
|
"eslint": "10.8.1",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|