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
@@ -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
  };
@@ -17,19 +17,12 @@ exports.gatherProviderChecks = gatherProviderChecks;
17
17
  const fs_1 = __importDefault(require("fs"));
18
18
  const path_1 = __importDefault(require("path"));
19
19
  const providers_1 = require("../../providers");
20
+ const skill_integrity_1 = require("../skill-integrity");
20
21
  const provider_version_1 = require("../provider-version");
21
22
  const status_1 = require("../../commands/hooks/status");
22
23
  const orchestrator_1 = require("../context/orchestrator");
23
24
  const registries_1 = require("../registries");
24
- /** File extension a healthy AWM install actually produces for each non-`'link'` renderer —
25
- * used by `skillsGlobalCheck` to require AWM-shaped evidence, not just an arbitrary
26
- * non-empty directory (a user's own unrelated file in `~/.cursor/rules` would otherwise
27
- * read as `'supported'`). Renderers absent from this map (i.e. `'link'`) never reach the
28
- * branch that reads it. */
29
- const RENDERED_SKILL_EXTENSIONS = {
30
- 'cursor-mdc': '.mdc',
31
- 'copilot-instructions': '.instructions.md',
32
- };
25
+ const registry_1 = require("../renderers/registry");
33
26
  /** Structural classification, computed purely from `provider`'s config shape — see
34
27
  * `ProviderTier`'s doc comment in `types.ts` for what each tier means. */
35
28
  function providerTier(provider) {
@@ -66,7 +59,7 @@ function binaryVersionCheck(agent) {
66
59
  * `dir` is null — i.e. the provider has no global skill discovery mechanism at all
67
60
  * (today: Copilot, see `globalUnsupportedReason` in providers/index.ts).
68
61
  *
69
- * `renderer` gates which verification is possible: `classifyGlobalSkills` (via `integrity`)
62
+ * `renderer` gates which verification is possible: `classifySkillLinks` (via `integrity`)
70
63
  * only ever sees symlinks — `if (!lst.isSymbolicLink()) continue;` — so for a rendered
71
64
  * format (`cursor-mdc`, `copilot-instructions`) it scans a directory of real files and
72
65
  * finds nothing, which would make `broken` silently read 0 regardless of whether the
@@ -87,7 +80,7 @@ function skillsGlobalCheck(dir, owners, integrity, renderer) {
87
80
  // `remediationCode: 'awm-init'` is a worse remedy than none, but this
88
81
  // check has no channel to report "can't tell" separately from "absent"
89
82
  // (same tradeoff already made by this file's agentsNativeCheck and by
90
- // skill-integrity.ts's classifyGlobalSkills — a systemic, pre-existing
83
+ // skill-integrity.ts's classifySkillLinks — a systemic, pre-existing
91
84
  // pattern in this codebase, not introduced here).
92
85
  entries = [];
93
86
  }
@@ -97,7 +90,12 @@ function skillsGlobalCheck(dir, owners, integrity, renderer) {
97
90
  // Still not full integrity verification (a stray file with the right extension but
98
91
  // wrong content still passes) — that residual gap is the same honest tradeoff this
99
92
  // function's doc comment already accepts for the non-`'link'` branch generally.
100
- const ext = RENDERED_SKILL_EXTENSIONS[renderer];
93
+ // `rendererExtension` (core/renderers/registry.ts) is the ONE table mapping a
94
+ // renderer to the file it produces. This site used to keep its own partial copy —
95
+ // the fourth such copy in the codebase, and the exact drift that made a rendered
96
+ // artifact invisible to the "is it installed" check for a whole release. A new
97
+ // renderer must not be able to be added without this reading it.
98
+ const ext = (0, registry_1.rendererExtension)(renderer);
101
99
  const present = ext ? entries.some((e) => e.endsWith(ext)) : entries.length > 0;
102
100
  return {
103
101
  id: 'skills.global',
@@ -165,20 +163,69 @@ function tomlAgentsHealthy(dir, entries) {
165
163
  * run (e.g. claude-code-only, opencode-only, antigravity-only) would always render
166
164
  * inapplicable rows as red ✖ and `overall` could never be 'healthy'.
167
165
  */
166
+ /**
167
+ * Workflows installed at machine scope. Today only Antigravity declares a `workflow`
168
+ * config at all (`~/.gemini/antigravity/global_workflows`), and nothing verified it:
169
+ * `awm init` installed the baseline's workflows there and every diagnostic looked only
170
+ * at skills and native agents, so a broken or emptied workflow directory reported
171
+ * `healthy` forever on the one provider that uses it.
172
+ *
173
+ * Same N/A discipline as `agentsNativeCheck`: a provider with no workflow config, or a
174
+ * registry that ships no `workflows/`, emits NO row rather than a red one nobody can
175
+ * act on — an absent row means "nothing to verify", not "verified fine".
176
+ */
177
+ function workflowsGlobalCheck(agent) {
178
+ const provider = (0, providers_1.providerFor)(agent);
179
+ if (!provider.workflow || provider.workflow.global === null)
180
+ return null;
181
+ const dir = provider.workflow.global;
182
+ let entries;
183
+ try {
184
+ entries = fs_1.default.readdirSync(dir);
185
+ }
186
+ catch {
187
+ return null;
188
+ }
189
+ if (entries.length === 0)
190
+ return null;
191
+ // Los workflows se instalan con el renderer `link`, asi que un symlink colgante es
192
+ // exactamente la misma clase de rotura que en skills — y se clasifica con la misma
193
+ // funcion, no con una copia local que pueda divergir.
194
+ const integrity = (0, skill_integrity_1.classifySkillLinks)(dir, (0, registries_1.contentRoots)());
195
+ const broken = integrity.repairable.length + integrity.dead.length;
196
+ return {
197
+ id: 'workflows.global',
198
+ state: broken > 0 ? 'broken' : 'healthy',
199
+ target: dir,
200
+ detail: broken > 0 ? `${broken} broken link(s)` : undefined,
201
+ remediationCode: broken > 0 ? 'awm-init' : undefined,
202
+ };
203
+ }
168
204
  function agentsNativeCheck(agent) {
169
205
  const provider = (0, providers_1.providerFor)(agent);
170
206
  if (!provider.agent || provider.agent.global === null)
171
207
  return null;
172
208
  const dir = provider.agent.global;
173
209
  let entries;
210
+ // `absent` degrada el estado global, asi que sin `remediationCode` doctor
211
+ // salia 1 sin decir que hacer — y para un registry que simplemente no trae
212
+ // `agents/` (lo normal) ese rojo no tiene accion posible. Se reporta como
213
+ // `unsupported`, que describe la realidad: no hay artefactos nativos que
214
+ // verificar, y no es culpa de la instalacion.
215
+ // Un registry que simplemente no trae `agents/` es lo normal, no un defecto
216
+ // de la instalacion — y no hay accion que el usuario pueda tomar. Antes esto
217
+ // devolvia `absent`, un estado que DEGRADA, y sin `remediationCode`: doctor
218
+ // salia 1 mostrando `✖ native agents` sin decir que hacer, justo despues de
219
+ // un `awm init` exitoso. Cuando no hay nada nativo que verificar, no se
220
+ // emite fila — el mismo criterio que ya usan los demas casos N/A de aca.
174
221
  try {
175
222
  entries = fs_1.default.readdirSync(dir);
176
223
  }
177
224
  catch {
178
- return { id: 'agents.native', state: 'absent', target: dir };
225
+ return null;
179
226
  }
180
227
  if (entries.length === 0)
181
- return { id: 'agents.native', state: 'absent', target: dir };
228
+ return null;
182
229
  if (provider.agent.renderer === 'codex-agent-toml') {
183
230
  const { broken } = tomlAgentsHealthy(dir, entries);
184
231
  return {
@@ -292,6 +339,7 @@ function gatherProviderChecks(agents, scanSkills, projectRoot) {
292
339
  binaryVersionCheck(agent),
293
340
  skillsGlobalCheck(dir, owners, integrity, provider.skill.renderer),
294
341
  agentsNativeCheck(agent),
342
+ workflowsGlobalCheck(agent),
295
343
  hookTrustCheck(agent),
296
344
  contextGlobalCheck(agent, projectRoot),
297
345
  ].filter((check) => check !== null);
@@ -94,7 +94,7 @@ function discoverWorkflows(roots = (0, registries_1.contentRoots)()) {
94
94
  for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
95
95
  if (entry.isDirectory() || !entry.name.endsWith('.md'))
96
96
  continue;
97
- const name = entry.name.replace('.md', '');
97
+ const name = entry.name.replace(/\.md$/, '');
98
98
  const filePath = path_1.default.join(dir, entry.name);
99
99
  mergeEntry('workflow', byName, {
100
100
  name,
@@ -120,7 +120,7 @@ function discoverAgents(roots = (0, registries_1.contentRoots)()) {
120
120
  for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
121
121
  if (entry.isDirectory() || !entry.name.endsWith('.md'))
122
122
  continue;
123
- const name = entry.name.replace('.md', '');
123
+ const name = entry.name.replace(/\.md$/, '');
124
124
  const filePath = path_1.default.join(dir, entry.name);
125
125
  mergeEntry('agent', byName, {
126
126
  name,
@@ -17,6 +17,7 @@ exports.planInitMutationTargets = planInitMutationTargets;
17
17
  // entry when nothing exists there yet). The one failure mode that matters is
18
18
  // under-enumeration: a real write to a path NOT in this list would land
19
19
  // outside the backup session and survive a rollback.
20
+ const fs_1 = __importDefault(require("fs"));
20
21
  const path_1 = __importDefault(require("path"));
21
22
  const providers_1 = require("../../providers");
22
23
  const bundles_1 = require("../bundles");
@@ -40,6 +41,17 @@ function physicalTarget(type, agent, scope, installName, projectRoot) {
40
41
  return null;
41
42
  }
42
43
  }
44
+ /** La forma canonica de un directorio: resuelve symlinks si existe, y si no existe
45
+ * todavia devuelve la entrada tal cual — enumerar un destino aun inexistente es
46
+ * deliberado en este modulo (un backup vacio no es un problema; no enumerarlo si). */
47
+ function canonicalDir(dir) {
48
+ try {
49
+ return fs_1.default.realpathSync(dir);
50
+ }
51
+ catch {
52
+ return dir;
53
+ }
54
+ }
43
55
  function addBundleTargets(targets, bundleName, bundles, agent, scope, projectRoot, contentDir) {
44
56
  let intents;
45
57
  try {
@@ -72,7 +84,17 @@ function addBundleTargets(targets, bundleName, bundles, agent, scope, projectRoo
72
84
  * read the project profile, but never writes).
73
85
  */
74
86
  function planInitMutationTargets(params) {
75
- const { cwd, agent, bundles } = params;
87
+ const { agent, bundles } = params;
88
+ // `cwd` canonico ANTES de derivar nada de el. Algunos destinos salen de `cwd` tal
89
+ // cual y otros de `findProjectRoot(cwd)`, que hace `realpathSync`: mientras las dos
90
+ // formas coincidan el `Set` deduplica y no se nota, pero en macOS `/var/folders/…`
91
+ // es un symlink a `/private/var/folders/…` y entonces NO coinciden — la CI de macOS
92
+ // mostro `AGENTS.md` y `.awm/context/awm-context.md` dos veces, una por forma.
93
+ //
94
+ // Esta lista es la que `beginBackupSession` respalda y la que el rollback restaura.
95
+ // Un archivo con dos entradas es un archivo respaldado dos veces y restaurado dos
96
+ // veces, en el mecanismo cuyo unico trabajo es dejar el disco como estaba.
97
+ const cwd = canonicalDir(params.cwd);
76
98
  const targets = new Set();
77
99
  // preferences.json + the artifact ownership ledger (state/artifacts.json)
78
100
  targets.add(path_1.default.join((0, paths_1.awmHome)(), 'preferences.json'));
@@ -110,6 +132,17 @@ function planInitMutationTargets(params) {
110
132
  for (const b of machineBundles) {
111
133
  addBundleTargets(targets, b.name, bundles, agent, 'global', cwd, contentDir);
112
134
  }
135
+ // Providers whose context injection is itself project-scope (managed-agents-md
136
+ // with a null globalPath — Cursor, Copilot) write AGENTS.md and the materialized
137
+ // .awm/context/ file. `stepContextInjection` now refuses to write those without a
138
+ // discovered project root, but enumerating them against `cwd` unconditionally
139
+ // costs nothing (a target that goes unwritten is a no-op backup entry, per this
140
+ // module's opening note) and keeps the guarantee from resting on a single site:
141
+ // under-enumeration here is the one failure mode that silently defeats rollback.
142
+ if (injection?.type === 'managed-agents-md' && injection.globalPath === null) {
143
+ targets.add(path_1.default.join(cwd, path_1.default.basename(injection.localFile)));
144
+ targets.add((0, materializer_1.projectContextPath)(cwd));
145
+ }
113
146
  // project-level: profile, sensors manifest, project injection, and every
114
147
  // extension currently recorded in .awm/profile.json
115
148
  const projectRoot = (0, profile_1.findProjectRoot)(cwd);
@@ -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,