agentic-workflow-manager 3.13.4 → 3.13.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/src/commands/add.js +5 -0
  2. package/dist/src/commands/doctor.js +1 -0
  3. package/dist/src/commands/hooks/claude.js +15 -2
  4. package/dist/src/commands/hooks/shared.js +6 -1
  5. package/dist/src/commands/init.js +1 -0
  6. package/dist/src/commands/pin.js +24 -4
  7. package/dist/src/commands/preflight/checks.js +3 -2
  8. package/dist/src/commands/registry/add.js +1 -1
  9. package/dist/src/commands/sensors/index.js +7 -0
  10. package/dist/src/commands/sensors/init.js +48 -7
  11. package/dist/src/commands/sensors/install.js +20 -11
  12. package/dist/src/commands/sensors/run.js +31 -29
  13. package/dist/src/commands/sync.js +29 -0
  14. package/dist/src/commands/watch/index.js +11 -1
  15. package/dist/src/core/artifact-name.js +80 -0
  16. package/dist/src/core/bundle-install.js +55 -12
  17. package/dist/src/core/bundles.js +72 -13
  18. package/dist/src/core/context/regenerate.js +21 -4
  19. package/dist/src/core/context/strategies/codex-agents.js +18 -2
  20. package/dist/src/core/diagnostics/checks.js +26 -2
  21. package/dist/src/core/diagnostics/context.js +91 -25
  22. package/dist/src/core/diagnostics/provider-checks.js +62 -14
  23. package/dist/src/core/discovery.js +15 -23
  24. package/dist/src/core/export/transform.js +30 -25
  25. package/dist/src/core/frontmatter.js +223 -0
  26. package/dist/src/core/init/mutation-targets.js +11 -0
  27. package/dist/src/core/init/steps.js +70 -19
  28. package/dist/src/core/install-planner.js +62 -17
  29. package/dist/src/core/install-transaction.js +13 -1
  30. package/dist/src/core/journal/adapter.js +10 -1
  31. package/dist/src/core/paths.js +61 -8
  32. package/dist/src/core/profile.js +12 -0
  33. package/dist/src/core/provider-artifacts.js +38 -13
  34. package/dist/src/core/provider-version.js +12 -6
  35. package/dist/src/core/registries.js +40 -19
  36. package/dist/src/core/renderers/canonical-agent.js +33 -2
  37. package/dist/src/core/renderers/registry.js +37 -0
  38. package/dist/src/core/renderers/skill-source.js +11 -19
  39. package/dist/src/core/skill-integrity.js +71 -10
  40. package/dist/src/core/update-check.js +14 -2
  41. package/dist/src/core/versioning.js +17 -2
  42. package/dist/src/index.js +31 -23
  43. package/dist/src/providers/index.js +7 -2
  44. package/dist/src/ui/text.js +10 -2
  45. package/dist/src/utils/config.js +7 -8
  46. package/dist/tests/commands/doctor.test.js +6 -0
  47. package/dist/tests/commands/pin.test.js +29 -2
  48. package/dist/tests/commands/preflight/preflight.test.js +31 -17
  49. package/dist/tests/commands/registry/install-bundles.test.js +6 -6
  50. package/dist/tests/commands/sensors/init-pack-unavailable.test.js +96 -0
  51. package/dist/tests/commands/sensors/install-settings-safety.test.js +87 -0
  52. package/dist/tests/commands/sensors/run-is-read-only.test.js +0 -0
  53. package/dist/tests/commands/sensors/run.test.js +24 -51
  54. package/dist/tests/commands/sensors/status-windows.test.js +39 -22
  55. package/dist/tests/commands/sensors/status.test.js +27 -12
  56. package/dist/tests/core/artifact-name-containment.test.js +86 -0
  57. package/dist/tests/core/context/agents-md-single-slot.test.js +69 -0
  58. package/dist/tests/core/context/regenerate.test.js +39 -8
  59. package/dist/tests/core/diagnostics/checks.test.js +14 -6
  60. package/dist/tests/core/diagnostics/provider-tier.test.js +3 -3
  61. package/dist/tests/core/diagnostics/rendered-artifact-visibility.test.js +69 -0
  62. package/dist/tests/core/discovery.test.js +10 -1
  63. package/dist/tests/core/export/transform.test.js +110 -21
  64. package/dist/tests/core/frontmatter-description-vs-yaml.test.js +118 -0
  65. package/dist/tests/core/init/all-baseline-bundles.test.js +64 -0
  66. package/dist/tests/core/init/context-injection-no-project.test.js +117 -0
  67. package/dist/tests/core/init/orchestrator.test.js +1 -1
  68. package/dist/tests/core/init/steps-registry-sync.test.js +1 -1
  69. package/dist/tests/core/init/steps.test.js +11 -5
  70. package/dist/tests/core/path-resolution-no-shell.test.js +136 -0
  71. package/dist/tests/core/paths.test.js +7 -34
  72. package/dist/tests/core/project-skill-links.test.js +159 -0
  73. package/dist/tests/core/provider-artifacts.test.js +66 -14
  74. package/dist/tests/core/provider-capability-guards.test.js +92 -0
  75. package/dist/tests/core/reconciliation.test.js +15 -15
  76. package/dist/tests/core/registry-manifest.test.js +1 -1
  77. package/dist/tests/core/renderers/canonical-agent.test.js +36 -0
  78. package/dist/tests/core/renderers/cursor-mdc.test.js +37 -7
  79. package/dist/tests/core/renderers/skill-source-block-scalar.test.js +117 -0
  80. package/dist/tests/core/semver-fails-closed.test.js +30 -0
  81. package/dist/tests/core/skill-integrity.test.js +10 -10
  82. package/dist/tests/core/skill-repair-safety.test.js +92 -0
  83. package/dist/tests/core/sync-profile-atomicity.test.js +122 -0
  84. package/dist/tests/integration/copilot-init-isolated.test.js +2 -2
  85. package/dist/tests/providers/index.test.js +1 -1
  86. package/dist/tests/structural/renderer-table-is-single-source.test.js +95 -0
  87. package/dist/tests/structural/symlink-type-is-explicit.test.js +73 -0
  88. package/dist/tests/ui/text.test.js +9 -0
  89. package/dist/tests/utils/registry-view-overrides.test.js +2 -2
  90. package/package.json +6 -1
@@ -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);
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.matchFrontmatterBlock = matchFrontmatterBlock;
6
+ exports.findFrontmatterDescription = exports.isBlockScalarHeader = exports.readFrontmatterDescription = exports.matchFrontmatterBlock = void 0;
7
7
  exports.readArtifactDescription = readArtifactDescription;
8
8
  exports.discoverSkills = discoverSkills;
9
9
  exports.discoverWorkflows = discoverWorkflows;
@@ -12,31 +12,23 @@ exports.discoverAgents = discoverAgents;
12
12
  const fs_1 = __importDefault(require("fs"));
13
13
  const path_1 = __importDefault(require("path"));
14
14
  const registries_1 = require("./registries");
15
- /** Extracts the raw frontmatter text (between the --- delimiters), or null if the block is missing/malformed. CRLF-tolerant. */
16
- function matchFrontmatterBlock(raw) {
17
- const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
18
- return fmMatch ? fmMatch[1] : null;
19
- }
15
+ // El parseo de frontmatter vive en el modulo HOJA `core/frontmatter.ts`
16
+ // (sin imports, para que consumidores puros como export/transform.ts no
17
+ // arrastren fs/git por transitividad). Se re-exporta aca porque este modulo
18
+ // ya era el punto de entrada historico para esos helpers.
19
+ var frontmatter_1 = require("./frontmatter");
20
+ Object.defineProperty(exports, "matchFrontmatterBlock", { enumerable: true, get: function () { return frontmatter_1.matchFrontmatterBlock; } });
21
+ Object.defineProperty(exports, "readFrontmatterDescription", { enumerable: true, get: function () { return frontmatter_1.readFrontmatterDescription; } });
22
+ Object.defineProperty(exports, "isBlockScalarHeader", { enumerable: true, get: function () { return frontmatter_1.isBlockScalarHeader; } });
23
+ Object.defineProperty(exports, "findFrontmatterDescription", { enumerable: true, get: function () { return frontmatter_1.findFrontmatterDescription; } });
24
+ const frontmatter_2 = require("./frontmatter");
20
25
  function readArtifactDescription(filePath) {
21
26
  try {
22
27
  const raw = fs_1.default.readFileSync(filePath, 'utf-8');
23
- const frontmatter = matchFrontmatterBlock(raw);
28
+ const frontmatter = (0, frontmatter_2.matchFrontmatterBlock)(raw);
24
29
  if (frontmatter === null)
25
30
  return '';
26
- const line = frontmatter
27
- .split(/\r?\n/)
28
- .find((l) => /^description\s*:/.test(l));
29
- if (!line)
30
- return '';
31
- let val = line.replace(/^description\s*:/, '').trim();
32
- if ((val.startsWith('"') && val.endsWith('"')) ||
33
- (val.startsWith("'") && val.endsWith("'"))) {
34
- val = val.slice(1, -1);
35
- }
36
- const BLOCK_INDICATORS = new Set(['>-', '>', '|-', '|', '>+', '|+']);
37
- if (BLOCK_INDICATORS.has(val.trim()))
38
- return '';
39
- return val.trim();
31
+ return (0, frontmatter_2.readFrontmatterDescription)(frontmatter);
40
32
  }
41
33
  catch {
42
34
  return '';
@@ -102,7 +94,7 @@ function discoverWorkflows(roots = (0, registries_1.contentRoots)()) {
102
94
  for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
103
95
  if (entry.isDirectory() || !entry.name.endsWith('.md'))
104
96
  continue;
105
- const name = entry.name.replace('.md', '');
97
+ const name = entry.name.replace(/\.md$/, '');
106
98
  const filePath = path_1.default.join(dir, entry.name);
107
99
  mergeEntry('workflow', byName, {
108
100
  name,
@@ -128,7 +120,7 @@ function discoverAgents(roots = (0, registries_1.contentRoots)()) {
128
120
  for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
129
121
  if (entry.isDirectory() || !entry.name.endsWith('.md'))
130
122
  continue;
131
- const name = entry.name.replace('.md', '');
123
+ const name = entry.name.replace(/\.md$/, '');
132
124
  const filePath = path_1.default.join(dir, entry.name);
133
125
  mergeEntry('agent', byName, {
134
126
  name,
@@ -7,7 +7,10 @@ exports.claudeAiTransform = claudeAiTransform;
7
7
  //
8
8
  // Transform mecánico claude.ai (R3.1): función pura string → string.
9
9
  // Frontmatter line-based plano (los SKILL.md del baseline usan claves de una
10
- // línea) — sin parser YAML a propósito (YAGNI, cero deps).
10
+ // línea) — sin parser YAML a propósito (YAGNI, cero deps). La única forma
11
+ // multilínea soportada es el block scalar de `description:`, resuelto vía la
12
+ // función compartida de discovery.ts (ver claudeAiTransform).
13
+ const frontmatter_1 = require("../frontmatter");
11
14
  const DEFERENCE_LINE = (skillName) => `In environments with AWM installed (Claude Code), defer to the registry's ${skillName} skill — this port is for environments without filesystem access.`;
12
15
  exports.DEFERENCE_LINE = DEFERENCE_LINE;
13
16
  // Paths intra-registry: resuelven en Claude Code (donde el registry está en
@@ -73,34 +76,36 @@ function claudeAiTransform(skillMd, skillName) {
73
76
  const body = skillMd.slice(end + endMatch[0].length);
74
77
  const fmLines = skillMd.slice(startLen, end).split(/\r?\n/)
75
78
  .filter((l) => !/^(version|portable):/.test(l));
76
- const descIdx = fmLines.findIndex((l) => /^description:/.test(l));
77
- if (descIdx === -1) {
79
+ // UN SOLO camino para todas las formas de `description`. Antes habia uno por
80
+ // forma (plano / entrecomillado / bloque) y cada vez que el LECTOR aprendia
81
+ // una forma nueva, el ESCRITOR de aca quedaba atras — la divergencia exacta
82
+ // que este modulo dice cerrar. Casos reales que produjo esa asimetria:
83
+ // - escalar plano multilinea: la deference line se insertaba en la primera
84
+ // linea y la continuacion quedaba huerfana debajo, o sea la frase de
85
+ // deference terminaba enterrada en el medio de la descripcion;
86
+ // - escalar plano con ` # comentario` final: la deference line se anexaba
87
+ // DESPUES del `#`, o sea YAML se la comia entera como comentario y el
88
+ // artefacto exportado perdia en silencio la unica frase que este
89
+ // transform existe para agregar.
90
+ // Ahora se resuelve el campo con la funcion compartida, se reemplaza su
91
+ // extension COMPLETA (startLine..endLine) y se emite siempre un escalar
92
+ // double-quoted via JSON.stringify — superset valido de YAML que cubre
93
+ // comillas, `:`, `#` y los `\n` de un literal `|` sin decidir estilo.
94
+ const field = (0, frontmatter_1.findFrontmatterDescription)(fmLines.join('\n'));
95
+ if (field.startLine === -1) {
78
96
  throw new Error('frontmatter has no description field');
79
97
  }
80
- const descLine = fmLines[descIdx];
81
- const value = descLine.slice('description:'.length).trim();
82
- if (value === '' || value === '>' || value === '|' || value.startsWith('>') || value.startsWith('|')) {
83
- throw new Error('description must be single-line (block scalars are not supported by the export transform)');
98
+ const rawValue = fmLines[field.startLine].replace(/^description\s*:/, '').trim();
99
+ // Empieza con `>`/`|` pero no es un indicador bien formado (ej. `>-basura`):
100
+ // YAML mismo lo rechaza. Fallar explicito en vez de publicar basura.
101
+ if (/^[>|]/.test(rawValue) && !(0, frontmatter_1.isBlockScalarHeader)(rawValue)) {
102
+ throw new Error(`malformed block scalar indicator in description: ${rawValue}`);
84
103
  }
85
- const deference = (0, exports.DEFERENCE_LINE)(skillName);
86
- // Quote-style detection mirrors readArtifactDescription in discovery.ts: both
87
- // single- and double-quoted scalars are first-class, and we work off the
88
- // trimmed value so trailing whitespace after a closing quote doesn't fool us.
89
- const isDoubleQuoted = value.length >= 2 && value.startsWith('"') && value.endsWith('"');
90
- const isSingleQuoted = value.length >= 2 && value.startsWith("'") && value.endsWith("'");
91
- if ((value.startsWith('"') || value.startsWith("'")) && !isDoubleQuoted && !isSingleQuoted) {
92
- throw new Error('description has trailing content after its closing quote (e.g. an inline comment) — not supported by the export transform; remove the comment or use a port.claude-ai.md override');
104
+ if (!field.value) {
105
+ throw new Error((0, frontmatter_1.isBlockScalarHeader)(rawValue) ? 'description block scalar has no content' : 'description is empty');
93
106
  }
94
- // YAML single-quoted scalars escape a literal ' by doubling it (''); the
95
- // deference text ("...registry's..." see DEFERENCE_LINE) contains an
96
- // apostrophe, so it must be escaped before splicing into a single-quoted
97
- // description or it would prematurely close the YAML string.
98
- const newValue = isDoubleQuoted
99
- ? `${value.slice(0, -1)} ${deference}"`
100
- : isSingleQuoted
101
- ? `${value.slice(0, -1)} ${deference.replace(/'/g, "''")}'`
102
- : `${value} ${deference}`;
103
- fmLines[descIdx] = `description: ${newValue}`;
107
+ const merged = JSON.stringify(`${field.value} ${(0, exports.DEFERENCE_LINE)(skillName)}`);
108
+ fmLines.splice(field.startLine, field.endLine - field.startLine + 1, `description: ${merged}`);
104
109
  // Solo el body: el frontmatter ya se editó arriba y sus campos no son prosa
105
110
  // navegable (R2.4).
106
111
  return `---\n${fmLines.join('\n')}\n---\n${stripIntraRegistryPaths(body)}`;
@@ -0,0 +1,223 @@
1
+ "use strict";
2
+ // src/core/frontmatter.ts
3
+ //
4
+ // Modulo HOJA: sin imports. Es la unica fuente de verdad para leer el
5
+ // frontmatter YAML de un artefacto (SKILL.md, agents/*.md, workflows/*.md).
6
+ //
7
+ // Por que hoja: sus consumidores incluyen `core/export/transform.ts`, que se
8
+ // documenta como funcion pura string -> string sin dependencias. Si esto
9
+ // viviera en `discovery.ts` (que importa `registries.ts`, y este a su vez
10
+ // resuelve `awmHome()` y carga `simple-git` en tiempo de import), un modulo
11
+ // puro terminaria arrastrando la capa de fs/git por transitividad.
12
+ //
13
+ // Por que existe: el frontmatter se parseaba a mano en CUATRO lugares, cada
14
+ // uno con su propia copia incompleta. Un block scalar YAML valido
15
+ // (`description: >-`, con el texto en las lineas indentadas siguientes)
16
+ // degradaba en silencio en discovery, crasheaba `awm add -a cursor|copilot`,
17
+ // abortaba `awm export <bundle>` entero y rompia `awm add -a codex`. La
18
+ // semantica de aca se verifica contra js-yaml (parser real) en
19
+ // tests/core/frontmatter-description-vs-yaml.test.ts — no contra valores
20
+ // esperados escritos a mano, que pueden envejecer hacia la expectativa
21
+ // equivocada.
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.matchFrontmatterBlock = matchFrontmatterBlock;
24
+ exports.isBlockScalarHeader = isBlockScalarHeader;
25
+ exports.findFrontmatterDescription = findFrontmatterDescription;
26
+ exports.readFrontmatterDescription = readFrontmatterDescription;
27
+ /** Texto crudo del frontmatter (entre los delimitadores `---`), o null si el
28
+ * bloque falta o esta mal formado. Tolera CRLF. */
29
+ function matchFrontmatterBlock(raw) {
30
+ const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
31
+ return fmMatch ? fmMatch[1] : null;
32
+ }
33
+ /** Indicador de block scalar en la linea de la clave: estilo (`>` folded /
34
+ * `|` literal) + chomping opcional (`-`/`+`) + indentacion explicita opcional
35
+ * + comentario final opcional. Detalles verificados contra js-yaml:
36
+ * - la indentacion explicita es UN digito 1-9 (ni `0` ni `10`);
37
+ * - `>- # nota` es valido y su bloque se lee normal;
38
+ * - `>-basura` NO es valido y aca tampoco matchea. */
39
+ const BLOCK_SCALAR_HEADER = /^([>|])(?:([+-])?([1-9])?|([1-9])?([+-])?)(?:\s+#.*)?$/;
40
+ /** ¿El valor de la linea de la clave es un indicador de block scalar?
41
+ * Cualquier caller que RAMIFIQUE sobre "esto es un bloque" debe usar esta
42
+ * funcion, nunca una aproximacion propia: una guarda por prefijo (`/^[>|]/`)
43
+ * y un resolver por match completo se desincronizan, y en ese hueco el
44
+ * escritor borro contenido real creyendo que resolvia un bloque. */
45
+ function isBlockScalarHeader(value) {
46
+ return BLOCK_SCALAR_HEADER.test(value.trim());
47
+ }
48
+ /** Indice de la comilla que CIERRA el escalar que empieza en la posicion 0, o
49
+ * -1 si no cierra. Cada estilo escapa distinto: en single-quoted un `''` es
50
+ * un apostrofe literal (no un cierre); en double-quoted un `\"` es una
51
+ * comilla literal. */
52
+ function findClosingQuote(value, quote) {
53
+ for (let i = 1; i < value.length; i++) {
54
+ if (quote === '"' && value[i] === '\\') {
55
+ i++;
56
+ continue;
57
+ }
58
+ if (value[i] !== quote)
59
+ continue;
60
+ if (quote === "'" && value[i + 1] === "'") {
61
+ i++;
62
+ continue;
63
+ }
64
+ return i;
65
+ }
66
+ return -1;
67
+ }
68
+ function foldBlock(stripped, isLiteral) {
69
+ if (isLiteral)
70
+ return stripped.join('\n').trim();
71
+ // Folded (`>`): las lineas al nivel base del bloque se pliegan entre si con
72
+ // UN espacio; una linea MAS indentada no se pliega (conserva su salto y su
73
+ // sangria — la regla que permite incrustar una lista dentro de un folded);
74
+ // y k lineas en blanco consecutivas producen k saltos de linea.
75
+ // Ojo: NO se hace trim por linea — YAML conserva los espacios finales de
76
+ // cada linea y el plegado agrega el suyo encima.
77
+ let out = '';
78
+ let prevLiteral = false;
79
+ let started = false;
80
+ let blanks = 0;
81
+ for (const line of stripped) {
82
+ if (line === '') {
83
+ blanks++;
84
+ continue;
85
+ }
86
+ const literal = /^[ \t]/.test(line);
87
+ if (!started) {
88
+ out = line;
89
+ started = true;
90
+ prevLiteral = literal;
91
+ blanks = 0;
92
+ continue;
93
+ }
94
+ if (blanks > 0) {
95
+ out += '\n'.repeat(blanks);
96
+ blanks = 0;
97
+ }
98
+ else if (literal || prevLiteral)
99
+ out += '\n';
100
+ else
101
+ out += ' ';
102
+ out += line;
103
+ prevLiteral = literal;
104
+ }
105
+ return out.trim();
106
+ }
107
+ /**
108
+ * Localiza y RESUELVE el campo `description` de un bloque de frontmatter,
109
+ * devolviendo tambien la extension exacta que ocupa (para que un escritor
110
+ * pueda reemplazarlo entero).
111
+ *
112
+ * Formas soportadas, todas contrastadas contra js-yaml: escalar plano (de una
113
+ * o varias lineas), escalar entrecomillado (simple y doble, con su escape
114
+ * propio deshecho), y block scalar folded/literal con chomping e indentacion
115
+ * explicita.
116
+ */
117
+ function findFrontmatterDescription(frontmatter) {
118
+ const lines = frontmatter.split(/\r?\n/);
119
+ // Ancla en COLUMNA 0 a proposito: `description` en un frontmatter es
120
+ // siempre clave de nivel superior. Un `^\s*` haria ganar a un
121
+ // `description:` INDENTADO — anidado en otro mapa, o una linea de
122
+ // CONTENIDO de otro block scalar que casualmente empiece asi.
123
+ const startLine = lines.findIndex((l) => /^description\s*:/.test(l));
124
+ if (startLine === -1)
125
+ return { value: '', startLine: -1, endLine: -1 };
126
+ const raw = lines[startLine].replace(/^description\s*:/, '').trim();
127
+ if (isBlockScalarHeader(raw)) {
128
+ const isLiteral = raw.trim().startsWith('|');
129
+ // El bloque son las lineas ESTRICTAMENTE indentadas respecto de la
130
+ // clave (columna 0). Las lineas en blanco no lo cortan: pertenecen a el.
131
+ const block = [];
132
+ let last = startLine;
133
+ for (let i = startLine + 1; i < lines.length; i++) {
134
+ if (lines[i].trim() === '') {
135
+ block.push(lines[i]);
136
+ continue;
137
+ }
138
+ if (!/^[ \t]/.test(lines[i]))
139
+ break;
140
+ block.push(lines[i]);
141
+ last = i;
142
+ }
143
+ while (block.length > 0 && block[block.length - 1].trim() === '')
144
+ block.pop();
145
+ const first = block.find((l) => l.trim() !== '');
146
+ if (first === undefined)
147
+ return { value: '', startLine, endLine: startLine };
148
+ const declared = raw.match(/[1-9]/);
149
+ const blockIndent = declared !== null ? Number(declared[0]) : first.match(/^[ \t]*/)[0].length;
150
+ // Una linea con MENOS indentacion que el bloque es YAML invalido
151
+ // (js-yaml lanza "bad indentation"). Cortar a ciegas con slice()
152
+ // comeria caracteres reales: preferimos no inventar un valor.
153
+ if (block.some((l) => l.trim() !== '' && l.match(/^[ \t]*/)[0].length < blockIndent)) {
154
+ return { value: '', startLine, endLine: last };
155
+ }
156
+ const stripped = block.map((l) => l.slice(blockIndent));
157
+ return { value: foldBlock(stripped, isLiteral), startLine, endLine: last };
158
+ }
159
+ // Empieza con `>`/`|` pero NO es un indicador bien formado (`>0`, `>12`,
160
+ // `>-basura`): YAML lo rechaza. Devolver '' hace que los consumidores
161
+ // fallen fuerte (throw en parseSkillSource / claudeAiTransform) en vez de
162
+ // caer al camino de escalar plano de abajo, que publicaria el indicador
163
+ // mismo como si fuera la descripcion.
164
+ if (/^[>|]/.test(raw))
165
+ return { value: '', startLine, endLine: startLine };
166
+ // Continuacion multilinea. Aplica a DOS formas, y ambas la necesitan:
167
+ // - escalar plano (`description: primera` + lineas indentadas), que YAML
168
+ // pliega con espacios igual que un `>`;
169
+ // - escalar ENTRECOMILLADO cuya comilla de cierre esta en una linea
170
+ // posterior (`description: "hola` / ` mundo"`), que sin esto devolvia
171
+ // `"hola` — con la comilla de apertura pegada — como descripcion.
172
+ // Sin juntar las lineas primero, del lado escritor ademas quedaban
173
+ // huerfanas en la salida exportada.
174
+ let endLine = startLine;
175
+ let value = raw;
176
+ const opensUnclosedQuote = /^['"]/.test(raw) && findClosingQuote(raw, raw[0]) === -1;
177
+ if (raw !== '' && (!/^['"]/.test(raw) || opensUnclosedQuote)) {
178
+ const cont = [];
179
+ for (let i = startLine + 1; i < lines.length; i++) {
180
+ if (lines[i].trim() === '')
181
+ break;
182
+ if (!/^[ \t]/.test(lines[i]))
183
+ break;
184
+ cont.push(lines[i].trim());
185
+ endLine = i;
186
+ }
187
+ if (cont.length > 0)
188
+ value = [raw, ...cont].join(' ');
189
+ }
190
+ // Escalares entrecomillados. Dos cosas que no alcanzan por separado:
191
+ // 1. Hay que ENCONTRAR la comilla de cierre real antes de mirar el resto:
192
+ // lo que sigue a esa comilla es un comentario, no parte del valor.
193
+ // Comparar con `endsWith` fallaba justo cuando habia comentario
194
+ // (`description: "x" # nota` devolvia `"x"` CON las comillas literales,
195
+ // mientras js-yaml devuelve `x`).
196
+ // 2. Hay que deshacer el escape propio de cada estilo: single-quoted
197
+ // duplica el apostrofe (`''`); double-quoted es superset de la string
198
+ // JSON, asi que JSON.parse resuelve \n, \t, \", \\ y \uXXXX.
199
+ const quote = value[0];
200
+ if (quote === "'" || quote === '"') {
201
+ const close = findClosingQuote(value, quote);
202
+ if (close !== -1) {
203
+ const scalar = value.slice(0, close + 1);
204
+ if (quote === "'") {
205
+ return { value: scalar.slice(1, -1).replace(/''/g, "'").trim(), startLine, endLine };
206
+ }
207
+ try {
208
+ return { value: String(JSON.parse(scalar)).trim(), startLine, endLine };
209
+ }
210
+ catch {
211
+ return { value: scalar.slice(1, -1).trim(), startLine, endLine };
212
+ }
213
+ }
214
+ }
215
+ // Escalar plano: un `#` PRECEDIDO DE ESPACIO abre un comentario y no forma
216
+ // parte del valor (un `#` pegado a texto, como en `C#`, si). Dentro de
217
+ // comillas el `#` es literal, y esas ramas ya retornaron.
218
+ return { value: value.replace(/\s+#.*$/, '').trim(), startLine, endLine };
219
+ }
220
+ /** Azucar para el caso comun: solo el valor resuelto de `description`. */
221
+ function readFrontmatterDescription(frontmatter) {
222
+ return findFrontmatterDescription(frontmatter).value;
223
+ }
@@ -110,6 +110,17 @@ function planInitMutationTargets(params) {
110
110
  for (const b of machineBundles) {
111
111
  addBundleTargets(targets, b.name, bundles, agent, 'global', cwd, contentDir);
112
112
  }
113
+ // Providers whose context injection is itself project-scope (managed-agents-md
114
+ // with a null globalPath — Cursor, Copilot) write AGENTS.md and the materialized
115
+ // .awm/context/ file. `stepContextInjection` now refuses to write those without a
116
+ // discovered project root, but enumerating them against `cwd` unconditionally
117
+ // costs nothing (a target that goes unwritten is a no-op backup entry, per this
118
+ // module's opening note) and keeps the guarantee from resting on a single site:
119
+ // under-enumeration here is the one failure mode that silently defeats rollback.
120
+ if (injection?.type === 'managed-agents-md' && injection.globalPath === null) {
121
+ targets.add(path_1.default.join(cwd, path_1.default.basename(injection.localFile)));
122
+ targets.add((0, materializer_1.projectContextPath)(cwd));
123
+ }
113
124
  // project-level: profile, sensors manifest, project injection, and every
114
125
  // extension currently recorded in .awm/profile.json
115
126
  const projectRoot = (0, profile_1.findProjectRoot)(cwd);