agentic-workflow-manager 3.13.5 → 3.13.7

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 (84) hide show
  1. package/dist/scripts/support-matrix.js +138 -0
  2. package/dist/src/commands/add.js +5 -0
  3. package/dist/src/commands/doctor.js +1 -0
  4. package/dist/src/commands/hooks/claude.js +15 -2
  5. package/dist/src/commands/hooks/shared.js +6 -1
  6. package/dist/src/commands/init.js +1 -0
  7. package/dist/src/commands/pin.js +24 -4
  8. package/dist/src/commands/preflight/checks.js +3 -2
  9. package/dist/src/commands/registry/add.js +1 -1
  10. package/dist/src/commands/sensors/index.js +7 -0
  11. package/dist/src/commands/sensors/init.js +48 -7
  12. package/dist/src/commands/sensors/install.js +20 -11
  13. package/dist/src/commands/sensors/run.js +31 -29
  14. package/dist/src/commands/sync.js +29 -0
  15. package/dist/src/commands/watch/index.js +11 -1
  16. package/dist/src/core/artifact-name.js +80 -0
  17. package/dist/src/core/bundle-install.js +55 -12
  18. package/dist/src/core/bundles.js +72 -13
  19. package/dist/src/core/context/regenerate.js +21 -4
  20. package/dist/src/core/context/strategies/codex-agents.js +18 -2
  21. package/dist/src/core/diagnostics/checks.js +26 -2
  22. package/dist/src/core/diagnostics/context.js +91 -25
  23. package/dist/src/core/diagnostics/provider-checks.js +62 -14
  24. package/dist/src/core/discovery.js +2 -2
  25. package/dist/src/core/init/mutation-targets.js +34 -1
  26. package/dist/src/core/init/steps.js +70 -19
  27. package/dist/src/core/install-planner.js +62 -17
  28. package/dist/src/core/install-transaction.js +13 -1
  29. package/dist/src/core/journal/adapter.js +10 -1
  30. package/dist/src/core/paths.js +61 -8
  31. package/dist/src/core/profile.js +12 -0
  32. package/dist/src/core/provider-artifacts.js +38 -13
  33. package/dist/src/core/provider-version.js +12 -6
  34. package/dist/src/core/registries.js +40 -19
  35. package/dist/src/core/renderers/registry.js +37 -0
  36. package/dist/src/core/skill-integrity.js +71 -10
  37. package/dist/src/core/update-check.js +14 -2
  38. package/dist/src/core/versioning.js +17 -2
  39. package/dist/src/index.js +31 -23
  40. package/dist/src/providers/index.js +7 -2
  41. package/dist/src/utils/config.js +7 -8
  42. package/dist/tests/commands/doctor.test.js +6 -0
  43. package/dist/tests/commands/pin.test.js +29 -2
  44. package/dist/tests/commands/preflight/preflight.test.js +31 -17
  45. package/dist/tests/commands/registry/install-bundles.test.js +6 -6
  46. package/dist/tests/commands/sensors/init-pack-unavailable.test.js +96 -0
  47. package/dist/tests/commands/sensors/install-settings-safety.test.js +87 -0
  48. package/dist/tests/commands/sensors/run-is-read-only.test.js +0 -0
  49. package/dist/tests/commands/sensors/run.test.js +24 -51
  50. package/dist/tests/commands/sensors/status-windows.test.js +39 -22
  51. package/dist/tests/commands/sensors/status.test.js +27 -12
  52. package/dist/tests/commands/watch/runner.test.js +10 -0
  53. package/dist/tests/core/artifact-name-containment.test.js +86 -0
  54. package/dist/tests/core/context/agents-md-single-slot.test.js +69 -0
  55. package/dist/tests/core/context/regenerate.test.js +39 -8
  56. package/dist/tests/core/diagnostics/checks.test.js +14 -6
  57. package/dist/tests/core/diagnostics/provider-tier.test.js +3 -3
  58. package/dist/tests/core/diagnostics/rendered-artifact-visibility.test.js +69 -0
  59. package/dist/tests/core/init/all-baseline-bundles.test.js +64 -0
  60. package/dist/tests/core/init/context-injection-no-project.test.js +117 -0
  61. package/dist/tests/core/init/mutation-targets-canonical.test.js +81 -0
  62. package/dist/tests/core/init/mutation-targets.test.js +4 -4
  63. package/dist/tests/core/init/orchestrator.test.js +1 -1
  64. package/dist/tests/core/init/steps-registry-sync.test.js +1 -1
  65. package/dist/tests/core/init/steps.test.js +11 -5
  66. package/dist/tests/core/path-resolution-no-shell.test.js +136 -0
  67. package/dist/tests/core/paths.test.js +7 -34
  68. package/dist/tests/core/project-skill-links.test.js +159 -0
  69. package/dist/tests/core/provider-artifacts.test.js +66 -14
  70. package/dist/tests/core/provider-capability-guards.test.js +92 -0
  71. package/dist/tests/core/reconciliation.test.js +15 -15
  72. package/dist/tests/core/registry-manifest.test.js +1 -1
  73. package/dist/tests/core/semver-fails-closed.test.js +30 -0
  74. package/dist/tests/core/skill-integrity.test.js +10 -10
  75. package/dist/tests/core/skill-repair-safety.test.js +92 -0
  76. package/dist/tests/core/sync-profile-atomicity.test.js +122 -0
  77. package/dist/tests/integration/copilot-init-isolated.test.js +2 -2
  78. package/dist/tests/providers/index.test.js +1 -1
  79. package/dist/tests/structural/renderer-table-is-single-source.test.js +95 -0
  80. package/dist/tests/structural/support-matrix-is-current.test.js +100 -0
  81. package/dist/tests/structural/symlink-type-is-explicit.test.js +73 -0
  82. package/dist/tests/support/tmp.js +31 -0
  83. package/dist/tests/utils/registry-view-overrides.test.js +2 -2
  84. package/package.json +6 -2
@@ -5,6 +5,7 @@ const child_process_1 = require("child_process");
5
5
  const init_1 = require("./init");
6
6
  const supervisor_1 = require("./supervisor");
7
7
  const process_1 = require("../../core/journal/process");
8
+ const adapter_1 = require("../../core/journal/adapter");
8
9
  function currentBranch(cwd) {
9
10
  // stdio explicito (ver EXEC_STDIO en journal/process.ts): evita el relay
10
11
  // default de execFileSync del stderr de git hacia el stderr del llamante,
@@ -25,7 +26,7 @@ function registerWatchCommand(program) {
25
26
  .command('watch')
26
27
  .description('supervisor durable: ejecuta jobs, releva controladores caidos, nunca mata trabajo vivo')
27
28
  .option('--init', 'bootstrap: crea el journal de la rama actual, detecta verificadores y sale')
28
- .option('--provider <p>', 'codex | claude-code', 'codex')
29
+ .option('--provider <p>', adapter_1.WATCH_PROVIDERS.join(' | '), 'codex')
29
30
  .option('--heartbeat-timeout <min>', 'minutos de silencio de heartbeat', '5')
30
31
  .option('--activity-window <min>', 'minutos extra sin actividad de proceso', '10')
31
32
  .action(async (opts) => {
@@ -36,6 +37,15 @@ function registerWatchCommand(program) {
36
37
  process.stdout.write(`journal inicializado para ${branch}; verificadores requeridos: ${JSON.stringify(out.requiredVerifiers)}\n`);
37
38
  return;
38
39
  }
40
+ // Validado ACA, antes de tocar nada: `adapterFor` ya rechazaba lo
41
+ // desconocido, pero recien en el primer tick — con el journal escrito y el
42
+ // lock tomado, y el error saliendo del supervisor en vez de del flag que lo
43
+ // causo. Un typo en `--provider` tiene que costar un mensaje, no un ciclo.
44
+ if (!(0, adapter_1.isWatchProvider)(opts.provider)) {
45
+ process.stderr.write(`--provider invalido: ${String(opts.provider)} (validos: ${adapter_1.WATCH_PROVIDERS.join(', ')})\n`);
46
+ process.exitCode = 1;
47
+ return;
48
+ }
39
49
  const cfg = {
40
50
  ...supervisor_1.DEFAULT_SUPERVISOR_CONFIG,
41
51
  provider: opts.provider,
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ // src/core/artifact-name.ts
3
+ //
4
+ // Modulo HOJA (sin imports de otros modulos del proyecto): validacion de los
5
+ // nombres de artefacto que llegan desde CONTENIDO DE REGISTRY.
6
+ //
7
+ // Por que existe. Los nombres de `bundle.json` (`skills[]`, `workflows[]`,
8
+ // `agents[]`) se usaban verbatim para construir rutas de instalacion:
9
+ //
10
+ // path.join('~/.claude/skills', '../../.ssh/authorized_keys')
11
+ // => '~/.ssh/authorized_keys'
12
+ //
13
+ // y como `replaceArtifact` hace `fs.rmSync(targetPath, {recursive:true})` antes
14
+ // de enlazar, un nombre como `../../.ssh` BORRABA recursivamente el ~/.ssh real
15
+ // del usuario. Con `../../.config/autostart/x.desktop` se consigue ejecucion al
16
+ // siguiente login. Confirmado end-to-end contra el binario real.
17
+ //
18
+ // El registry es contenido de terceros: un registry de equipo, uno interno, o
19
+ // uno que alguien agrego con `awm registry add`. Todos los demas lectores de
20
+ // contenido de registry de este repo (readRegistriesConfig, readRegistryManifest,
21
+ // readProfile) ya rechazaban `..` y separadores — este camino era el unico sin
22
+ // la guarda.
23
+ //
24
+ // Esta validacion es la PRIMERA de dos capas. La segunda es la asercion de
25
+ // contencion en `physicalTarget` (install-planner.ts), que verifica que la ruta
26
+ // resuelta caiga realmente dentro del directorio destino. Se mantienen las dos
27
+ // a proposito: esta da un mensaje accionable que nombra el artefacto culpable;
28
+ // aquella es el ancla estructural que atrapa cualquier camino futuro que
29
+ // construya rutas sin pasar por aca.
30
+ Object.defineProperty(exports, "__esModule", { value: true });
31
+ exports.isSafeArtifactName = isSafeArtifactName;
32
+ exports.assertSafeArtifactName = assertSafeArtifactName;
33
+ /** Nombres reservados de Windows: no pueden ser un archivo ni un directorio.
34
+ * Se rechazan en TODA plataforma a proposito — el registry es contenido
35
+ * compartido, y un nombre que solo rompe en las maquinas Windows del equipo es
36
+ * peor que uno rechazado de forma consistente en todas. */
37
+ const WINDOWS_RESERVED = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\..*)?$/i;
38
+ /** ¿Es seguro usar este nombre como UN componente de ruta dentro del directorio
39
+ * de instalacion? Sin `..`, sin separadores, sin rutas absolutas, sin bytes de
40
+ * control, sin nombres reservados. */
41
+ function isSafeArtifactName(name) {
42
+ if (typeof name !== 'string')
43
+ return false;
44
+ const value = name.trim();
45
+ if (value === '' || value !== name)
46
+ return false; // vacio, o con espacios al borde
47
+ // Windows recorta en silencio el punto/espacio final, asi que `evil.` y
48
+ // `evil` terminan en el MISMO archivo — un nombre que apunta a otro destino
49
+ // del que aparenta.
50
+ if (/[. ]$/.test(value))
51
+ return false;
52
+ // Bytes de control y NUL: truncan la ruta a nivel syscall en algunos SO.
53
+ // eslint-disable-next-line no-control-regex
54
+ if (/[\u0000-\u001f\u007f]/.test(value))
55
+ return false;
56
+ // Cualquier separador (de ambas plataformas) convierte esto en una ruta,
57
+ // no en un nombre. Idem `..`/`.` como componente completo.
58
+ if (value.includes('/') || value.includes('\\'))
59
+ return false;
60
+ if (value === '.' || value === '..')
61
+ return false;
62
+ // Absolutos de Windows (`C:\...`, `C:algo`) y de UNC ya quedan cubiertos por
63
+ // el chequeo de separador, pero un `C:` pelado no — y sigue siendo una
64
+ // referencia de unidad, no un nombre.
65
+ if (/^[a-zA-Z]:/.test(value))
66
+ return false;
67
+ if (WINDOWS_RESERVED.test(value))
68
+ return false;
69
+ return true;
70
+ }
71
+ /** Forma asertiva: lanza con un mensaje que nombra el tipo y el valor ofensivo,
72
+ * para que el operador pueda ubicarlo en el `bundle.json` del registry. */
73
+ function assertSafeArtifactName(name, type) {
74
+ if (!isSafeArtifactName(name)) {
75
+ throw new Error(`unsafe ${type} name from registry content: ${JSON.stringify(name)}. ` +
76
+ `Artifact names must be a single path component — no "..", no path separators, ` +
77
+ `no absolute paths, no control characters, and not a Windows reserved name.`);
78
+ }
79
+ return name;
80
+ }
@@ -10,6 +10,7 @@ exports.syncProfile = syncProfile;
10
10
  // src/core/bundle-install.ts
11
11
  const fs_1 = __importDefault(require("fs"));
12
12
  const path_1 = __importDefault(require("path"));
13
+ const artifact_name_1 = require("./artifact-name");
13
14
  const bundles_1 = require("./bundles");
14
15
  const providers_1 = require("../providers");
15
16
  const profile_1 = require("./profile");
@@ -18,14 +19,24 @@ const config_1 = require("../utils/config");
18
19
  const install_planner_1 = require("./install-planner");
19
20
  const install_transaction_1 = require("./install-transaction");
20
21
  function bundleArtifacts(b, contentDir) {
22
+ // Estos nombres vienen VERBATIM del `bundle.json` de un registry — contenido
23
+ // de terceros. Sin validar, `path.join(installDir, name)` con `../../.ssh`
24
+ // escapaba del directorio destino, y como el instalador hace
25
+ // `rmSync(target, {recursive:true})` antes de enlazar, eso BORRABA el ~/.ssh
26
+ // real del usuario. Se valida en el origen, donde el mensaje puede nombrar
27
+ // el artefacto culpable; `physicalTarget` ademas asegura contencion
28
+ // estructural sobre la ruta ya resuelta.
21
29
  const refs = [];
22
30
  for (const s of b.skills) {
31
+ (0, artifact_name_1.assertSafeArtifactName)(s.name, 'skill');
23
32
  refs.push({ name: s.name, type: 'skill', installName: s.name, sourcePath: path_1.default.join(contentDir, 'skills', s.name) });
24
33
  }
25
34
  for (const w of b.workflows) {
35
+ (0, artifact_name_1.assertSafeArtifactName)(w, 'workflow');
26
36
  refs.push({ name: w, type: 'workflow', installName: `${w}.md`, sourcePath: path_1.default.join(contentDir, 'workflows', `${w}.md`) });
27
37
  }
28
38
  for (const a of b.agents) {
39
+ (0, artifact_name_1.assertSafeArtifactName)(a, 'agent');
29
40
  refs.push({ name: a, type: 'agent', installName: `${a}.md`, sourcePath: path_1.default.join(contentDir, 'agents', `${a}.md`) });
30
41
  }
31
42
  return refs;
@@ -109,30 +120,62 @@ function addBundle(opts) {
109
120
  */
110
121
  function syncProfile(opts) {
111
122
  const profile = (0, profile_1.readProfile)(opts.projectRoot);
112
- const installed = [];
113
123
  const skipped = [];
114
- const modifiedFiles = [];
115
- const transactionIds = [];
124
+ // ONE plan for the whole sync, not one per extension.
125
+ //
126
+ // This used to loop `installBundle` over `profile.extensions`, so N extensions
127
+ // meant N independent `applyInstallPlan` transactions. Each is atomic on its own;
128
+ // none is atomic with the others. A failure on the third extension left the first
129
+ // two committed and the tree in a state matching neither before nor after — and
130
+ // `awm sync` is the command a team runs to make a checkout match its profile, so
131
+ // "partially matches" is the one outcome it must not produce.
132
+ //
133
+ // Merging is safe because the two halves were already built for it: expanding a
134
+ // bundle (`expandBundleArtifacts`) is pure, and `planInstall` dedups by physical
135
+ // target across ALL the artifacts it is handed — which is also why two extensions
136
+ // sharing a dependency now collapse into one operation instead of racing to write
137
+ // the same path twice. `applyInstallPlan` backs up and rolls back per plan, so one
138
+ // plan is precisely what makes the sync atomic.
139
+ const intents = [];
116
140
  for (const ext of profile.extensions) {
117
- if (!opts.bundles.some((b) => b.name === ext)) {
141
+ const bundle = opts.bundles.find((b) => b.name === ext);
142
+ if (!bundle) {
118
143
  skipped.push(`${ext} (bundle not found in registry — remove with \`awm remove ${ext}\`)`);
119
144
  continue;
120
145
  }
121
- const summary = installBundle({
146
+ intents.push(...expandBundleArtifacts({
122
147
  bundleName: ext,
123
148
  bundles: opts.bundles,
124
149
  agents: opts.agents,
125
150
  method: opts.method,
126
151
  projectRoot: opts.projectRoot,
127
152
  contentDir: opts.contentDir,
128
- applyPlan: opts.applyPlan,
129
- });
130
- installed.push(...summary.installed);
131
- skipped.push(...summary.skipped);
132
- modifiedFiles.push(...summary.modifiedFiles);
133
- transactionIds.push(summary.transactionId);
153
+ }));
134
154
  }
135
155
  if (profile.extensions.length > 0)
136
156
  (0, profile_1.ensureSkillsGitignored)(opts.projectRoot, opts.agents);
137
- return { installed, skipped, extensions: profile.extensions, transactionIds, modifiedFiles };
157
+ // Nothing resolvable: no plan, no transaction, nothing to name to `awm backup
158
+ // restore`. An empty `transactionIds` is the honest answer, not a phantom id.
159
+ if (intents.length === 0) {
160
+ return { installed: [], skipped, extensions: profile.extensions, transactionIds: [], modifiedFiles: [] };
161
+ }
162
+ const plan = (0, install_planner_1.planInstall)({
163
+ artifacts: intents,
164
+ selectedAgents: opts.agents,
165
+ enabledAgents: (0, config_1.getPreferences)().enabledAgents,
166
+ // Every extension in the profile is a project-scope bundle by construction
167
+ // (`shouldRecordExtension` only records those), so the whole sync resolves to
168
+ // one scope — which is what lets it be one plan.
169
+ scope: 'local',
170
+ projectRoot: opts.projectRoot,
171
+ method: opts.method,
172
+ });
173
+ const summary = (opts.applyPlan ?? install_transaction_1.applyInstallPlan)(plan);
174
+ return {
175
+ installed: summary.installed,
176
+ skipped: [...skipped, ...summary.skipped],
177
+ extensions: profile.extensions,
178
+ transactionIds: [summary.transactionId],
179
+ modifiedFiles: summary.modifiedFiles,
180
+ };
138
181
  }
@@ -16,15 +16,59 @@ const registries_1 = require("./registries");
16
16
  function catalogPath(contentDir) {
17
17
  return path_1.default.join(contentDir, 'catalog.json');
18
18
  }
19
+ /** Lee `catalog.json` de un registry.
20
+ *
21
+ * El contenido de un registry es INPUT NO CONFIABLE (un registry de equipo,
22
+ * interno, o agregado con `awm registry add`), asi que se valida forma antes de
23
+ * desreferenciar. Sin esto, un solo archivo malformado en CUALQUIER registry
24
+ * — incluido uno que llega por un `awm update` rutinario — hacia que `awm list`
25
+ * y `awm add` murieran con un stack trace crudo de Node y ningun mensaje
26
+ * accionable. Los demas lectores de contenido de registry de este repo
27
+ * (readRegistriesConfig, readRegistryManifest, readProfile) ya validaban; estos
28
+ * dos eran los que faltaban. */
19
29
  function readCatalog(contentDir) {
20
30
  const file = catalogPath(contentDir);
21
31
  if (!fs_1.default.existsSync(file))
22
32
  return [];
23
- const parsed = JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
24
- return parsed.bundles ?? [];
33
+ let parsed;
34
+ try {
35
+ parsed = JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
36
+ }
37
+ catch {
38
+ throw new Error(`${file} is not valid JSON. Fix the file in its registry, then re-run \`awm update\`.`);
39
+ }
40
+ if (parsed === null || typeof parsed !== 'object') {
41
+ throw new Error(`${file} must contain a JSON object.`);
42
+ }
43
+ const bundles = parsed.bundles;
44
+ if (bundles === undefined)
45
+ return [];
46
+ if (!Array.isArray(bundles)) {
47
+ throw new Error(`${file}: "bundles" must be an array.`);
48
+ }
49
+ // Una entrada sin `source` usable haria que `path.join` tirara un
50
+ // TypeError opaco mas adelante; se descarta aca, nombrando el archivo.
51
+ return bundles.filter((entry) => entry !== null && typeof entry === 'object' && typeof entry.source === 'string');
25
52
  }
53
+ /** Tolera `skills` ausente o mal formado: lo que no sea un array se trata como
54
+ * vacio en vez de tirar `(raw ?? []).map is not a function`, y cada entrada se
55
+ * valida individualmente. */
26
56
  function normalizeSkillRefs(raw) {
27
- return (raw ?? []).map((s) => typeof s === 'string' ? { name: s, onSignal: false } : { name: s.name, onSignal: s.onSignal === true });
57
+ if (!Array.isArray(raw))
58
+ return [];
59
+ return raw.flatMap((s) => {
60
+ if (typeof s === 'string')
61
+ return [{ name: s, onSignal: false }];
62
+ if (s !== null && typeof s === 'object' && typeof s.name === 'string') {
63
+ return [{ name: s.name, onSignal: s.onSignal === true }];
64
+ }
65
+ return [];
66
+ });
67
+ }
68
+ /** Array de strings, o vacio. Cualquier otra forma en el contenido del registry
69
+ * se ignora en vez de propagarse hasta un crash aguas abajo. */
70
+ function stringArray(raw) {
71
+ return Array.isArray(raw) ? raw.filter((v) => typeof v === 'string') : [];
28
72
  }
29
73
  function discoverBundles(contentDir) {
30
74
  const entries = readCatalog(contentDir);
@@ -33,17 +77,32 @@ function discoverBundles(contentDir) {
33
77
  const manifestPath = path_1.default.join(contentDir, entry.source, 'bundle.json');
34
78
  if (!fs_1.default.existsSync(manifestPath))
35
79
  continue;
36
- const raw = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf-8'));
80
+ let raw;
81
+ try {
82
+ raw = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf-8'));
83
+ }
84
+ catch {
85
+ throw new Error(`${manifestPath} is not valid JSON. Fix the file in its registry, then re-run \`awm update\`.`);
86
+ }
87
+ if (raw === null || typeof raw !== 'object') {
88
+ throw new Error(`${manifestPath} must contain a JSON object.`);
89
+ }
90
+ const m = raw;
91
+ // Un bundle sin nombre usable no es instalable y solo produciria errores
92
+ // opacos mas adelante — se nombra el archivo y se falla aca.
93
+ if (typeof m.name !== 'string' || m.name === '') {
94
+ throw new Error(`${manifestPath}: "name" must be a non-empty string.`);
95
+ }
37
96
  bundles.push({
38
- name: raw.name,
39
- description: raw.description ?? '',
40
- version: raw.version ?? '0.0.0',
41
- scope: raw.scope ?? 'project',
42
- visibility: raw.visibility ?? 'public',
43
- dependsOn: raw.dependsOn ?? [],
44
- skills: normalizeSkillRefs(raw.skills),
45
- workflows: raw.workflows ?? [],
46
- agents: raw.agents ?? [],
97
+ name: m.name,
98
+ description: typeof m.description === 'string' ? m.description : '',
99
+ version: typeof m.version === 'string' ? m.version : '0.0.0',
100
+ scope: m.scope ?? 'project',
101
+ visibility: m.visibility ?? 'public',
102
+ dependsOn: stringArray(m.dependsOn),
103
+ skills: normalizeSkillRefs(m.skills),
104
+ workflows: stringArray(m.workflows),
105
+ agents: stringArray(m.agents),
47
106
  contentRoot: contentDir,
48
107
  });
49
108
  }
@@ -5,7 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.regenerateGlobalContext = regenerateGlobalContext;
7
7
  // W3 — Regeneración del contexto global tras `awm update`.
8
- // Para cada agente con inyección config-instructions cuya config exista:
8
+ // Para cada agente con un mecanismo de inyección aplicable:
9
9
  // - 'stale' (sentinel presente, archivo materializado ausente/viejo) → re-materializa.
10
10
  // - 'injected' (ya fresco) → no toca nada.
11
11
  // - 'absent' (sentinel ausente) → no inyecta (eso es `awm init`).
@@ -21,10 +21,27 @@ function regenerateGlobalContext(targets = [...providers_1.AGENT_TARGETS], orch
21
21
  const out = [];
22
22
  for (const agent of targets) {
23
23
  const inj = (0, providers_1.providerFor)(agent).injection;
24
- if (!inj || inj.type !== 'config-instructions')
25
- continue;
26
- if (!fs_1.default.existsSync(inj.configPath))
24
+ if (!inj)
27
25
  continue;
26
+ // Antes esto exigia `type === 'config-instructions'`, o sea que SOLO se
27
+ // regeneraba OpenCode: para codex, cursor y copilot (`managed-agents-md`)
28
+ // `awm update` era un no-op silencioso — ni un aviso — y el unico modo
29
+ // de refrescar su contexto era volver a correr `awm init`. Peor: doctor
30
+ // marcaba ⚠ pero seguia reportando `status: healthy` / exit 0, asi que
31
+ // un AGENTS.md desactualizado era invisible para cualquier gate de CI
32
+ // que mirara el codigo de salida.
33
+ //
34
+ // El destino global de `managed-agents-md` puede ser null (cursor y
35
+ // copilot no tienen archivo de contexto global); ahi no hay nada global
36
+ // que regenerar y se saltea limpio.
37
+ if (inj.type === 'config-instructions') {
38
+ if (!fs_1.default.existsSync(inj.configPath))
39
+ continue;
40
+ }
41
+ else if (inj.type === 'managed-agents-md') {
42
+ if (inj.globalPath === null)
43
+ continue;
44
+ }
28
45
  const op = {
29
46
  agent,
30
47
  scope: 'global',
@@ -101,9 +101,25 @@ class CodexAgentsStrategy {
101
101
  // separate global file, so this is the only writer of the project AGENTS.md.
102
102
  const injection = provider.injection;
103
103
  const contextCoversThisFile = injection?.type === 'managed-agents-md' && injection.globalPath === null;
104
- const result = contextCoversThisFile
104
+ // La guarda de arriba solo mira a ESTE provider. Pero el bloque es uno
105
+ // solo por archivo, y OTRO provider ya habilitado (cursor/copilot, cuyo
106
+ // contexto ES este archivo) pudo haberlo escrito antes — con el contexto
107
+ // AWM completo, del que PROJECT_GUIDANCE es apenas la cola. Sin este
108
+ // chequeo, un `awm init -a codex` posterior lo reemplazaba por las 6
109
+ // lineas de PROJECT_GUIDANCE: 140 -> 6, y para Copilot ese archivo es su
110
+ // UNICO canal de entrega, asi que se quedaba sin nada. Y al reves otra
111
+ // vez en el siguiente init. Se compara contra el bloque real en disco en
112
+ // vez de asumir quien escribio: si ya cubre PROJECT_GUIDANCE, lo que hay
113
+ // es un superconjunto y pisarlo seria perder informacion.
114
+ const agentsMdPath = path_1.default.join(projectRoot, 'AGENTS.md');
115
+ const existingBody = fs_1.default.existsSync(agentsMdPath)
116
+ ? (0, managed_block_1.managedBlockBody)(fs_1.default.readFileSync(agentsMdPath, 'utf-8'))
117
+ : null;
118
+ const alreadyCovered = existingBody !== null &&
119
+ (0, managed_block_1.normalizeManagedBody)(existingBody).includes((0, managed_block_1.normalizeManagedBody)(PROJECT_GUIDANCE));
120
+ const result = contextCoversThisFile || alreadyCovered
105
121
  ? 'unchanged'
106
- : injectFile(path_1.default.join(projectRoot, 'AGENTS.md'), PROJECT_GUIDANCE);
122
+ : injectFile(agentsMdPath, PROJECT_GUIDANCE);
107
123
  let carrierResult = 'unchanged';
108
124
  if (agent === 'cursor') {
109
125
  // Cursor's Background/Cloud Agent does not reliably read AGENTS.md (open,
@@ -30,7 +30,15 @@ function machineChecks(m) {
30
30
  detail: `git ${m.registryCache.gitState ?? 'unknown'}`, remedy: none });
31
31
  }
32
32
  // machine.hook
33
- if (m.hook.present && !m.hook.degraded) {
33
+ // Un provider sin mecanismo de hooks no puede "tener el hook instalado":
34
+ // no se emite fila alguna, igual que ya hacen el paso de init
35
+ // (init/steps.ts, "no hook mechanism for this agent") y el reporte por
36
+ // provider (provider-checks.ts, que descarta la fila). Emitir `missing`
37
+ // aca era lo que ponia `awm init` en exit 1 para 4 de los 6 providers.
38
+ if (!m.hook.applicable) {
39
+ // sin fila
40
+ }
41
+ else if (m.hook.present && !m.hook.degraded) {
34
42
  out.push({ id: 'machine.hook', level: 'machine', label: 'hook SessionStart', status: 'ok', remedy: none });
35
43
  }
36
44
  else if (m.hook.present) {
@@ -106,6 +114,17 @@ function projectChecks(p) {
106
114
  out.push({ id: 'project.activation', level: 'project', label: 'active bundles', status: 'missing',
107
115
  detail: `${missingLinks.length} missing, ${p.activeBundles.broken.length} broken`, remedy: cmd('awm sync') });
108
116
  }
117
+ // project.orphans — links colgantes que ya no pertenecen a ninguna extension.
118
+ // Fila propia y no parte de `project.activation`: ese check mide "falta lo que el
119
+ // profile pide", y esto es lo contrario — sobra lo que el profile ya no pide. Sin
120
+ // separarlos, un proyecto con todo lo esperado instalado reportaba `ok` mientras
121
+ // arrastraba skills muertas que el agente sigue intentando leer.
122
+ const orphans = p.orphanLinks.repairable.length + p.orphanLinks.dead.length;
123
+ if (orphans > 0) {
124
+ out.push({ id: 'project.orphans', level: 'project', label: 'orphaned skill links', status: 'warn',
125
+ detail: `${p.orphanLinks.repairable.length} repairable, ${p.orphanLinks.dead.length} dead`,
126
+ remedy: cmd('awm sync') });
127
+ }
109
128
  // project.sensors
110
129
  out.push(p.sensors.present
111
130
  ? { id: 'project.sensors', level: 'project', label: 'sensors', status: 'ok', remedy: none }
@@ -137,7 +156,12 @@ function projectChecks(p) {
137
156
  // actionable-but-expected transient states (rendered with ◷/⚠, not ✖) and
138
157
  // do not by themselves degrade `overall`, mirroring how a 'warn' CheckResult
139
158
  // doesn't degrade the machine+project report either.
140
- const DEGRADING_PROVIDER_STATES = ['missing', 'unsupported', 'broken', 'absent', 'conflict'];
159
+ // `stale` cuenta como degradado: un contexto desactualizado es exactamente la
160
+ // clase de problema que un gate de CI tiene que ver. Antes doctor imprimia ⚠
161
+ // junto a `status: healthy` y exit 0, asi que un AGENTS.md viejo — o sobrescrito
162
+ // por otro provider — era invisible para cualquier script que mirara el codigo
163
+ // de salida. Un aviso que no cambia el veredicto no es un aviso, es ruido.
164
+ const DEGRADING_PROVIDER_STATES = ['missing', 'unsupported', 'broken', 'absent', 'conflict', 'stale'];
141
165
  function computeProviderOverall(providers) {
142
166
  const degraded = providers.some((p) => p.checks.some((c) => DEGRADING_PROVIDER_STATES.includes(c.state)));
143
167
  return degraded ? 'degraded' : 'healthy';