agentic-workflow-manager 3.13.4 → 3.13.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.
Files changed (90) hide show
  1. package/dist/src/commands/add.js +5 -0
  2. package/dist/src/commands/doctor.js +1 -0
  3. package/dist/src/commands/hooks/claude.js +15 -2
  4. package/dist/src/commands/hooks/shared.js +6 -1
  5. package/dist/src/commands/init.js +1 -0
  6. package/dist/src/commands/pin.js +24 -4
  7. package/dist/src/commands/preflight/checks.js +3 -2
  8. package/dist/src/commands/registry/add.js +1 -1
  9. package/dist/src/commands/sensors/index.js +7 -0
  10. package/dist/src/commands/sensors/init.js +48 -7
  11. package/dist/src/commands/sensors/install.js +20 -11
  12. package/dist/src/commands/sensors/run.js +31 -29
  13. package/dist/src/commands/sync.js +29 -0
  14. package/dist/src/commands/watch/index.js +11 -1
  15. package/dist/src/core/artifact-name.js +80 -0
  16. package/dist/src/core/bundle-install.js +55 -12
  17. package/dist/src/core/bundles.js +72 -13
  18. package/dist/src/core/context/regenerate.js +21 -4
  19. package/dist/src/core/context/strategies/codex-agents.js +18 -2
  20. package/dist/src/core/diagnostics/checks.js +26 -2
  21. package/dist/src/core/diagnostics/context.js +91 -25
  22. package/dist/src/core/diagnostics/provider-checks.js +62 -14
  23. package/dist/src/core/discovery.js +15 -23
  24. package/dist/src/core/export/transform.js +30 -25
  25. package/dist/src/core/frontmatter.js +223 -0
  26. package/dist/src/core/init/mutation-targets.js +11 -0
  27. package/dist/src/core/init/steps.js +70 -19
  28. package/dist/src/core/install-planner.js +62 -17
  29. package/dist/src/core/install-transaction.js +13 -1
  30. package/dist/src/core/journal/adapter.js +10 -1
  31. package/dist/src/core/paths.js +61 -8
  32. package/dist/src/core/profile.js +12 -0
  33. package/dist/src/core/provider-artifacts.js +38 -13
  34. package/dist/src/core/provider-version.js +12 -6
  35. package/dist/src/core/registries.js +40 -19
  36. package/dist/src/core/renderers/canonical-agent.js +33 -2
  37. package/dist/src/core/renderers/registry.js +37 -0
  38. package/dist/src/core/renderers/skill-source.js +11 -19
  39. package/dist/src/core/skill-integrity.js +71 -10
  40. package/dist/src/core/update-check.js +14 -2
  41. package/dist/src/core/versioning.js +17 -2
  42. package/dist/src/index.js +31 -23
  43. package/dist/src/providers/index.js +7 -2
  44. package/dist/src/ui/text.js +10 -2
  45. package/dist/src/utils/config.js +7 -8
  46. package/dist/tests/commands/doctor.test.js +6 -0
  47. package/dist/tests/commands/pin.test.js +29 -2
  48. package/dist/tests/commands/preflight/preflight.test.js +31 -17
  49. package/dist/tests/commands/registry/install-bundles.test.js +6 -6
  50. package/dist/tests/commands/sensors/init-pack-unavailable.test.js +96 -0
  51. package/dist/tests/commands/sensors/install-settings-safety.test.js +87 -0
  52. package/dist/tests/commands/sensors/run-is-read-only.test.js +0 -0
  53. package/dist/tests/commands/sensors/run.test.js +24 -51
  54. package/dist/tests/commands/sensors/status-windows.test.js +39 -22
  55. package/dist/tests/commands/sensors/status.test.js +27 -12
  56. package/dist/tests/core/artifact-name-containment.test.js +86 -0
  57. package/dist/tests/core/context/agents-md-single-slot.test.js +69 -0
  58. package/dist/tests/core/context/regenerate.test.js +39 -8
  59. package/dist/tests/core/diagnostics/checks.test.js +14 -6
  60. package/dist/tests/core/diagnostics/provider-tier.test.js +3 -3
  61. package/dist/tests/core/diagnostics/rendered-artifact-visibility.test.js +69 -0
  62. package/dist/tests/core/discovery.test.js +10 -1
  63. package/dist/tests/core/export/transform.test.js +110 -21
  64. package/dist/tests/core/frontmatter-description-vs-yaml.test.js +118 -0
  65. package/dist/tests/core/init/all-baseline-bundles.test.js +64 -0
  66. package/dist/tests/core/init/context-injection-no-project.test.js +117 -0
  67. package/dist/tests/core/init/orchestrator.test.js +1 -1
  68. package/dist/tests/core/init/steps-registry-sync.test.js +1 -1
  69. package/dist/tests/core/init/steps.test.js +11 -5
  70. package/dist/tests/core/path-resolution-no-shell.test.js +136 -0
  71. package/dist/tests/core/paths.test.js +7 -34
  72. package/dist/tests/core/project-skill-links.test.js +159 -0
  73. package/dist/tests/core/provider-artifacts.test.js +66 -14
  74. package/dist/tests/core/provider-capability-guards.test.js +92 -0
  75. package/dist/tests/core/reconciliation.test.js +15 -15
  76. package/dist/tests/core/registry-manifest.test.js +1 -1
  77. package/dist/tests/core/renderers/canonical-agent.test.js +36 -0
  78. package/dist/tests/core/renderers/cursor-mdc.test.js +37 -7
  79. package/dist/tests/core/renderers/skill-source-block-scalar.test.js +117 -0
  80. package/dist/tests/core/semver-fails-closed.test.js +30 -0
  81. package/dist/tests/core/skill-integrity.test.js +10 -10
  82. package/dist/tests/core/skill-repair-safety.test.js +92 -0
  83. package/dist/tests/core/sync-profile-atomicity.test.js +122 -0
  84. package/dist/tests/integration/copilot-init-isolated.test.js +2 -2
  85. package/dist/tests/providers/index.test.js +1 -1
  86. package/dist/tests/structural/renderer-table-is-single-source.test.js +95 -0
  87. package/dist/tests/structural/symlink-type-is-explicit.test.js +73 -0
  88. package/dist/tests/ui/text.test.js +9 -0
  89. package/dist/tests/utils/registry-view-overrides.test.js +2 -2
  90. package/package.json +6 -1
@@ -55,6 +55,7 @@ exports.defaultActions = {
55
55
  method: o.method,
56
56
  projectRoot: o.projectRoot,
57
57
  contentDir: o.contentDir,
58
+ scopeOverride: o.scopeOverride,
58
59
  }),
59
60
  // Same as installBundle above — defaults to the real applyInstallPlan.
60
61
  syncProfile: (o) => (0, bundle_install_1.syncProfile)({
@@ -66,14 +67,21 @@ exports.defaultActions = {
66
67
  }),
67
68
  initSensors: (o) => {
68
69
  const result = (0, init_1.initSensors)({ cwd: o.cwd, registryRoot: o.registryRoot, configure: o.configure });
69
- return { detection: result.detection };
70
+ // `unavailablePack`/`manifest` viajan a proposito: `stepSensors` los reporta.
71
+ // Este wrapper proyectaba solo `detection`, asi que cualquier hallazgo nuevo de
72
+ // initSensors moria aca en silencio antes de llegar al step que lo muestra.
73
+ return {
74
+ detection: result.detection,
75
+ manifest: { pack: result.manifest.pack },
76
+ ...(result.unavailablePack ? { unavailablePack: result.unavailablePack } : {}),
77
+ };
70
78
  },
71
79
  addExtension: (root, name) => { (0, profile_1.addExtension)(root, name); },
72
80
  ensureProfile: (root) => { (0, profile_1.ensureProfile)(root); },
73
81
  gatherProject: (cwd, bundles, agent) => (0, context_1.gatherContext)({ cwd, bundles, agent }).project,
74
82
  contextStatus: (op) => realInjectionOrchestrator.contextStatus(op),
75
83
  installContext: (op) => { realInjectionOrchestrator.installContext(op); },
76
- repairGlobalSkills: (skillsDir, registryContentDirs) => (0, skill_integrity_1.repairGlobalSkills)(skillsDir, registryContentDirs),
84
+ repairGlobalSkills: (skillsDir, registryContentDirs) => (0, skill_integrity_1.repairSkillLinks)(skillsDir, registryContentDirs),
77
85
  injectProjectConstitution: (o) => {
78
86
  if ((0, providers_1.getInjection)(o.agent)?.type === 'managed-agents-md') {
79
87
  return new codex_agents_1.CodexAgentsStrategy().injectProject(o.projectRoot, (0, providers_1.providerFor)(o.agent), o.agent) === 'injected' ? 'injected' : 'already';
@@ -113,8 +121,12 @@ function failed(id, level, error) {
113
121
  * physical operation and simply re-confirms ownership — it does not change
114
122
  * the co-owner's installed content.
115
123
  */
116
- function sharedInstallAgents(d) {
117
- const group = (0, install_planner_1.agentsSharingSkillTarget)(d.agent, d.enabledAgents, 'global', d.cwd);
124
+ function sharedInstallAgents(d, scope = 'global') {
125
+ // El scope tiene que ser el MISMO con el que se va a instalar: para un
126
+ // provider sin directorio global (Copilot) el baseline va a scope local, y
127
+ // preguntar por el grupo compartido en 'global' devolvia un grupo calculado
128
+ // sobre un destino que para el no existe.
129
+ const group = (0, install_planner_1.agentsSharingSkillTarget)(d.agent, d.enabledAgents, scope, d.cwd);
118
130
  return group.includes(d.agent) ? group : [d.agent, ...group];
119
131
  }
120
132
  /**
@@ -188,18 +200,34 @@ function stepDevCore(d) {
188
200
  if (devCore.present && devCore.brokenLinks.length === 0) {
189
201
  return ok('machine.devCore', 'machine', 'skipped');
190
202
  }
191
- // Find the baseline bundle (there may be several; pick first baseline)
192
- const baselineBundle = d.bundles.find((b) => b.scope === 'baseline');
193
- const bundleName = baselineBundle?.name ?? 'dev';
194
- d.actions.installBundle({
195
- bundleName,
196
- bundles: d.bundles,
197
- agents: sharedInstallAgents(d),
198
- method: d.installMethod,
199
- projectRoot: d.cwd,
200
- contentDir: d.contentDir,
201
- });
202
- return ok('machine.devCore', 'machine', 'applied');
203
+ // TODOS los bundles baseline, no el primero. `scope: 'baseline'` es una propiedad
204
+ // que un bundle TIENE, no un lugar que solo uno puede ocupar — y
205
+ // `reconciliation.ts` (`awm update`) siempre los filtro todos. Con `find` aca, un
206
+ // registry con dos baselines instalaba uno en `awm init` y aparecia el segundo, de
207
+ // la nada, en el siguiente `awm update`; nada antes lo habia reportado como
208
+ // faltante. El fallback a 'dev' se conserva para un registry sin ningun baseline.
209
+ const baselineNames = d.bundles.filter((b) => b.scope === 'baseline').map((b) => b.name);
210
+ const toInstall = baselineNames.length > 0 ? baselineNames : ['dev'];
211
+ // Un provider sin directorio global de skills (hoy: Copilot) no tiene donde
212
+ // poner el baseline a nivel maquina — y el hecho `devCore` lo reporta como
213
+ // "trivialmente satisfecho" para no volver a intentar un install global
214
+ // condenado a tirar. Pero nada instalaba nunca nada en su lugar: Copilot
215
+ // terminaba con CERO skills mientras `awm init` imprimia `✔ dev-core` y
216
+ // `awm doctor` decia `healthy`. Su propio `globalUnsupportedReason` ya dice
217
+ // que hay que instalar por proyecto, asi que eso es lo que se hace.
218
+ const localOnly = (0, providers_1.providerFor)(d.agent).skill.global === null;
219
+ for (const bundleName of toInstall) {
220
+ d.actions.installBundle({
221
+ bundleName,
222
+ bundles: d.bundles,
223
+ agents: sharedInstallAgents(d, localOnly ? 'local' : 'global'),
224
+ method: d.installMethod,
225
+ projectRoot: d.cwd,
226
+ contentDir: d.contentDir,
227
+ ...(localOnly ? { scopeOverride: 'local' } : {}),
228
+ });
229
+ }
230
+ return ok('machine.devCore', 'machine', 'applied', localOnly ? 'installed at project scope (no global skill dir)' : undefined);
203
231
  }
204
232
  /** Step 3.5 – Repair broken global skill symlinks (orphans outside the baseline). */
205
233
  function stepGlobalSkillsRepair(d) {
@@ -294,7 +322,14 @@ function stepSensors(d) {
294
322
  return ok('project.sensors', 'project', 'skipped', 'no project');
295
323
  if (proj.sensors.present)
296
324
  return ok('project.sensors', 'project', 'skipped');
297
- d.actions.initSensors({ cwd: proj.root, registryRoot: d.sensorPacksRoot, configure: true });
325
+ const res = d.actions.initSensors({ cwd: proj.root, registryRoot: d.sensorPacksRoot, configure: true });
326
+ // A registry without the detected pack yields a fallback (or empty) manifest. The
327
+ // step still succeeded — a manifest was written — but reporting a bare 'applied'
328
+ // would let `awm init` hand back a quality gate that measures less than the
329
+ // operator has any reason to expect. Say which pack was missing, here, once.
330
+ if (res.unavailablePack) {
331
+ return ok('project.sensors', 'project', 'applied', `registry has no '${res.unavailablePack}' sensor-pack — wrote '${res.manifest?.pack ?? 'fallback'}' instead; run \`awm update\``);
332
+ }
298
333
  return ok('project.sensors', 'project', 'applied');
299
334
  }
300
335
  /** Step 8 – Signal that the agent should run the project-constitution skill. */
@@ -348,8 +383,24 @@ function stepContextInjection(d) {
348
383
  // AGENTS.md backup target via the same findProjectRoot(cwd) call, so using d.cwd here
349
384
  // whenever it differs from the walked-up project root (e.g. `awm init` run from a
350
385
  // subdirectory) would write to a path the backup session never snapshotted — a failed
351
- // init couldn't roll it back. Falls back to d.cwd only when there's no discovered
352
- // project yet, matching this op's own pre-existing behavior in that case.
386
+ // init couldn't roll it back.
387
+ //
388
+ // The `?? d.cwd` fallback below is the legitimate case: a provider whose ONLY
389
+ // delivery channel is project-scope (Copilot), run in a directory the user means
390
+ // as their project but which carries no marker yet (no .git, no package.json).
391
+ // Removing it would leave Copilot with no context at all there. What made it a bug
392
+ // was that mutation-targets.ts enumerated the AGENTS.md / .awm/context/ backup
393
+ // targets only inside `if (projectRoot)`, so this write landed outside the backup
394
+ // session and survived a rollback that reported a clean restore. That is fixed
395
+ // where it belongs — in the enumeration, which now covers `cwd` unconditionally
396
+ // for these providers.
397
+ //
398
+ // `--machine-only` is the case that genuinely must not write. It nulls
399
+ // `ctx.project` on purpose, and this MACHINE-level step read that as "no project
400
+ // found" and wrote into cwd anyway — under the one flag that promises not to.
401
+ if (scope === 'local' && d.machineOnly) {
402
+ return ok('machine.contextInjection', 'machine', 'skipped', `${(0, providers_1.providerFor)(d.agent).label} delivers context at project scope — skipped by --machine-only`);
403
+ }
353
404
  const op = {
354
405
  agent: d.agent,
355
406
  scope,
@@ -23,6 +23,7 @@ exports.planRemoval = planRemoval;
23
23
  // persistence (artifact-state.ts). This module only computes the plan.
24
24
  const path_1 = __importDefault(require("path"));
25
25
  const providers_1 = require("../providers");
26
+ const registry_1 = require("./renderers/registry");
26
27
  /** Resolves the single physical filesystem location an intent renders to for one agent. */
27
28
  /**
28
29
  * Resolves the physical target path + renderer for one artifact intent on one
@@ -44,21 +45,27 @@ function physicalTarget(intent, agent, scope, projectRoot) {
44
45
  // trailing `.md` (if any) on intent.installName, so e.g. `using-awm`
45
46
  // (skills carry no extension) or `using-awm.md` both become
46
47
  // `using-awm.instructions.md`, never `using-awm.md.instructions.md`.
47
- // Deliberately NOT path.parse(...).name: it strips everything after the
48
- // LAST dot, not just a real trailing extension a skill literally named
49
- // `v1.2-migration` would silently truncate to `v1.mdc`, dropping
50
- // `2-migration` and risking a collision with any other skill named `v1`.
51
- // `.md` is the only extension a skill's installName is ever expected to
52
- // carry (skills are markdown files), so stripping that literal suffix is
53
- // both sufficient and precise.
54
- const baseName = intent.installName.endsWith('.md')
55
- ? intent.installName.slice(0, -'.md'.length)
56
- : intent.installName;
57
- const filename = config.renderer === 'codex-agent-toml' ? `${baseName}.toml`
58
- : config.renderer === 'cursor-mdc' ? `${baseName}.mdc`
59
- : config.renderer === 'copilot-instructions' ? `${baseName}.instructions.md`
60
- : intent.installName;
61
- return { targetPath: path_1.default.join(dir, filename), renderer: config.renderer };
48
+ // `renderedFilename` (core/renderers/registry.ts) es la unica fuente de
49
+ // verdad de este mapeo. Antes vivia inline aca y re-derivado a mano en tres
50
+ // lectores mas, uno de los cuales lo omitia por completo — y ese era
51
+ // justamente el que decidia si un artefacto estaba instalado.
52
+ const filename = (0, registry_1.renderedFilename)(intent.installName, config.renderer);
53
+ const targetPath = path_1.default.join(dir, filename);
54
+ // Asercion de contencion: el destino resuelto DEBE caer dentro del
55
+ // directorio del provider. `dir` es el ancla de confianza real (sale de la
56
+ // config del provider, no del contenido), asi que verificarlo aca atrapa
57
+ // cualquier camino — presente o futuro — que construya un `installName` sin
58
+ // pasar por `assertSafeArtifactName`. Los nombres llegan desde el
59
+ // `bundle.json` de un registry, y el instalador borra el destino de forma
60
+ // recursiva antes de enlazar: un escape aca no es un archivo mal ubicado,
61
+ // es borrado arbitrario fuera del sandbox.
62
+ const resolvedDir = path_1.default.resolve(dir);
63
+ const resolved = path_1.default.resolve(targetPath);
64
+ if (resolved !== resolvedDir && !resolved.startsWith(resolvedDir + path_1.default.sep)) {
65
+ throw new Error(`refusing to install outside the target directory: ${JSON.stringify(intent.installName)} ` +
66
+ `resolves to ${resolved}, which is not inside ${resolvedDir}`);
67
+ }
68
+ return { targetPath, renderer: config.renderer };
62
69
  }
63
70
  /**
64
71
  * The physical directory `agent`'s skill artifacts resolve to at `scope` —
@@ -121,7 +128,19 @@ function assertCompleteSharedGroup(intent, selected, enabled, scope, projectRoot
121
128
  if (intent.type !== 'skill')
122
129
  return;
123
130
  for (const agent of selected) {
124
- const target = physicalTarget(intent, agent, scope, projectRoot).targetPath;
131
+ // El `.filter()` de abajo ya tenia esta guarda; esta linea no — y es la
132
+ // que corre PRIMERO. Con copilot en `selected` (que es lo que hace
133
+ // `awm update`, que apunta a todos los agentes habilitados) tiraba aca,
134
+ // antes de llegar al loop de planificacion, rompiendo el comando para
135
+ // TODOS los demas agentes de la maquina. Un agente que no soporta este
136
+ // scope no tiene grupo compartido que verificar: no es parte del grupo.
137
+ let target;
138
+ try {
139
+ target = physicalTarget(intent, agent, scope, projectRoot).targetPath;
140
+ }
141
+ catch {
142
+ continue;
143
+ }
125
144
  // Same reasoning as agentsSharingSkillTarget above: a candidate in
126
145
  // `enabled` that doesn't support this scope (e.g. Copilot at `global`)
127
146
  // can't be part of the shared-target group — it just isn't a
@@ -172,7 +191,33 @@ function planInstall(params) {
172
191
  }
173
192
  const groups = new Map();
174
193
  for (const intent of artifacts) {
175
- for (const agent of selectedAgents) {
194
+ // Agentes que estructuralmente NO pueden recibir este artefacto en este
195
+ // scope — hoy: copilot en scope global, que no tiene mecanismo de
196
+ // descubrimiento de skills a nivel usuario.
197
+ //
198
+ // Se saltean, pero SOLO si algun otro agente seleccionado si puede. La
199
+ // distincion es deliberada:
200
+ // - `awm update` / `awm add <bundle>` corren sobre TODOS los agentes
201
+ // habilitados. Que uno no pueda no es un error del usuario, y hacer
202
+ // tirar todo el plan rompia el comando para TODOS los demas agentes
203
+ // de la maquina — incluido el re-sync de hooks, que ni se alcanzaba.
204
+ // - `awm add x -a copilot -s global` es una peticion EXPLICITA. Ahi no
205
+ // hay a quien saltear, asi que sigue tirando con la explicacion
206
+ // (`globalUnsupportedReason`), que es justo lo que el usuario
207
+ // necesita leer.
208
+ const capable = selectedAgents.filter((agent) => {
209
+ if (!(0, providers_1.providerFor)(agent)[intent.type])
210
+ return false;
211
+ try {
212
+ physicalTarget(intent, agent, scope, projectRoot);
213
+ return true;
214
+ }
215
+ catch {
216
+ return false;
217
+ }
218
+ });
219
+ const effectiveAgents = capable.length > 0 ? capable : selectedAgents;
220
+ for (const agent of effectiveAgents) {
176
221
  const config = (0, providers_1.providerFor)(agent)[intent.type];
177
222
  if (!config)
178
223
  continue;
@@ -41,8 +41,20 @@ const executor_1 = require("./executor");
41
41
  * accepts. Strips both `:` and `.` (an ISO timestamp has both) so the result
42
42
  * is filesystem- and regex-safe either way.
43
43
  */
44
+ /** Id de transaccion: timestamp + sufijo aleatorio.
45
+ *
46
+ * El timestamp solo tiene resolucion de 1 ms y NO era unico. Dos
47
+ * transacciones en el mismo milisegundo (p. ej. `syncProfile`, que corre una
48
+ * por extension en un loop) producian el mismo id, y como cada una reescribe
49
+ * `manifest.json` con SUS entradas y numera los slots de backup desde 0, la
50
+ * segunda pisaba el backup de la primera: el respaldo del archivo original del
51
+ * usuario quedaba irrecuperable y `awm backup restore <id>` restauraba el
52
+ * target equivocado. Es un agujero en el unico mecanismo del que depende toda
53
+ * la promesa de "siempre se puede revertir". El regex de `restoreBackup` ya
54
+ * acepta este formato. */
44
55
  function sanitizeTransactionTimestamp() {
45
- return new Date().toISOString().replace(/[:.]/g, '-');
56
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
57
+ return `${stamp}-${crypto_1.default.randomBytes(4).toString('hex')}`;
46
58
  }
47
59
  function ensureBackupDir(backupDir) {
48
60
  fs_1.default.mkdirSync(backupDir, { recursive: true });
@@ -1,5 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WATCH_PROVIDERS = void 0;
4
+ exports.isWatchProvider = isWatchProvider;
3
5
  exports.adapterFor = adapterFor;
4
6
  const process_1 = require("./process");
5
7
  const RESUME_PROMPT_PREFIX = 'Sos el orquestador SDD de este repo. Corre `awm job reconcile` y ejecuta ';
@@ -18,10 +20,17 @@ const claudeAdapter = {
18
20
  activity: process_1.activitySnapshot,
19
21
  safeToReplace: baseSafeToReplace,
20
22
  };
23
+ /** Los providers que `awm watch` sabe supervisar. Exportado para que el CLI valide en
24
+ * el borde en vez de dejar que un `--provider` invalido llegue hasta el primer tick,
25
+ * ya con el journal tocado y el lock tomado. */
26
+ exports.WATCH_PROVIDERS = ['codex', 'claude-code'];
27
+ function isWatchProvider(value) {
28
+ return typeof value === 'string' && exports.WATCH_PROVIDERS.includes(value);
29
+ }
21
30
  function adapterFor(provider) {
22
31
  if (provider === 'codex')
23
32
  return codexAdapter;
24
33
  if (provider === 'claude-code')
25
34
  return claudeAdapter;
26
- throw new Error(`provider desconocido: ${provider} (validos: codex, claude-code)`);
35
+ throw new Error(`provider desconocido: ${provider} (validos: ${exports.WATCH_PROVIDERS.join(', ')})`);
27
36
  }
@@ -17,8 +17,8 @@ exports.resolveOnPath = resolveOnPath;
17
17
  // Functions are evaluated at CALL TIME (not require time) so env overrides are
18
18
  // always honored and tests need no jest.resetModules().
19
19
  const os_1 = __importDefault(require("os"));
20
+ const fs_1 = __importDefault(require("fs"));
20
21
  const path_1 = __importDefault(require("path"));
21
- const child_process_1 = require("child_process");
22
22
  /** User home directory with a robust fallback. Never returns a raw, possibly-empty process.env.HOME. */
23
23
  function homeDir() {
24
24
  return process.env.HOME || os_1.default.homedir();
@@ -74,14 +74,67 @@ function noteWindowsCaveat(log) {
74
74
  if (isWindowsNative())
75
75
  log(exports.WINDOWS_KNOWN_GAP);
76
76
  }
77
- /** Resolve a binary on PATH portably: `where` on win32, POSIX `command -v` elsewhere. */
77
+ /**
78
+ * Resolve a binary on PATH, IN PROCESS — no shell, no subprocess.
79
+ *
80
+ * SECURITY (this is the whole reason for the implementation below): this used
81
+ * to build `command -v ${bin}` / `where ${bin}` and hand it to `execSync`,
82
+ * which runs it through a shell. `bin` is the first token of a sensor's `cmd`,
83
+ * and that string comes from `.awm/sensors.json` — a file committed in the
84
+ * repo, and also regenerated from a registry sensor-pack's `defaultCmd`. So a
85
+ * cloned untrusted repo, or a third-party registry, could put shell syntax
86
+ * there. The token can't contain whitespace (it's a split token), but `${IFS}`
87
+ * and `$(...)` make that irrelevant. Confirmed against the real binary: a
88
+ * crafted `cmd` executed an arbitrary command during `awm sensors status` —
89
+ * a read-only inspection command — which then reported the sensor HEALTHY,
90
+ * because the injected command exited 0.
91
+ *
92
+ * Resolving PATH ourselves removes the interpreter entirely, so there is
93
+ * nothing left to inject into on any platform. It's also faster (no process
94
+ * spawn) and more predictable than depending on `where`/`command -v` being
95
+ * present and well-behaved.
96
+ */
78
97
  function resolveOnPath(bin) {
79
- const cmd = isWindowsNative() ? `where ${bin}` : `command -v ${bin}`;
80
- try {
81
- (0, child_process_1.execSync)(cmd, { stdio: 'pipe' });
82
- return true;
83
- }
84
- catch {
98
+ if (typeof bin !== 'string' || bin.trim() === '')
85
99
  return false;
100
+ // win32 resolves `eslint` to `eslint.cmd`/`eslint.exe` via PATHEXT. Missing
101
+ // this broke every npm-shim-backed sensor on Windows (real bug, published in
102
+ // v3.9.0). POSIX has no such concept — the name is the name.
103
+ const extensions = isWindowsNative()
104
+ ? (process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean)
105
+ : [];
106
+ const isExecutable = (candidate) => {
107
+ try {
108
+ if (!fs_1.default.statSync(candidate).isFile())
109
+ return false;
110
+ }
111
+ catch {
112
+ return false;
113
+ }
114
+ // On win32 the executable bit doesn't exist; being a file with a PATHEXT
115
+ // extension is the whole contract.
116
+ if (isWindowsNative())
117
+ return true;
118
+ try {
119
+ fs_1.default.accessSync(candidate, fs_1.default.constants.X_OK);
120
+ return true;
121
+ }
122
+ catch {
123
+ return false;
124
+ }
125
+ };
126
+ // PATHEXT is conventionally uppercase (`.COM;.EXE;.BAT;.CMD`) while the files
127
+ // on disk are usually lowercase (`eslint.cmd`). On real Windows that's a
128
+ // non-issue because the filesystem is case-insensitive — but it IS an issue
129
+ // on any case-sensitive volume, so try both spellings rather than depend on
130
+ // filesystem behavior we don't control.
131
+ const matches = (base) => isExecutable(base) ||
132
+ extensions.some((ext) => isExecutable(base + ext) || isExecutable(base + ext.toLowerCase()));
133
+ // A name carrying a separator is a path, not a PATH lookup — resolve it
134
+ // directly instead of joining it onto every PATH entry.
135
+ if (bin.includes('/') || (isWindowsNative() && bin.includes('\\'))) {
136
+ return matches(path_1.default.resolve(bin));
86
137
  }
138
+ const entries = (process.env.PATH ?? '').split(path_1.default.delimiter).filter(Boolean);
139
+ return entries.some((dir) => matches(path_1.default.join(dir, bin)));
87
140
  }
@@ -125,6 +125,18 @@ function ensureSkillsGitignored(root, agents) {
125
125
  const config = provider[type];
126
126
  if (!config)
127
127
  continue;
128
+ // Solo se ignoran los artefactos que son SYMLINKS especificos de la
129
+ // maquina (renderer `link`), que es lo que `awm sync` reconstruye.
130
+ // Un renderer que produce CONTENIDO (`.mdc` de cursor,
131
+ // `.instructions.md` de copilot) genera archivos reales que el
132
+ // equipo necesita commitear — y para copilot ese es su UNICO canal
133
+ // de entrega, asi que ignorarlos hacia que los companeros no
134
+ // recibieran absolutamente nada. Tambien arrastraba a
135
+ // `.cursor/rules/awm.mdc`, el carrier que existe justamente para
136
+ // que el contexto sobreviva al Background Agent de Cursor, que lee
137
+ // del checkout de git.
138
+ if (config.renderer !== 'link')
139
+ continue;
128
140
  const entry = config.local.endsWith('/') ? config.local : `${config.local}/`;
129
141
  if (!toIgnore.includes(entry))
130
142
  toIgnore.push(entry);
@@ -8,39 +8,64 @@ exports.scanLegacyArtifacts = scanLegacyArtifacts;
8
8
  const fs_1 = __importDefault(require("fs"));
9
9
  const path_1 = __importDefault(require("path"));
10
10
  const providers_1 = require("../providers");
11
+ const artifact_state_1 = require("./artifact-state");
11
12
  const ARTIFACT_TYPES = ['skill', 'workflow', 'agent'];
12
13
  function preflightLinkArtifactPairs(pairs) {
13
14
  for (const { agent, artifact } of pairs) {
14
15
  (0, providers_1.assertLinkRenderer)(artifact.type, agent);
15
16
  }
16
17
  }
17
- function scanLegacyArtifacts(agents, scope) {
18
+ /**
19
+ * Artefactos instalados que AWM puede ofrecer para remover.
20
+ *
21
+ * Cuatro defectos corregidos aca, todos con perdida de datos o no-operacion:
22
+ *
23
+ * 1. **Clave por nombre solo**, a traves de TODOS los tipos y agentes. Un
24
+ * workflow `deploy.md` de antigravity y un agent `deploy.md` de claude-code
25
+ * colapsaban en UNA entrada, asi que elegir "deploy.md" borraba los dos. El
26
+ * escritor (`planInstall`) siempre clavea por ubicacion fisica.
27
+ * 2. **Scope local resuelto contra `process.cwd()`**: `config.local` es
28
+ * relativo (`.claude/skills`), asi que desde un subdirectorio no encontraba
29
+ * nada, y cuando encontraba algo le pasaba una ruta RELATIVA a `fs.rmSync`.
30
+ * Ahora recibe el projectRoot explicito, como todos los demas consumidores.
31
+ * 3. **Listaba el directorio entero**, ofreciendo borrar skills que el usuario
32
+ * escribio a mano. Ahora se cruza contra `readArtifactState()`: solo se
33
+ * ofrece lo que AWM realmente instalo. El registro de propiedad existe
34
+ * justamente para poder distinguirlo.
35
+ * 4. **Se tragaba `UnsupportedRendererError`**, asi que era un no-op permanente
36
+ * para skills de cursor/copilot y agents de codex: `awm remove -a cursor`
37
+ * decia "no hay artefactos instalados" con el directorio lleno.
38
+ */
39
+ function scanLegacyArtifacts(agents, scope, projectRoot = process.cwd(), owned = (0, artifact_state_1.readArtifactState)()) {
18
40
  const artifacts = new Map();
41
+ const ownedPaths = new Set(owned.map((r) => path_1.default.resolve(r.targetPath)));
19
42
  for (const agent of agents) {
20
43
  for (const type of ARTIFACT_TYPES) {
21
- let config;
22
- try {
23
- config = (0, providers_1.assertLinkRenderer)(type, agent);
24
- }
25
- catch (error) {
26
- if (error instanceof providers_1.UnsupportedRendererError)
27
- continue;
28
- throw error;
29
- }
44
+ const config = (0, providers_1.providerFor)(agent)[type];
30
45
  if (!config)
31
46
  continue;
32
- const dir = config[scope];
47
+ // Se usa la config del provider directamente: `assertLinkRenderer`
48
+ // TIRA para renderers que producen contenido, y atrapar ese throw
49
+ // convertia a cursor/copilot/codex en invisibles para `awm remove`.
50
+ const dir = scope === 'local' ? path_1.default.join(projectRoot, config.local) : config.global;
33
51
  if (dir === null || !fs_1.default.existsSync(dir))
34
52
  continue;
35
53
  for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
36
54
  const fullPath = path_1.default.join(dir, entry.name);
37
- const existing = artifacts.get(entry.name);
55
+ // Solo lo que AWM instalo. Un archivo del usuario en el mismo
56
+ // directorio no es nuestro para ofrecerlo en un menu de borrado.
57
+ if (!ownedPaths.has(path_1.default.resolve(fullPath)))
58
+ continue;
59
+ // Clave por TIPO + nombre: dos artefactos de tipos distintos con
60
+ // el mismo nombre son cosas distintas en lugares distintos.
61
+ const key = `${type}\u0000${entry.name}`;
62
+ const existing = artifacts.get(key);
38
63
  if (existing) {
39
64
  existing.installedIn.push(agent);
40
65
  existing.fullPaths.push(fullPath);
41
66
  }
42
67
  else {
43
- artifacts.set(entry.name, {
68
+ artifacts.set(key, {
44
69
  name: entry.name,
45
70
  type,
46
71
  installedIn: [agent],
@@ -19,18 +19,24 @@ function assertProviderSupported(agent, exec = child_process_1.execFileSync) {
19
19
  }
20
20
  catch (error) {
21
21
  const code = error.code;
22
+ // `provider.label` y `versionCommand`, no "Codex" literal. Esta funcion es
23
+ // generica sobre AgentTarget desde siempre, pero cada mensaje y el patron de
24
+ // parseo nombraban al unico provider que hoy declara `versionCommand` — el
25
+ // segundo en declararlo habria reportado "Codex no esta instalado" al no
26
+ // encontrar SU binario, y habria fallado a parsear una salida perfectamente
27
+ // valida contra el formato de otro programa.
22
28
  if (code === 'ENOENT') {
23
- throw new Error('Codex is not installed or not available on PATH. ' +
24
- 'Install the current stable @openai/codex release, then re-run.');
29
+ throw new Error(`${provider.label} is not installed or not available on PATH ` +
30
+ `(tried \`${provider.versionCommand.command}\`). Install it, then re-run.`);
25
31
  }
26
- throw new Error(`Codex version probe failed: ${error.message}`);
32
+ throw new Error(`${provider.label} version probe failed: ${error.message}`);
27
33
  }
28
- const match = output.trim().match(/^codex-cli (\d+\.\d+\.\d+)$/);
34
+ const match = output.trim().match(provider.versionCommand.versionPattern);
29
35
  if (!match) {
30
- throw new Error(`could not parse Codex version from: ${output.trim()}`);
36
+ throw new Error(`could not parse ${provider.label} version from: ${output.trim()}`);
31
37
  }
32
38
  if ((0, versioning_1.compareSemver)(match[1], provider.minimumVersion) < 0) {
33
- throw new Error(`requires Codex >= ${provider.minimumVersion}; found ${match[1]}`);
39
+ throw new Error(`requires ${provider.label} >= ${provider.minimumVersion}; found ${match[1]}`);
34
40
  }
35
41
  return { provider: agent, version: match[1] };
36
42
  }
@@ -3,7 +3,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.REGISTRY_MANIFEST_NAME = exports.CONTENT_DIR_NAMES = exports.REGISTRIES_CONFIG_PATH = exports.REGISTRIES_DIR = void 0;
6
+ exports.REGISTRY_MANIFEST_NAME = exports.CONTENT_DIR_NAMES = void 0;
7
+ exports.registriesDir = registriesDir;
8
+ exports.registriesConfigPath = registriesConfigPath;
7
9
  exports.registryContentRoot = registryContentRoot;
8
10
  exports.readRegistriesConfig = readRegistriesConfig;
9
11
  exports.writeRegistriesConfig = writeRegistriesConfig;
@@ -30,49 +32,57 @@ const versioning_1 = require("./versioning");
30
32
  const cli_version_1 = require("./cli-version");
31
33
  const paths_1 = require("./paths");
32
34
  // Computed at require-time by calling the shared resolver (single source of truth: core/paths.ts).
33
- const AWM_HOME = (0, paths_1.awmHome)();
34
- exports.REGISTRIES_DIR = path_1.default.join(AWM_HOME, 'registries');
35
- exports.REGISTRIES_CONFIG_PATH = path_1.default.join(AWM_HOME, 'registries.json');
35
+ // Funciones, no constantes de nivel de modulo. `awmHome()` lee AWM_HOME/HOME EN CADA
36
+ // LLAMADA a proposito (paths.ts) — resolverlo una sola vez al hacer `require` congelaba
37
+ // la respuesta en el instante del import, asi que cualquier cosa que fijara AWM_HOME
38
+ // despues (un test, un comando que cambia de home) seguia apuntando al viejo. Este
39
+ // modulo era el unico lugar que contradecia esa regla.
40
+ function registriesDir() {
41
+ return path_1.default.join((0, paths_1.awmHome)(), 'registries');
42
+ }
43
+ function registriesConfigPath() {
44
+ return path_1.default.join((0, paths_1.awmHome)(), 'registries.json');
45
+ }
36
46
  exports.CONTENT_DIR_NAMES = ['skills', 'bundles', 'workflows', 'agents'];
37
47
  function registryContentRoot(name) {
38
- const root = path_1.default.join(exports.REGISTRIES_DIR, name);
39
- if (!path_1.default.resolve(root).startsWith(path_1.default.resolve(exports.REGISTRIES_DIR) + path_1.default.sep)) {
48
+ const root = path_1.default.join(registriesDir(), name);
49
+ if (!path_1.default.resolve(root).startsWith(path_1.default.resolve(registriesDir()) + path_1.default.sep)) {
40
50
  throw new Error(`Invalid registry name "${name}" — must not contain path separators`);
41
51
  }
42
52
  return root;
43
53
  }
44
54
  function readRegistriesConfig() {
45
- if (!fs_1.default.existsSync(exports.REGISTRIES_CONFIG_PATH))
55
+ if (!fs_1.default.existsSync(registriesConfigPath()))
46
56
  return [];
47
57
  let raw;
48
58
  try {
49
- raw = JSON.parse(fs_1.default.readFileSync(exports.REGISTRIES_CONFIG_PATH, 'utf-8'));
59
+ raw = JSON.parse(fs_1.default.readFileSync(registriesConfigPath(), 'utf-8'));
50
60
  }
51
61
  catch (e) {
52
- throw new Error(`Invalid registries config at ${exports.REGISTRIES_CONFIG_PATH}: ${e instanceof Error ? e.message : String(e)}`);
62
+ throw new Error(`Invalid registries config at ${registriesConfigPath()}: ${e instanceof Error ? e.message : String(e)}`);
53
63
  }
54
64
  if (!Array.isArray(raw)) {
55
- throw new Error(`Invalid registries config at ${exports.REGISTRIES_CONFIG_PATH}: expected a JSON array`);
65
+ throw new Error(`Invalid registries config at ${registriesConfigPath()}: expected a JSON array`);
56
66
  }
57
67
  for (const entry of raw) {
58
68
  if (typeof entry?.name !== 'string' || typeof entry?.remote !== 'string') {
59
- throw new Error(`Invalid registries config at ${exports.REGISTRIES_CONFIG_PATH}: malformed entry ${JSON.stringify(entry)}`);
69
+ throw new Error(`Invalid registries config at ${registriesConfigPath()}: malformed entry ${JSON.stringify(entry)}`);
60
70
  }
61
71
  if (entry.name === '.' || entry.name.includes('/') || entry.name.includes('\\') || entry.name.includes('..')) {
62
- throw new Error(`Invalid registries config at ${exports.REGISTRIES_CONFIG_PATH}: malformed entry name "${entry.name}" (path traversal)`);
72
+ throw new Error(`Invalid registries config at ${registriesConfigPath()}: malformed entry name "${entry.name}" (path traversal)`);
63
73
  }
64
74
  }
65
75
  return raw;
66
76
  }
67
77
  function writeRegistriesConfig(entries) {
68
- fs_1.default.mkdirSync(AWM_HOME, { recursive: true });
69
- fs_1.default.writeFileSync(exports.REGISTRIES_CONFIG_PATH, JSON.stringify(entries, null, 2) + '\n', 'utf-8');
78
+ fs_1.default.mkdirSync((0, paths_1.awmHome)(), { recursive: true });
79
+ fs_1.default.writeFileSync(registriesConfigPath(), JSON.stringify(entries, null, 2) + '\n', 'utf-8');
70
80
  }
71
81
  /** Bootstrap de máquina (awm init): si no hay registries.json, lo crea con la
72
82
  * entrada baseline (remote por cadena WS-2: env > prefs > default).
73
83
  * Devuelve true si sembró. Nunca toca un registries.json existente. */
74
84
  function seedBaselineRegistry() {
75
- if (fs_1.default.existsSync(exports.REGISTRIES_CONFIG_PATH))
85
+ if (fs_1.default.existsSync(registriesConfigPath()))
76
86
  return false;
77
87
  writeRegistriesConfig([{ name: 'baseline', remote: (0, registry_1.resolveBaseRemote)() }]);
78
88
  return true;
@@ -111,7 +121,7 @@ async function syncRegistries() {
111
121
  try {
112
122
  const freshClone = !fs_1.default.existsSync(reg.contentRoot);
113
123
  if (freshClone) {
114
- fs_1.default.mkdirSync(exports.REGISTRIES_DIR, { recursive: true });
124
+ fs_1.default.mkdirSync(registriesDir(), { recursive: true });
115
125
  try {
116
126
  await (0, simple_git_1.default)().clone(reg.remote, reg.contentRoot);
117
127
  }
@@ -219,11 +229,11 @@ function readRegistryManifest(root) {
219
229
  }
220
230
  return { overrides: new Set(overrides), minCliVersion };
221
231
  }
222
- /** Nombre del registry dueño de un path: el nombre del clone bajo REGISTRIES_DIR,
232
+ /** Nombre del registry dueño de un path: el nombre del clone bajo registriesDir(),
223
233
  * o null si no pertenece a ninguno. */
224
234
  function registryNameForPath(p) {
225
235
  const resolved = path_1.default.resolve(p);
226
- const regsRoot = path_1.default.resolve(exports.REGISTRIES_DIR) + path_1.default.sep;
236
+ const regsRoot = path_1.default.resolve(registriesDir()) + path_1.default.sep;
227
237
  if (resolved.startsWith(regsRoot)) {
228
238
  const first = resolved.slice(regsRoot.length).split(path_1.default.sep)[0];
229
239
  return first || null;
@@ -243,8 +253,19 @@ function verifyMinCliVersions(current = (0, cli_version_1.cliVersion)()) {
243
253
  catch {
244
254
  continue;
245
255
  }
246
- if (min && (0, versioning_1.compareSemver)(current, min) < 0)
256
+ // Falla CERRADO: si alguna de las dos versiones no es legible, se
257
+ // reporta el fallo en vez de dejar pasar. Antes `compareSemver`
258
+ // devolvia NaN y `NaN < 0` es false, asi que un `minCliVersion`
259
+ // malformado hacia que el gate se saltara en silencio.
260
+ if (!min)
261
+ continue;
262
+ try {
263
+ if ((0, versioning_1.compareSemver)(current, min) < 0)
264
+ failures.push({ name: reg.name, min });
265
+ }
266
+ catch {
247
267
  failures.push({ name: reg.name, min });
268
+ }
248
269
  }
249
270
  return failures;
250
271
  }