agentic-workflow-manager 3.11.0 → 3.13.0

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 (43) hide show
  1. package/dist/src/commands/doctor.js +1 -1
  2. package/dist/src/commands/preflight/checks.js +63 -5
  3. package/dist/src/commands/preflight/index.js +6 -1
  4. package/dist/src/commands/sensors/baseline.js +4 -3
  5. package/dist/src/core/context/materializer.js +7 -0
  6. package/dist/src/core/context/orchestrator.js +26 -6
  7. package/dist/src/core/context/strategies/codex-agents.js +69 -15
  8. package/dist/src/core/diagnostics/context.js +11 -6
  9. package/dist/src/core/diagnostics/provider-checks.js +92 -11
  10. package/dist/src/core/init/mutation-targets.js +18 -2
  11. package/dist/src/core/init/provider-facts.js +5 -4
  12. package/dist/src/core/init/steps.js +16 -2
  13. package/dist/src/core/install-planner.js +56 -6
  14. package/dist/src/core/install-transaction.js +55 -6
  15. package/dist/src/core/provider-artifacts.js +1 -1
  16. package/dist/src/core/renderers/copilot-instructions.js +28 -0
  17. package/dist/src/core/renderers/cursor-mdc.js +49 -0
  18. package/dist/src/core/renderers/skill-source.js +50 -0
  19. package/dist/src/core/skill-integrity.js +1 -1
  20. package/dist/src/index.js +8 -0
  21. package/dist/src/providers/index.js +69 -2
  22. package/dist/tests/commands/add.test.js +96 -0
  23. package/dist/tests/commands/doctor.test.js +25 -0
  24. package/dist/tests/commands/init.test.js +56 -0
  25. package/dist/tests/commands/preflight/preflight.test.js +95 -0
  26. package/dist/tests/core/bundle-install.test.js +63 -0
  27. package/dist/tests/core/context/materializer.test.js +8 -0
  28. package/dist/tests/core/context/orchestrator.test.js +51 -0
  29. package/dist/tests/core/context/strategies/codex-agents.test.js +157 -20
  30. package/dist/tests/core/diagnostics/checks.test.js +1 -0
  31. package/dist/tests/core/diagnostics/provider-tier.test.js +292 -0
  32. package/dist/tests/core/init/mutation-targets.test.js +63 -0
  33. package/dist/tests/core/init/provider-facts.test.js +16 -0
  34. package/dist/tests/core/init/steps.test.js +37 -0
  35. package/dist/tests/core/install-planner.test.js +118 -0
  36. package/dist/tests/core/install-transaction.test.js +109 -0
  37. package/dist/tests/core/provider-artifacts.test.js +11 -0
  38. package/dist/tests/core/renderers/copilot-instructions.test.js +47 -0
  39. package/dist/tests/core/renderers/cursor-mdc.test.js +137 -0
  40. package/dist/tests/core/skill-integrity.test.js +18 -0
  41. package/dist/tests/providers/index.test.js +45 -1
  42. package/dist/tests/providers/injection-config.test.js +16 -0
  43. package/package.json +1 -1
@@ -26,6 +26,7 @@ const artifact_state_1 = require("../artifact-state");
26
26
  const paths_1 = require("../paths");
27
27
  const profile_1 = require("../profile");
28
28
  const registries_1 = require("../registries");
29
+ const materializer_1 = require("../context/materializer");
29
30
  // Thin wrapper over install-planner.ts's `physicalTarget` (the single source
30
31
  // of truth for the dir+filename computation, including the `.toml` rename for
31
32
  // codex-agent-toml) — adapts its throw-on-unsupported contract to this
@@ -92,13 +93,14 @@ function planInitMutationTargets(params) {
92
93
  // whole tree (backupEntryFor does a recursive fs.cpSync for directory
93
94
  // targets — verified in install-transaction.ts), covering any entry that
94
95
  // repair might mutate. Broad-but-safe, per this module's own philosophy.
95
- targets.add(provider.skill.global);
96
+ if (provider.skill.global !== null)
97
+ targets.add(provider.skill.global);
96
98
  // global context / AGENTS.md injection (covered by the hook for claude-code)
97
99
  const injection = provider.injection;
98
100
  if (injection) {
99
101
  if (injection.type === 'config-instructions')
100
102
  targets.add(injection.configPath);
101
- if (injection.type === 'managed-agents-md')
103
+ if (injection.type === 'managed-agents-md' && injection.globalPath !== null)
102
104
  targets.add(injection.globalPath);
103
105
  }
104
106
  // machine-level bundle targets: baseline (dev-core) + ambient, global scope
@@ -119,6 +121,20 @@ function planInitMutationTargets(params) {
119
121
  }
120
122
  if (injection?.type === 'managed-agents-md') {
121
123
  targets.add(path_1.default.join(projectRoot, path_1.default.basename(injection.localFile)));
124
+ if (injection.globalPath === null) {
125
+ // Local-scope context injection (Cursor/Copilot — stepContextInjection,
126
+ // steps.ts) materializes its source content under the project root before
127
+ // writing it into the AGENTS.md target above; that materialized file is a
128
+ // real write this run can make and was previously absent from this
129
+ // enumeration entirely.
130
+ targets.add((0, materializer_1.projectContextPath)(projectRoot));
131
+ }
132
+ }
133
+ if (agent === 'cursor') {
134
+ // CodexAgentsStrategy.injectProject's redundant always-on carrier
135
+ // (codex-agents.ts) — written whenever agent === 'cursor', independent of
136
+ // the managed-agents-md branch above.
137
+ targets.add(path_1.default.join(projectRoot, '.cursor', 'rules', 'awm.mdc'));
122
138
  }
123
139
  let profile;
124
140
  try {
@@ -127,14 +127,15 @@ function providerManagedPaths(agent) {
127
127
  if (provider.injection) {
128
128
  if (provider.injection.type === 'config-instructions')
129
129
  paths.add(provider.injection.configPath);
130
- if (provider.injection.type === 'managed-agents-md')
130
+ if (provider.injection.type === 'managed-agents-md' && provider.injection.globalPath !== null) {
131
131
  paths.add(provider.injection.globalPath);
132
+ }
132
133
  }
133
- if (provider.skill)
134
+ if (provider.skill && provider.skill.global !== null)
134
135
  paths.add(provider.skill.global);
135
- if (provider.workflow)
136
+ if (provider.workflow && provider.workflow.global !== null)
136
137
  paths.add(provider.workflow.global);
137
- if (provider.agent)
138
+ if (provider.agent && provider.agent.global !== null)
138
139
  paths.add(provider.agent.global);
139
140
  return Array.from(paths).sort();
140
141
  }
@@ -76,7 +76,7 @@ exports.defaultActions = {
76
76
  repairGlobalSkills: (skillsDir, registryContentDirs) => (0, skill_integrity_1.repairGlobalSkills)(skillsDir, registryContentDirs),
77
77
  injectProjectConstitution: (o) => {
78
78
  if ((0, providers_1.getInjection)(o.agent)?.type === 'managed-agents-md') {
79
- return new codex_agents_1.CodexAgentsStrategy().injectProject(o.projectRoot) === 'injected' ? 'injected' : 'already';
79
+ return new codex_agents_1.CodexAgentsStrategy().injectProject(o.projectRoot, (0, providers_1.providerFor)(o.agent), o.agent) === 'injected' ? 'injected' : 'already';
80
80
  }
81
81
  return (0, project_constitution_inject_1.injectProjectConstitution)(o.projectRoot, o.agent);
82
82
  },
@@ -208,6 +208,8 @@ function stepGlobalSkillsRepair(d) {
208
208
  if (broken === 0)
209
209
  return ok('machine.globalSkills', 'machine', 'skipped');
210
210
  const skillsDir = (0, providers_1.providerFor)(d.agent).skill.global;
211
+ if (skillsDir === null)
212
+ return ok('machine.globalSkills', 'machine', 'skipped');
211
213
  const r = d.actions.repairGlobalSkills(skillsDir, (0, registries_2.contentRoots)());
212
214
  return ok('machine.globalSkills', 'machine', 'applied', `re-linked ${r.relinked.length}, pruned ${r.pruned.length}`);
213
215
  }
@@ -337,12 +339,24 @@ function stepContextInjection(d) {
337
339
  return ok('machine.contextInjection', 'machine', 'skipped', 'no injection mechanism');
338
340
  if (inj.type === 'cc-settings-merge')
339
341
  return ok('machine.contextInjection', 'machine', 'skipped', 'covered by hook');
342
+ // Providers with no global AGENTS.md-equivalent (managed-agents-md with a null
343
+ // globalPath — today: Copilot, and Cursor's global scope) deliver context at
344
+ // project scope instead.
345
+ const scope = inj.type === 'managed-agents-md' && inj.globalPath === null ? 'local' : 'global';
346
+ // d.ctx.project?.root (computed via findProjectRoot, diagnostics/context.ts) rather
347
+ // than raw d.cwd: mutation-targets.ts's planInitMutationTargets computes the local
348
+ // AGENTS.md backup target via the same findProjectRoot(cwd) call, so using d.cwd here
349
+ // whenever it differs from the walked-up project root (e.g. `awm init` run from a
350
+ // 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.
340
353
  const op = {
341
354
  agent: d.agent,
342
- scope: 'global',
355
+ scope,
343
356
  registryRoot: d.registryRoot,
344
357
  installMethod: d.installMethod,
345
358
  profileExtensions: [],
359
+ projectRoot: d.ctx.project?.root ?? d.cwd,
346
360
  };
347
361
  if (d.actions.contextStatus(op) === 'injected')
348
362
  return ok('machine.contextInjection', 'machine', 'skipped');
@@ -26,7 +26,8 @@ const providers_1 = require("../providers");
26
26
  /** Resolves the single physical filesystem location an intent renders to for one agent. */
27
27
  /**
28
28
  * Resolves the physical target path + renderer for one artifact intent on one
29
- * agent (dir + filename, applying the `.toml` rename for `codex-agent-toml`).
29
+ * agent (dir + filename, applying the renderer-specific extension rename for
30
+ * `codex-agent-toml`/`cursor-mdc`/`copilot-instructions`).
30
31
  * Shared with `core/init/mutation-targets.ts`, which needs the exact same
31
32
  * dir/filename computation to enumerate paths before a real `awm init` run —
32
33
  * duplicating this logic there would let the two silently diverge.
@@ -36,9 +37,27 @@ function physicalTarget(intent, agent, scope, projectRoot) {
36
37
  if (!config)
37
38
  throw new Error(`${intent.type}s are not supported by ${(0, providers_1.providerFor)(agent).label}`);
38
39
  const dir = scope === 'local' ? path_1.default.join(projectRoot, config.local) : config.global;
39
- const filename = config.renderer === 'codex-agent-toml'
40
- ? `${path_1.default.parse(intent.installName).name}.toml`
40
+ if (dir === null) {
41
+ throw (0, providers_1.unsupportedScopeError)(intent.type, scope, (0, providers_1.providerFor)(agent).label, config.globalUnsupportedReason);
42
+ }
43
+ // Rendered targets get a provider-specific extension in place of a
44
+ // trailing `.md` (if any) on intent.installName, so e.g. `using-awm`
45
+ // (skills carry no extension) or `using-awm.md` both become
46
+ // `using-awm.instructions.md`, never `using-awm.md.instructions.md`.
47
+ // Deliberately NOT path.parse(...).name: it strips everything after the
48
+ // LAST dot, not just a real trailing extension — a skill literally named
49
+ // `v1.2-migration` would silently truncate to `v1.mdc`, dropping
50
+ // `2-migration` and risking a collision with any other skill named `v1`.
51
+ // `.md` is the only extension a skill's installName is ever expected to
52
+ // carry (skills are markdown files), so stripping that literal suffix is
53
+ // both sufficient and precise.
54
+ const baseName = intent.installName.endsWith('.md')
55
+ ? intent.installName.slice(0, -'.md'.length)
41
56
  : intent.installName;
57
+ const filename = config.renderer === 'codex-agent-toml' ? `${baseName}.toml`
58
+ : config.renderer === 'cursor-mdc' ? `${baseName}.mdc`
59
+ : config.renderer === 'copilot-instructions' ? `${baseName}.instructions.md`
60
+ : intent.installName;
42
61
  return { targetPath: path_1.default.join(dir, filename), renderer: config.renderer };
43
62
  }
44
63
  /**
@@ -53,7 +72,12 @@ function physicalTarget(intent, agent, scope, projectRoot) {
53
72
  */
54
73
  function skillTargetDir(agent, scope, projectRoot) {
55
74
  const config = (0, providers_1.providerFor)(agent).skill;
56
- return scope === 'local' ? path_1.default.join(projectRoot, config.local) : config.global;
75
+ if (scope === 'local')
76
+ return path_1.default.join(projectRoot, config.local);
77
+ if (config.global === null) {
78
+ throw (0, providers_1.unsupportedScopeError)('skill', scope, (0, providers_1.providerFor)(agent).label, config.globalUnsupportedReason);
79
+ }
80
+ return config.global;
57
81
  }
58
82
  /**
59
83
  * Of `candidates`, the ones that share `agent`'s skill physical target at
@@ -69,7 +93,20 @@ function skillTargetDir(agent, scope, projectRoot) {
69
93
  */
70
94
  function agentsSharingSkillTarget(agent, candidates, scope, projectRoot) {
71
95
  const target = skillTargetDir(agent, scope, projectRoot);
72
- return candidates.filter((candidate) => skillTargetDir(candidate, scope, projectRoot) === target);
96
+ return candidates.filter((candidate) => {
97
+ // A candidate that doesn't support this scope at all (e.g. Copilot at
98
+ // `global` — skillTargetDir throws) trivially can't share `agent`'s
99
+ // target; it just isn't part of the group. Without this guard, a
100
+ // Copilot in `candidates` (enabled for some OTHER, valid install) would
101
+ // crash this whole computation for every unrelated agent, since the
102
+ // exception surfaces from inside `.filter()`'s callback uncaught.
103
+ try {
104
+ return skillTargetDir(candidate, scope, projectRoot) === target;
105
+ }
106
+ catch {
107
+ return false;
108
+ }
109
+ });
73
110
  }
74
111
  /**
75
112
  * Skills are, today, the only artifact type where two agents' provider
@@ -85,7 +122,20 @@ function assertCompleteSharedGroup(intent, selected, enabled, scope, projectRoot
85
122
  return;
86
123
  for (const agent of selected) {
87
124
  const target = physicalTarget(intent, agent, scope, projectRoot).targetPath;
88
- const group = enabled.filter((candidate) => physicalTarget(intent, candidate, scope, projectRoot).targetPath === target);
125
+ // Same reasoning as agentsSharingSkillTarget above: a candidate in
126
+ // `enabled` that doesn't support this scope (e.g. Copilot at `global`)
127
+ // can't be part of the shared-target group — it just isn't a
128
+ // candidate, not a hard failure of this assertion. Without this
129
+ // guard, having Copilot enabled at all would crash every OTHER
130
+ // agent's shared-group check.
131
+ const group = enabled.filter((candidate) => {
132
+ try {
133
+ return physicalTarget(intent, candidate, scope, projectRoot).targetPath === target;
134
+ }
135
+ catch {
136
+ return false;
137
+ }
138
+ });
89
139
  if (group.some((candidate) => !selected.includes(candidate))) {
90
140
  throw new Error(`Shared skill target cannot diverge; select the complete shared target group: ${group.join(',')}`);
91
141
  }
@@ -31,6 +31,8 @@ const artifact_state_1 = require("./artifact-state");
31
31
  const paths_1 = require("./paths");
32
32
  const atomic_file_1 = require("./atomic-file");
33
33
  const codex_agent_1 = require("./renderers/codex-agent");
34
+ const cursor_mdc_1 = require("./renderers/cursor-mdc");
35
+ const copilot_instructions_1 = require("./renderers/copilot-instructions");
34
36
  const executor_1 = require("./executor");
35
37
  /**
36
38
  * The single timestamp-sanitization rule for transaction IDs, shared by
@@ -225,12 +227,25 @@ function stageRenderedFile(content, targetPath) {
225
227
  fs_1.default.writeFileSync(staged, content, 'utf8');
226
228
  return staged;
227
229
  }
230
+ /**
231
+ * `cursor-mdc`/`copilot-instructions` targets are always 'skill'-type
232
+ * operations (providers/index.ts only assigns these renderers to a
233
+ * provider's `skill` ArtifactConfig), whose `sourcePath` is the skill's
234
+ * DIRECTORY (discovery.ts's `discoverSkills`/bundle-install.ts's
235
+ * `expandBundleArtifacts` both set it that way — the whole directory is what
236
+ * a `link` renderer symlinks), not the SKILL.md file itself. Both renderers
237
+ * are sourced from that directory's SKILL.md, so every call site needs this
238
+ * same one-line join instead of reading `op.sourcePath` directly.
239
+ */
240
+ function readSkillMdSource(op) {
241
+ return fs_1.default.readFileSync(path_1.default.join(op.sourcePath, 'SKILL.md'), 'utf8');
242
+ }
228
243
  /**
229
244
  * The real, filesystem-touching TransactionDeps used by applyInstallPlan by
230
- * default. Renders `codex-agent-toml` targets from the canonical agent
231
- * Markdown source at stage time; every other renderer uses the plain
232
- * symlink/copy staging from executor.ts. Never logs target contents or
233
- * environment variables.
245
+ * default. Renders `codex-agent-toml`/`cursor-mdc`/`copilot-instructions`
246
+ * targets from their respective canonical sources at stage time; every other
247
+ * renderer ('link') uses the plain symlink/copy staging from executor.ts.
248
+ * Never logs target contents or environment variables.
234
249
  */
235
250
  function defaultTransactionDeps() {
236
251
  let index = 0;
@@ -241,11 +256,17 @@ function defaultTransactionDeps() {
241
256
  if (!fs_1.default.existsSync(op.sourcePath)) {
242
257
  throw new Error(`Source path does not exist: ${op.sourcePath}`);
243
258
  }
259
+ // Renders without writing anything, purely to surface parse errors
260
+ // before any backup/replace happens.
244
261
  if (op.renderer === 'codex-agent-toml') {
245
- // Renders without writing anything, purely to surface parse errors
246
- // before any backup/replace happens.
247
262
  (0, codex_agent_1.renderCodexAgent)(fs_1.default.readFileSync(op.sourcePath, 'utf8'));
248
263
  }
264
+ else if (op.renderer === 'cursor-mdc') {
265
+ (0, cursor_mdc_1.renderCursorMdc)(readSkillMdSource(op));
266
+ }
267
+ else if (op.renderer === 'copilot-instructions') {
268
+ (0, copilot_instructions_1.renderCopilotInstructions)(readSkillMdSource(op));
269
+ }
249
270
  },
250
271
  backup(op, backupDir) {
251
272
  if (createdAt === null)
@@ -266,6 +287,14 @@ function defaultTransactionDeps() {
266
287
  const rendered = (0, codex_agent_1.renderCodexAgent)(fs_1.default.readFileSync(op.sourcePath, 'utf8'));
267
288
  return stageRenderedFile(rendered, op.targetPath);
268
289
  }
290
+ if (op.renderer === 'cursor-mdc') {
291
+ const rendered = (0, cursor_mdc_1.renderCursorMdc)(readSkillMdSource(op));
292
+ return stageRenderedFile(rendered, op.targetPath);
293
+ }
294
+ if (op.renderer === 'copilot-instructions') {
295
+ const rendered = (0, copilot_instructions_1.renderCopilotInstructions)(readSkillMdSource(op));
296
+ return stageRenderedFile(rendered, op.targetPath);
297
+ }
269
298
  return (0, executor_1.stageArtifact)(op.sourcePath, op.targetPath, op.method);
270
299
  },
271
300
  replace(op, staged) {
@@ -289,6 +318,26 @@ function defaultTransactionDeps() {
289
318
  }
290
319
  return;
291
320
  }
321
+ if (op.renderer === 'cursor-mdc') {
322
+ if (!stat.isFile()) {
323
+ throw new Error(`verification failed: ${op.targetPath} is not a regular file`);
324
+ }
325
+ const content = fs_1.default.readFileSync(op.targetPath, 'utf8');
326
+ if (!content.startsWith('---\n') || !content.includes('alwaysApply:')) {
327
+ throw new Error(`verification failed: ${op.targetPath} does not look like rendered Cursor .mdc`);
328
+ }
329
+ return;
330
+ }
331
+ if (op.renderer === 'copilot-instructions') {
332
+ if (!stat.isFile()) {
333
+ throw new Error(`verification failed: ${op.targetPath} is not a regular file`);
334
+ }
335
+ const content = fs_1.default.readFileSync(op.targetPath, 'utf8');
336
+ if (!content.startsWith('---\n') || !content.includes('applyTo:')) {
337
+ throw new Error(`verification failed: ${op.targetPath} does not look like rendered Copilot instructions`);
338
+ }
339
+ return;
340
+ }
292
341
  if (op.method === 'symlink' && !stat.isSymbolicLink()) {
293
342
  throw new Error(`verification failed: ${op.targetPath} is not a symlink`);
294
343
  }
@@ -30,7 +30,7 @@ function scanLegacyArtifacts(agents, scope) {
30
30
  if (!config)
31
31
  continue;
32
32
  const dir = config[scope];
33
- if (!fs_1.default.existsSync(dir))
33
+ if (dir === null || !fs_1.default.existsSync(dir))
34
34
  continue;
35
35
  for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
36
36
  const fullPath = path_1.default.join(dir, entry.name);
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderCopilotInstructions = renderCopilotInstructions;
4
+ // src/core/renderers/copilot-instructions.ts
5
+ //
6
+ // Renders a SKILL.md source into GitHub Copilot's `.instructions.md` format.
7
+ // Copilot's instructions format is fundamentally file-glob-triggered
8
+ // (`applyTo` matches file paths against the current edit), which doesn't map
9
+ // cleanly onto AWM's trigger-phrase-based skill activation — a real format
10
+ // mismatch this task cannot fully resolve (D4: this whole tier is "context
11
+ // read, not enforced", not runtime-gated the way Claude Code's own skill
12
+ // invocation is). `applyTo: "**"` (match every file) is the practical
13
+ // default: it keeps the skill's guidance always present in Copilot's context
14
+ // rather than guessing a file-type restriction that doesn't correspond to
15
+ // anything in the skill's actual metadata. A future task revisiting this
16
+ // tier should start from this note, not rediscover the mismatch.
17
+ const skill_source_1 = require("./skill-source");
18
+ function renderCopilotInstructions(source) {
19
+ const { body } = (0, skill_source_1.parseSkillSource)(source);
20
+ return [
21
+ '---',
22
+ 'applyTo: "**"',
23
+ '---',
24
+ '',
25
+ body,
26
+ '',
27
+ ].join('\n');
28
+ }
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderCursorMdc = renderCursorMdc;
4
+ // src/core/renderers/cursor-mdc.ts
5
+ //
6
+ // Renders a SKILL.md source into Cursor's `.mdc` rule format. Per this
7
+ // session's D4 correction note (docs/plans/2026-08-07-team-rollout-hardening-design.md),
8
+ // Cursor's current rule frontmatter has three keys: `description`, `globs`,
9
+ // `alwaysApply`. For an individual skill rule (not the Task 4.2 always-on
10
+ // `awm.mdc` context carrier), the correct activation mode is "Agent
11
+ // Requested": `description` set to the skill's own description (so Cursor
12
+ // can decide relevance), `globs` left blank, `alwaysApply: false` — letting
13
+ // Cursor pull the skill in contextually instead of force-loading every
14
+ // installed skill's full content into every request.
15
+ const skill_source_1 = require("./skill-source");
16
+ // YAML plain scalars break on a bare colon-followed-by-space (parsed as a
17
+ // mapping), a `#` preceded by whitespace ANYWHERE in the string — not just
18
+ // at the start — (starts a comment, silently truncating everything after
19
+ // it), a leading YAML-special indicator character, an embedded double
20
+ // quote, or an embedded control/null/DEL byte (invalid in a YAML plain
21
+ // scalar regardless of position, and would otherwise be emitted unquoted
22
+ // straight into the frontmatter) — the same class of problem tomlString/
23
+ // escapeControlChars (codex-agent.ts) guard against for TOML, adapted to
24
+ // YAML's own rules. JSON.stringify produces a YAML-1.1/1.2-compatible
25
+ // double-quoted scalar (YAML's double-quoted flow scalar is a superset of
26
+ // JSON string syntax), so it doubles as the escaping/quoting mechanism once
27
+ // quoting is needed — it \u-escapes \x00-\x1F, but NOT \x7F (DEL is not in
28
+ // JSON's own list of characters requiring escape), so yamlString below
29
+ // escapes that one byte itself after JSON.stringify runs.
30
+ const YAML_UNSAFE = /:(\s|$)|(?:^|\s)#|^[\s\-?:,[\]{}#&*!|>'"%@`]|"|[\x00-\x1f\x7f]/;
31
+ function yamlString(value) {
32
+ if (value !== value.trim() || value === '' || YAML_UNSAFE.test(value)) {
33
+ return JSON.stringify(value).replace(/\x7f/g, '\\u007f');
34
+ }
35
+ return value;
36
+ }
37
+ function renderCursorMdc(source) {
38
+ const { description, body } = (0, skill_source_1.parseSkillSource)(source);
39
+ return [
40
+ '---',
41
+ `description: ${yamlString(description)}`,
42
+ 'globs:',
43
+ 'alwaysApply: false',
44
+ '---',
45
+ '',
46
+ body,
47
+ '',
48
+ ].join('\n');
49
+ }
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseSkillSource = parseSkillSource;
4
+ // src/core/renderers/skill-source.ts
5
+ //
6
+ // Shared parsing for the two provider-specific skill renderers
7
+ // (cursor-mdc.ts, copilot-instructions.ts): both are sourced from a
8
+ // SKILL.md's frontmatter + body, the same relationship renderCodexAgent
9
+ // (codex-agent.ts) has with its canonical agent source — transform, not
10
+ // link. Reuses discovery.ts's `matchFrontmatterBlock` (already the single
11
+ // source of truth for locating the frontmatter block elsewhere in this
12
+ // codebase) rather than writing a second frontmatter parser.
13
+ const discovery_1 = require("../discovery");
14
+ /**
15
+ * Parses a raw SKILL.md source into its `description` frontmatter field and
16
+ * body content. Mirrors discovery.ts's `readArtifactDescription` for the
17
+ * quote-stripping/block-scalar handling of the `description` line, and
18
+ * canonical-agent.ts's `parseCanonicalAgent` for the "throw on missing
19
+ * required piece" discipline — a renderer should never silently embed an
20
+ * empty description or body.
21
+ */
22
+ function parseSkillSource(source) {
23
+ const frontmatter = (0, discovery_1.matchFrontmatterBlock)(source);
24
+ if (frontmatter === null)
25
+ throw new Error('skill source requires YAML frontmatter');
26
+ const line = frontmatter.split(/\r?\n/).find((l) => /^description\s*:/.test(l));
27
+ if (!line)
28
+ throw new Error('skill source requires a non-empty description');
29
+ let description = line.replace(/^description\s*:/, '').trim();
30
+ if ((description.startsWith('"') && description.endsWith('"')) ||
31
+ (description.startsWith("'") && description.endsWith("'"))) {
32
+ description = description.slice(1, -1);
33
+ }
34
+ // A YAML block scalar indicator (`>-`, `|-`, `>`, `|`, `>+`, `|+`) means the
35
+ // real description text lives on the FOLLOWING indented lines, not on this
36
+ // line at all — treating the bare indicator as the description would embed
37
+ // literal "|-" into every rendered skill. Mirrors discovery.ts's
38
+ // readArtifactDescription, which detects the same shape and treats it as
39
+ // absent rather than mis-parsing it.
40
+ const BLOCK_INDICATORS = new Set(['>-', '>', '|-', '|', '>+', '|+']);
41
+ if (BLOCK_INDICATORS.has(description))
42
+ description = '';
43
+ if (!description)
44
+ throw new Error('skill source requires a non-empty description');
45
+ const bodyMatch = source.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n([\s\S]*)$/);
46
+ const body = bodyMatch ? bodyMatch[1].trim() : '';
47
+ if (!body)
48
+ throw new Error('skill source requires a non-empty body');
49
+ return { description, body };
50
+ }
@@ -90,7 +90,7 @@ function reconcileAllSkillLinks(registryContentDirs) {
90
90
  const out = [];
91
91
  for (const agent of providers_1.AGENT_TARGETS) {
92
92
  const skillsDir = (0, providers_1.providerFor)(agent).skill.global;
93
- if (!fs_1.default.existsSync(skillsDir))
93
+ if (skillsDir === null || !fs_1.default.existsSync(skillsDir))
94
94
  continue;
95
95
  out.push({ agent, result: repairGlobalSkills(skillsDir, registryContentDirs) });
96
96
  }
package/dist/src/index.js CHANGED
@@ -183,6 +183,10 @@ program.command('add [name]')
183
183
  continue;
184
184
  }
185
185
  const targetDir = config[scopeVal];
186
+ if (targetDir === null) {
187
+ skipped.push(`${artifact.name} (${currentAgent})`);
188
+ continue;
189
+ }
186
190
  const finalDest = path_1.default.join(targetDir, artifact.name);
187
191
  (0, executor_1.installArtifact)(artifact.sourcePath, finalDest, methodVal);
188
192
  installed.push(`${artifact.name} → ${currentAgent} (${scopeVal})`);
@@ -350,6 +354,10 @@ program.command('add [name]')
350
354
  continue;
351
355
  }
352
356
  const targetDir = config[scopeVal];
357
+ if (targetDir === null) {
358
+ skipped.push(`${artifact.name} (${currentAgent})`);
359
+ continue;
360
+ }
353
361
  const finalDest = path_1.default.join(targetDir, artifact.name);
354
362
  (0, executor_1.installArtifact)(artifact.sourcePath, finalDest, methodVal);
355
363
  installed.push(`${artifact.name} → ${currentAgent} (${scopeVal})`);
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.UnsupportedRendererError = exports.AGENT_TARGETS = void 0;
7
7
  exports.isAgentTarget = isAgentTarget;
8
8
  exports.requireAgentTarget = requireAgentTarget;
9
+ exports.unsupportedScopeError = unsupportedScopeError;
9
10
  exports.providers = providers;
10
11
  exports.providerFor = providerFor;
11
12
  exports.getTargetPath = getTargetPath;
@@ -16,7 +17,7 @@ exports.getInjection = getInjection;
16
17
  // src/providers/index.ts
17
18
  const path_1 = __importDefault(require("path"));
18
19
  const paths_1 = require("../core/paths");
19
- exports.AGENT_TARGETS = ['antigravity', 'opencode', 'claude-code', 'codex'];
20
+ exports.AGENT_TARGETS = ['antigravity', 'opencode', 'claude-code', 'codex', 'cursor', 'copilot'];
20
21
  function isAgentTarget(value) {
21
22
  return typeof value === 'string' &&
22
23
  exports.AGENT_TARGETS.includes(value);
@@ -31,6 +32,14 @@ function requireAgentTarget(value) {
31
32
  class UnsupportedRendererError extends Error {
32
33
  }
33
34
  exports.UnsupportedRendererError = UnsupportedRendererError;
35
+ /** Shared message shape for "this scope isn't supported by this provider" —
36
+ * used everywhere a `null` `ArtifactConfig.global` is resolved (this file's
37
+ * `getTargetPath`, and `install-planner.ts`'s `physicalTarget`/`skillTargetDir`,
38
+ * which duplicate the resolution logic for their own return-shape needs). */
39
+ function unsupportedScopeError(artifactType, scope, providerLabel, reason) {
40
+ return new Error(`${artifactType} ${scope} scope is not supported by ${providerLabel}` +
41
+ (reason ? `: ${reason}` : '.'));
42
+ }
34
43
  function providers() {
35
44
  const home = (0, paths_1.homeDir)();
36
45
  const awm = (0, paths_1.awmHome)();
@@ -118,6 +127,44 @@ function providers() {
118
127
  localFile: 'AGENTS.md',
119
128
  },
120
129
  },
130
+ cursor: {
131
+ label: 'Cursor',
132
+ skill: {
133
+ global: path_1.default.join(home, '.cursor/rules'),
134
+ local: '.cursor/rules',
135
+ renderer: 'cursor-mdc',
136
+ },
137
+ workflow: null,
138
+ agent: null,
139
+ injection: {
140
+ type: 'managed-agents-md',
141
+ // Cursor has no confirmed user-level/global AGENTS.md-equivalent file — its
142
+ // "User Rules" live inside Cursor's own app settings, not a plain file on disk
143
+ // (per docs research done for this task, R4 Task 4.1). Until a primary source
144
+ // confirms a real global path, `null` here is the honest answer, not a guess.
145
+ globalPath: null,
146
+ localFile: 'AGENTS.md',
147
+ },
148
+ },
149
+ copilot: {
150
+ label: 'Copilot',
151
+ skill: {
152
+ global: null,
153
+ globalUnsupportedReason: 'GitHub Copilot has no user-level skill discovery mechanism — skills must be installed per-project.',
154
+ local: '.github/instructions',
155
+ renderer: 'copilot-instructions',
156
+ },
157
+ workflow: null,
158
+ agent: null,
159
+ injection: {
160
+ type: 'managed-agents-md',
161
+ // Copilot is inherently repository-scoped — confirmed no ~/.copilot or
162
+ // equivalent user-level AGENTS.md file exists. Task 4.2 owns the actual
163
+ // runtime handling of a null globalPath (project-only injection).
164
+ globalPath: null,
165
+ localFile: 'AGENTS.md',
166
+ },
167
+ },
121
168
  };
122
169
  }
123
170
  function providerFor(agent) {
@@ -137,7 +184,11 @@ function getTargetPath(type, agent, scope) {
137
184
  const config = provider[type];
138
185
  if (!config)
139
186
  throw new Error(`${type}s are not supported by ${provider.label}.`);
140
- return config[scope];
187
+ const targetPath = scope === 'global' ? config.global : config.local;
188
+ if (targetPath === null) {
189
+ throw unsupportedScopeError(type, scope, provider.label, config.globalUnsupportedReason);
190
+ }
191
+ return targetPath;
141
192
  }
142
193
  function getHookConfig(agent) {
143
194
  return providerFor(agent).hooks;
@@ -152,6 +203,22 @@ function getSettingsMergeHookConfig(agent) {
152
203
  }
153
204
  return config;
154
205
  }
206
+ /**
207
+ * Guards callers that only know how to stage a target via a plain
208
+ * symlink/copy (`core/executor.ts`'s stageArtifact) — today, that means
209
+ * `core/provider-artifacts.ts`'s legacy single-artifact scan/preflight and
210
+ * `src/index.ts`'s legacy interactive `awm add` flow, neither of which goes
211
+ * through install-planner.ts/install-transaction.ts's render-at-stage-time
212
+ * pipeline. Throws for ANY non-'link' renderer, including cursor-mdc/
213
+ * copilot-instructions (Task 4.3): a raw, unrendered copy of a SKILL.md into
214
+ * `.cursor/rules/` or `.github/instructions/` is not a degraded-but-usable
215
+ * install the way it might first appear — it lacks the frontmatter
216
+ * (`alwaysApply`/`applyTo`) and filename extension (`.mdc`/`.instructions.md`)
217
+ * both providers require to even recognize the file, so it would silently
218
+ * install something neither Cursor nor Copilot ever reads. These two
219
+ * renderers are only reachable through commands/add.ts's proper pipeline,
220
+ * which never calls this function.
221
+ */
155
222
  function assertLinkRenderer(type, agent) {
156
223
  if (!['skill', 'workflow', 'agent'].includes(type)) {
157
224
  throw new Error(`Unknown artifact type: ${String(type)}`);