agentic-workflow-manager 3.13.5 → 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 (78) 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 +2 -2
  24. package/dist/src/core/init/mutation-targets.js +11 -0
  25. package/dist/src/core/init/steps.js +70 -19
  26. package/dist/src/core/install-planner.js +62 -17
  27. package/dist/src/core/install-transaction.js +13 -1
  28. package/dist/src/core/journal/adapter.js +10 -1
  29. package/dist/src/core/paths.js +61 -8
  30. package/dist/src/core/profile.js +12 -0
  31. package/dist/src/core/provider-artifacts.js +38 -13
  32. package/dist/src/core/provider-version.js +12 -6
  33. package/dist/src/core/registries.js +40 -19
  34. package/dist/src/core/renderers/registry.js +37 -0
  35. package/dist/src/core/skill-integrity.js +71 -10
  36. package/dist/src/core/update-check.js +14 -2
  37. package/dist/src/core/versioning.js +17 -2
  38. package/dist/src/index.js +31 -23
  39. package/dist/src/providers/index.js +7 -2
  40. package/dist/src/utils/config.js +7 -8
  41. package/dist/tests/commands/doctor.test.js +6 -0
  42. package/dist/tests/commands/pin.test.js +29 -2
  43. package/dist/tests/commands/preflight/preflight.test.js +31 -17
  44. package/dist/tests/commands/registry/install-bundles.test.js +6 -6
  45. package/dist/tests/commands/sensors/init-pack-unavailable.test.js +96 -0
  46. package/dist/tests/commands/sensors/install-settings-safety.test.js +87 -0
  47. package/dist/tests/commands/sensors/run-is-read-only.test.js +0 -0
  48. package/dist/tests/commands/sensors/run.test.js +24 -51
  49. package/dist/tests/commands/sensors/status-windows.test.js +39 -22
  50. package/dist/tests/commands/sensors/status.test.js +27 -12
  51. package/dist/tests/core/artifact-name-containment.test.js +86 -0
  52. package/dist/tests/core/context/agents-md-single-slot.test.js +69 -0
  53. package/dist/tests/core/context/regenerate.test.js +39 -8
  54. package/dist/tests/core/diagnostics/checks.test.js +14 -6
  55. package/dist/tests/core/diagnostics/provider-tier.test.js +3 -3
  56. package/dist/tests/core/diagnostics/rendered-artifact-visibility.test.js +69 -0
  57. package/dist/tests/core/init/all-baseline-bundles.test.js +64 -0
  58. package/dist/tests/core/init/context-injection-no-project.test.js +117 -0
  59. package/dist/tests/core/init/orchestrator.test.js +1 -1
  60. package/dist/tests/core/init/steps-registry-sync.test.js +1 -1
  61. package/dist/tests/core/init/steps.test.js +11 -5
  62. package/dist/tests/core/path-resolution-no-shell.test.js +136 -0
  63. package/dist/tests/core/paths.test.js +7 -34
  64. package/dist/tests/core/project-skill-links.test.js +159 -0
  65. package/dist/tests/core/provider-artifacts.test.js +66 -14
  66. package/dist/tests/core/provider-capability-guards.test.js +92 -0
  67. package/dist/tests/core/reconciliation.test.js +15 -15
  68. package/dist/tests/core/registry-manifest.test.js +1 -1
  69. package/dist/tests/core/semver-fails-closed.test.js +30 -0
  70. package/dist/tests/core/skill-integrity.test.js +10 -10
  71. package/dist/tests/core/skill-repair-safety.test.js +92 -0
  72. package/dist/tests/core/sync-profile-atomicity.test.js +122 -0
  73. package/dist/tests/integration/copilot-init-isolated.test.js +2 -2
  74. package/dist/tests/providers/index.test.js +1 -1
  75. package/dist/tests/structural/renderer-table-is-single-source.test.js +95 -0
  76. package/dist/tests/structural/symlink-type-is-explicit.test.js +73 -0
  77. package/dist/tests/utils/registry-view-overrides.test.js +2 -2
  78. package/package.json +4 -1
@@ -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';
@@ -9,6 +9,7 @@ const fs_1 = __importDefault(require("fs"));
9
9
  const path_1 = __importDefault(require("path"));
10
10
  const child_process_1 = require("child_process");
11
11
  const providers_1 = require("../../providers");
12
+ const registry_1 = require("../renderers/registry");
12
13
  const registries_1 = require("../registries");
13
14
  const orchestrator_1 = require("../context/orchestrator");
14
15
  const status_1 = require("../../commands/hooks/status");
@@ -31,15 +32,29 @@ function linkState(dir, skill) {
31
32
  return fs_1.default.existsSync(p) ? 'present' : 'broken';
32
33
  return 'present'; // un dir/archivo real también cuenta como presente
33
34
  }
34
- function classifyLinks(skillNames, dir) {
35
+ /** Clasifica artefactos por su presencia en disco.
36
+ *
37
+ * `renderer` NO es opcional a proposito. El escritor (`physicalTarget`) estampa
38
+ * la extension del renderer sobre el nombre — `using-awm.mdc`,
39
+ * `using-awm.instructions.md` — y este lector buscaba el nombre pelado
40
+ * (`using-awm`), asi que para Cursor y Copilot NADA figuraba nunca como
41
+ * instalado: `awm init` reinstalaba el baseline completo en cada corrida (2
42
+ * directorios de backup sin podar por corrida) y `awm doctor` mostraba un rojo
43
+ * permanente cuyo remedio no podia satisfacerlo. Exigir el renderer obliga a
44
+ * cada caller a declarar de que artefacto habla, y `renderedFilename` es la
45
+ * misma funcion que usa el escritor — no pueden volver a discrepar.
46
+ *
47
+ * Los nombres devueltos son los LOGICOS (los de entrada), no los fisicos: los
48
+ * consumidores comparan contra listas de nombres de bundle. */
49
+ function classifyLinks(names, dir, renderer) {
35
50
  const linked = [];
36
51
  const broken = [];
37
- for (const s of skillNames) {
38
- const st = linkState(dir, s);
52
+ for (const name of names) {
53
+ const st = linkState(dir, (0, registry_1.renderedFilename)(name, renderer));
39
54
  if (st === 'present')
40
- linked.push(s);
55
+ linked.push(name);
41
56
  else if (st === 'broken')
42
- broken.push(s);
57
+ broken.push(name);
43
58
  }
44
59
  return { linked, broken };
45
60
  }
@@ -91,7 +106,7 @@ function gatherContextInjection() {
91
106
  }
92
107
  return out;
93
108
  }
94
- function gatherMachine(bundles, agent = 'claude-code') {
109
+ function gatherMachine(bundles, agent = 'claude-code', projectRoot) {
95
110
  // registryCache: estado del primer registry configurado (baseline en una máquina sembrada)
96
111
  const regs = (0, registries_1.listRegistries)();
97
112
  const first = regs[0];
@@ -105,21 +120,33 @@ function gatherMachine(bundles, agent = 'claude-code') {
105
120
  // caught while building this task's real end-to-end init test).
106
121
  let hookPresent = false;
107
122
  let hookDegraded = false;
108
- try {
109
- const hs = (0, status_1.computeHookStatus)(agent);
110
- hookPresent = hs.checks.settingsEntry.ok;
111
- hookDegraded = hs.overall === 'DEGRADED';
123
+ // La aplicabilidad se computa DONDE el provider esta en alcance, no se
124
+ // infiere de que `computeHookStatus` haya tirado (un throw puede ser "no
125
+ // soporta hooks" o un error real, y confundirlos fue el bug).
126
+ const hookApplicable = (0, providers_1.providerFor)(agent).hooks !== undefined;
127
+ if (hookApplicable) {
128
+ try {
129
+ const hs = (0, status_1.computeHookStatus)(agent);
130
+ hookPresent = hs.checks.settingsEntry.ok;
131
+ hookDegraded = hs.overall === 'DEGRADED';
132
+ }
133
+ catch { /* soporta hooks pero no se pudo leer el estado → ausente */ }
112
134
  }
113
- catch { /* sin soporte de hooks → ausente */ }
114
135
  // devCore (bundle baseline) — skillsDir is null only for providers with no global skill
115
136
  // discovery mechanism (today: Copilot); for those, there is no global-scope devCore
116
137
  // concept to satisfy at all, so it's treated as trivially satisfied (see below).
117
138
  const skillsDir = (0, providers_1.providerFor)(agent).skill.global;
118
- const baseline = bundles.find((b) => b.scope === 'baseline');
139
+ // TODOS los baseline, igual que `stepDevCore` los instala y que
140
+ // `reconciliation.ts` los reconcilia. Con `find`, el hecho que decide si el
141
+ // baseline esta satisfecho miraba solo el primero, asi que un segundo bundle
142
+ // baseline sin instalar reportaba `healthy` — y el step, que lee este mismo
143
+ // hecho, nunca lo instalaba.
144
+ const baselines = bundles.filter((b) => b.scope === 'baseline');
145
+ const baseline = baselines[0];
119
146
  let devCorePresent = false;
120
147
  let brokenLinks = [];
121
148
  if (baseline) {
122
- const skillNames = (0, bundles_1.resolveBundleSkills)(baseline.name, bundles);
149
+ const skillNames = [...new Set(baselines.flatMap((b) => (0, bundles_1.resolveBundleSkills)(b.name, bundles)))];
123
150
  if (skillsDir === null) {
124
151
  // No global skill directory for this agent (today: Copilot) — there is no
125
152
  // global-scope devCore/baseline-bundle concept to satisfy for it at all, so
@@ -131,10 +158,28 @@ function gatherMachine(bundles, agent = 'claude-code') {
131
158
  // (init/steps.ts) would fall through on every run and call installBundle at
132
159
  // global scope, which throws (skill.global === null), rolling back the
133
160
  // ENTIRE `awm init -a copilot` transaction, 100% of the time.
134
- devCorePresent = true;
161
+ // Sin directorio global de skills (hoy: Copilot) el baseline se
162
+ // instala a nivel PROYECTO — `stepDevCore` lo hace con
163
+ // scopeOverride 'local'. Antes esto se declaraba "trivialmente
164
+ // satisfecho" incondicionalmente: eso evitaba el crash-loop del
165
+ // install global, pero significaba que Copilot terminaba con CERO
166
+ // skills mientras init imprimia `✔ dev-core` y doctor decia
167
+ // `healthy`. Ahora se mira donde el artefacto REALMENTE va.
168
+ if (projectRoot) {
169
+ const localDir = path_1.default.join(projectRoot, (0, providers_1.providerFor)(agent).skill.local);
170
+ const { linked, broken } = classifyLinks(skillNames, localDir, (0, providers_1.providerFor)(agent).skill.renderer);
171
+ const absent = skillNames.filter((s) => !linked.includes(s) && !broken.includes(s));
172
+ devCorePresent = skillNames.length > 0 && (linked.length + broken.length) > 0;
173
+ brokenLinks = [...broken, ...absent];
174
+ }
175
+ else {
176
+ // Fuera de un proyecto no hay destino local: no hay nada que
177
+ // reclamar como faltante.
178
+ devCorePresent = true;
179
+ }
135
180
  }
136
181
  else {
137
- const { linked, broken } = classifyLinks(skillNames, skillsDir);
182
+ const { linked, broken } = classifyLinks(skillNames, skillsDir, (0, providers_1.providerFor)(agent).skill.renderer);
138
183
  const absent = skillNames.filter((s) => !linked.includes(s) && !broken.includes(s));
139
184
  devCorePresent = skillNames.length > 0 && (linked.length + broken.length) > 0;
140
185
  brokenLinks = [...broken, ...absent];
@@ -151,12 +196,24 @@ function gatherMachine(bundles, agent = 'claude-code') {
151
196
  // (tests/integration/codex-provider-isolated.test.ts).
152
197
  const agentConfig = (0, providers_1.providerFor)(agent).agent;
153
198
  if (agentConfig && agentConfig.global !== null) {
154
- const agentNames = (0, bundles_1.resolveBundleAgents)(baseline.name, bundles);
199
+ const agentNames = [...new Set(baselines.flatMap((b) => (0, bundles_1.resolveBundleAgents)(b.name, bundles)))];
155
200
  if (agentNames.length > 0) {
156
- const filenames = agentNames.map((n) => agentConfig.renderer === 'codex-agent-toml' ? `${n}.toml` : n);
157
- const { linked: agentLinked, broken: agentBroken } = classifyLinks(filenames, agentConfig.global);
201
+ // El installName real de un artefacto `agent` es `<n>.md`
202
+ // (bundle-install.ts), y el renderer decide la extension final:
203
+ // `.toml` para codex, `.md` tal cual para los renderers `link`.
204
+ // El ternario anterior devolvia `n` pelado para `link`, asi que
205
+ // `machine.devCore` quedaba permanentemente roto para
206
+ // claude-code — el provider por DEFECTO — contra el registry real.
207
+ const filenames = agentNames.map((n) => `${n}.md`);
208
+ const { linked: agentLinked, broken: agentBroken } = classifyLinks(filenames, agentConfig.global, agentConfig.renderer);
158
209
  const agentAbsent = filenames.filter((f) => !agentLinked.includes(f) && !agentBroken.includes(f));
159
- brokenLinks = [...brokenLinks, ...agentBroken, ...agentAbsent];
210
+ // `brokenLinks` es de diagnostico: se le muestra al usuario para
211
+ // que vaya a buscar ese archivo. Por eso se reportan los nombres
212
+ // FISICOS (`development-process.toml`), aunque la clasificacion
213
+ // trabaje con los logicos — que es lo que necesitan las
214
+ // comparaciones por cantidad de arriba.
215
+ const physical = (n) => (0, registry_1.renderedFilename)(n, agentConfig.renderer);
216
+ brokenLinks = [...brokenLinks, ...agentBroken.map(physical), ...agentAbsent.map(physical)];
160
217
  }
161
218
  }
162
219
  }
@@ -183,17 +240,17 @@ function gatherMachine(bundles, agent = 'claude-code') {
183
240
  const skillNames = (0, bundles_1.resolveBundleSkills)(name, bundles);
184
241
  if (skillNames.length === 0)
185
242
  return false;
186
- const { linked } = classifyLinks(skillNames, skillsDir);
243
+ const { linked } = classifyLinks(skillNames, skillsDir, (0, providers_1.providerFor)(agent).skill.renderer);
187
244
  return linked.length === skillNames.length;
188
245
  });
189
246
  return {
190
247
  registryCache: { present: cachePresent, gitState },
191
- hook: { present: hookPresent, degraded: hookDegraded },
248
+ hook: { present: hookPresent, degraded: hookDegraded, applicable: hookApplicable },
192
249
  devCore: { present: devCorePresent, brokenLinks },
193
250
  ambient: { wanted, installed },
194
251
  contextInjection: gatherContextInjection(),
195
252
  globalSkills: skillsDir !== null
196
- ? (0, skill_integrity_1.classifyGlobalSkills)(skillsDir, (0, registries_1.contentRoots)())
253
+ ? (0, skill_integrity_1.classifySkillLinks)(skillsDir, (0, registries_1.contentRoots)())
197
254
  : { valid: [], repairable: [], dead: [] },
198
255
  };
199
256
  }
@@ -207,7 +264,15 @@ function gatherProject(root, bundles, agent = 'claude-code') {
207
264
  if (!expected.includes(s))
208
265
  expected.push(s);
209
266
  }
210
- const { linked, broken } = classifyLinks(expected, localSkillsDir);
267
+ const { linked, broken } = classifyLinks(expected, localSkillsDir, (0, providers_1.providerFor)(agent).skill.renderer);
268
+ // Huerfanos: symlinks colgantes que el profile ya no reclama. Se clasifican sobre
269
+ // el dir REAL, no sobre la lista esperada — que es justamente lo que `linked`/
270
+ // `broken` de arriba no pueden ver. `awm sync` los cura o los poda.
271
+ const orphanScan = (0, skill_integrity_1.classifySkillLinks)(localSkillsDir, (0, registries_1.contentRoots)());
272
+ const orphanLinks = {
273
+ repairable: orphanScan.repairable.filter((n) => !expected.includes(n)),
274
+ dead: orphanScan.dead.filter((n) => !expected.includes(n)),
275
+ };
211
276
  let context = { present: false };
212
277
  if (fs_1.default.existsSync(path_1.default.join(root, 'CLAUDE.md')))
213
278
  context = { present: true, file: 'CLAUDE.md' };
@@ -217,6 +282,7 @@ function gatherProject(root, bundles, agent = 'claude-code') {
217
282
  root,
218
283
  profile: { present: profilePresent, extensions: profile.extensions },
219
284
  activeBundles: { expected, linked, broken },
285
+ orphanLinks,
220
286
  sensors: { present: fs_1.default.existsSync(path_1.default.join(root, '.awm', 'sensors.json')) },
221
287
  constitution: { present: fs_1.default.existsSync(path_1.default.join(root, 'CONSTITUTION.md')) },
222
288
  context,
@@ -228,9 +294,9 @@ function gatherContext(opts = {}) {
228
294
  const agent = opts.agent ?? 'claude-code';
229
295
  const root = (0, profile_1.findProjectRoot)(cwd);
230
296
  const agents = opts.agents ?? [agent];
231
- const scanSkills = opts.scanSkills ?? ((dir) => (0, skill_integrity_1.classifyGlobalSkills)(dir, (0, registries_1.contentRoots)()));
297
+ const scanSkills = opts.scanSkills ?? ((dir) => (0, skill_integrity_1.classifySkillLinks)(dir, (0, registries_1.contentRoots)()));
232
298
  return {
233
- machine: gatherMachine(bundles, agent),
299
+ machine: gatherMachine(bundles, agent, root ?? undefined),
234
300
  project: root ? gatherProject(root, bundles, agent) : null,
235
301
  providers: (0, provider_checks_1.gatherProviderChecks)(agents, scanSkills, root ?? undefined),
236
302
  };