agentic-workflow-manager 9.11.5 → 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.
@@ -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.
@@ -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
- if (!input.liveState || input.liveState.state !== 'certified')
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
  });
@@ -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(['cli', ...consumedRegistryComponents]);
71
- const present = new Set(report.components.map(component => component.component));
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;
@@ -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
- function cliVersion() {
16
- let dir = __dirname;
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 && typeof pkg.version === 'string')
23
- return pkg.version;
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
  }
@@ -637,7 +637,11 @@ describe('plan admit Commander wiring', () => {
637
637
  process.exitCode = undefined;
638
638
  }
639
639
  });
640
- it('does not run sensors after currentness blocks', async () => {
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).not.toHaveBeenCalled();
655
- expect(JSON.parse(String(output.mock.calls[0][0]))).toMatchObject({ state: 'blocked', currentness: 'stale', sensors: 'not-required' });
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();
@@ -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
+ });
@@ -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
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-workflow-manager",
3
- "version": "9.11.5",
3
+ "version": "9.11.6",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"
Binary file
Binary file