agentic-workflow-manager 3.11.0 → 3.12.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.
- package/dist/src/commands/doctor.js +1 -1
- package/dist/src/core/context/materializer.js +7 -0
- package/dist/src/core/context/orchestrator.js +26 -6
- package/dist/src/core/context/strategies/codex-agents.js +69 -15
- package/dist/src/core/diagnostics/context.js +11 -6
- package/dist/src/core/diagnostics/provider-checks.js +92 -11
- package/dist/src/core/init/mutation-targets.js +18 -2
- package/dist/src/core/init/provider-facts.js +5 -4
- package/dist/src/core/init/steps.js +16 -2
- package/dist/src/core/install-planner.js +56 -6
- package/dist/src/core/install-transaction.js +55 -6
- package/dist/src/core/provider-artifacts.js +1 -1
- package/dist/src/core/renderers/copilot-instructions.js +28 -0
- package/dist/src/core/renderers/cursor-mdc.js +49 -0
- package/dist/src/core/renderers/skill-source.js +50 -0
- package/dist/src/core/skill-integrity.js +1 -1
- package/dist/src/index.js +8 -0
- package/dist/src/providers/index.js +69 -2
- package/dist/tests/commands/add.test.js +96 -0
- package/dist/tests/commands/doctor.test.js +25 -0
- package/dist/tests/commands/init.test.js +56 -0
- package/dist/tests/core/bundle-install.test.js +63 -0
- package/dist/tests/core/context/materializer.test.js +8 -0
- package/dist/tests/core/context/orchestrator.test.js +51 -0
- package/dist/tests/core/context/strategies/codex-agents.test.js +157 -20
- package/dist/tests/core/diagnostics/checks.test.js +1 -0
- package/dist/tests/core/diagnostics/provider-tier.test.js +292 -0
- package/dist/tests/core/init/mutation-targets.test.js +63 -0
- package/dist/tests/core/init/provider-facts.test.js +16 -0
- package/dist/tests/core/init/steps.test.js +37 -0
- package/dist/tests/core/install-planner.test.js +118 -0
- package/dist/tests/core/install-transaction.test.js +109 -0
- package/dist/tests/core/provider-artifacts.test.js +11 -0
- package/dist/tests/core/renderers/copilot-instructions.test.js +47 -0
- package/dist/tests/core/renderers/cursor-mdc.test.js +137 -0
- package/dist/tests/core/skill-integrity.test.js +18 -0
- package/dist/tests/providers/index.test.js +45 -1
- package/dist/tests/providers/injection-config.test.js +16 -0
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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`
|
|
231
|
-
*
|
|
232
|
-
* symlink/copy staging from executor.ts.
|
|
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
|
-
|
|
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)}`);
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
// tests/commands/add.test.ts
|
|
7
|
+
//
|
|
8
|
+
// Real, unstubbed `awm add <bundle>` end-to-end for the Task 4.3 renderers:
|
|
9
|
+
// runAddBundleCore → (real) addBundle → installBundle → planInstall
|
|
10
|
+
// (install-planner.ts) → applyInstallPlan (install-transaction.ts) →
|
|
11
|
+
// renderCursorMdc/renderCopilotInstructions — proving the full pipeline
|
|
12
|
+
// wires together at the actual CLI command entrypoint, not just at the
|
|
13
|
+
// bundle-install.ts layer (see tests/core/bundle-install.test.ts for the
|
|
14
|
+
// lower-level equivalent). Per CLAUDE.md: isolated HOME/AWM_HOME tmpdirs,
|
|
15
|
+
// never the real ~/.awm.
|
|
16
|
+
const fs_1 = __importDefault(require("fs"));
|
|
17
|
+
const os_1 = __importDefault(require("os"));
|
|
18
|
+
const path_1 = __importDefault(require("path"));
|
|
19
|
+
const add_1 = require("../../src/commands/add");
|
|
20
|
+
const bundles_1 = require("../../src/core/bundles");
|
|
21
|
+
let tmpHome;
|
|
22
|
+
let originalHome;
|
|
23
|
+
let originalAwmHome;
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-add-e2e-home-'));
|
|
26
|
+
originalHome = process.env.HOME;
|
|
27
|
+
originalAwmHome = process.env.AWM_HOME;
|
|
28
|
+
process.env.HOME = tmpHome;
|
|
29
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
30
|
+
});
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
33
|
+
if (originalHome === undefined)
|
|
34
|
+
delete process.env.HOME;
|
|
35
|
+
else
|
|
36
|
+
process.env.HOME = originalHome;
|
|
37
|
+
if (originalAwmHome === undefined)
|
|
38
|
+
delete process.env.AWM_HOME;
|
|
39
|
+
else
|
|
40
|
+
process.env.AWM_HOME = originalAwmHome;
|
|
41
|
+
});
|
|
42
|
+
/** A single-bundle content registry: one skill with real description+body. */
|
|
43
|
+
function makeContentFixture() {
|
|
44
|
+
const tmp = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-add-e2e-registry-'));
|
|
45
|
+
fs_1.default.mkdirSync(path_1.default.join(tmp, 'bundles', 'demo'), { recursive: true });
|
|
46
|
+
fs_1.default.mkdirSync(path_1.default.join(tmp, 'skills', 'demo-skill'), { recursive: true });
|
|
47
|
+
fs_1.default.writeFileSync(path_1.default.join(tmp, 'skills', 'demo-skill', 'SKILL.md'), '---\nname: demo-skill\ndescription: A demo skill for add.test.ts e2e\n---\n\nFollow the demo skill body.\n');
|
|
48
|
+
fs_1.default.writeFileSync(path_1.default.join(tmp, 'catalog.json'), JSON.stringify({
|
|
49
|
+
version: 1,
|
|
50
|
+
bundles: [{ name: 'demo', source: './bundles/demo', version: '1.0.0', scope: 'project' }],
|
|
51
|
+
}));
|
|
52
|
+
fs_1.default.writeFileSync(path_1.default.join(tmp, 'bundles', 'demo', 'bundle.json'), JSON.stringify({
|
|
53
|
+
name: 'demo', version: '1.0.0', description: 'Demo', scope: 'project', dependsOn: [],
|
|
54
|
+
skills: ['demo-skill'], workflows: [], agents: [],
|
|
55
|
+
}));
|
|
56
|
+
return tmp;
|
|
57
|
+
}
|
|
58
|
+
function prefsFor(agent) {
|
|
59
|
+
return { defaultAgent: agent, enabledAgents: [agent], installMethod: 'symlink', defaultScope: 'local' };
|
|
60
|
+
}
|
|
61
|
+
/** findProjectRoot (core/profile.ts) needs a recognizable project marker
|
|
62
|
+
* (.git/, package.json, or .awm/profile.json) — a bare tmpdir isn't one. */
|
|
63
|
+
function makeProjectRoot() {
|
|
64
|
+
const projectRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-add-e2e-project-'));
|
|
65
|
+
fs_1.default.writeFileSync(path_1.default.join(projectRoot, 'package.json'), JSON.stringify({ name: 'fixture' }));
|
|
66
|
+
return projectRoot;
|
|
67
|
+
}
|
|
68
|
+
it('awm add demo materializes a real Cursor .mdc file end-to-end', () => {
|
|
69
|
+
const content = makeContentFixture();
|
|
70
|
+
const projectRoot = makeProjectRoot();
|
|
71
|
+
const bundles = (0, bundles_1.discoverBundles)(content);
|
|
72
|
+
const outcome = (0, add_1.runAddBundleCore)({ name: 'demo', agent: 'cursor', cwd: projectRoot }, prefsFor('cursor'), bundles);
|
|
73
|
+
expect(outcome.code).toBe(0);
|
|
74
|
+
const mdcPath = path_1.default.join(projectRoot, '.cursor/rules/demo-skill.mdc');
|
|
75
|
+
expect(fs_1.default.existsSync(mdcPath)).toBe(true);
|
|
76
|
+
const rendered = fs_1.default.readFileSync(mdcPath, 'utf8');
|
|
77
|
+
expect(rendered).toContain('description: A demo skill for add.test.ts e2e');
|
|
78
|
+
expect(rendered).toContain('alwaysApply: false');
|
|
79
|
+
expect(rendered).toContain('Follow the demo skill body.');
|
|
80
|
+
fs_1.default.rmSync(content, { recursive: true, force: true });
|
|
81
|
+
fs_1.default.rmSync(projectRoot, { recursive: true, force: true });
|
|
82
|
+
});
|
|
83
|
+
it('awm add demo materializes a real Copilot .instructions.md file end-to-end', () => {
|
|
84
|
+
const content = makeContentFixture();
|
|
85
|
+
const projectRoot = makeProjectRoot();
|
|
86
|
+
const bundles = (0, bundles_1.discoverBundles)(content);
|
|
87
|
+
const outcome = (0, add_1.runAddBundleCore)({ name: 'demo', agent: 'copilot', cwd: projectRoot }, prefsFor('copilot'), bundles);
|
|
88
|
+
expect(outcome.code).toBe(0);
|
|
89
|
+
const instructionsPath = path_1.default.join(projectRoot, '.github/instructions/demo-skill.instructions.md');
|
|
90
|
+
expect(fs_1.default.existsSync(instructionsPath)).toBe(true);
|
|
91
|
+
const rendered = fs_1.default.readFileSync(instructionsPath, 'utf8');
|
|
92
|
+
expect(rendered).toContain('applyTo: "**"');
|
|
93
|
+
expect(rendered).toContain('Follow the demo skill body.');
|
|
94
|
+
fs_1.default.rmSync(content, { recursive: true, force: true });
|
|
95
|
+
fs_1.default.rmSync(projectRoot, { recursive: true, force: true });
|
|
96
|
+
});
|
|
@@ -58,6 +58,22 @@ describe('renderReport', () => {
|
|
|
58
58
|
expect(out).toContain('→ skill: project-constitution');
|
|
59
59
|
});
|
|
60
60
|
});
|
|
61
|
+
describe('renderProviderReport — capability tier (Task 4.4)', () => {
|
|
62
|
+
it('shows the tier next to each provider label', () => {
|
|
63
|
+
const report = {
|
|
64
|
+
overall: 'healthy',
|
|
65
|
+
providers: [
|
|
66
|
+
{ id: 'claude-code', label: 'Claude Code', tier: 'hooks-native', checks: [] },
|
|
67
|
+
{ id: 'cursor', label: 'Cursor', tier: 'agents-md-managed', checks: [] },
|
|
68
|
+
{ id: 'antigravity', label: 'Antigravity', tier: 'context-only', checks: [] },
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
const out = (0, doctor_1.renderProviderReport)(report);
|
|
72
|
+
expect(out).toContain('Provider: Claude Code (hooks-native)');
|
|
73
|
+
expect(out).toContain('Provider: Cursor (agents-md-managed)');
|
|
74
|
+
expect(out).toContain('Provider: Antigravity (context-only)');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
61
77
|
describe('runDoctor', () => {
|
|
62
78
|
let tmpHome;
|
|
63
79
|
let tmpWork;
|
|
@@ -120,6 +136,7 @@ describe('runDoctor', () => {
|
|
|
120
136
|
.toEqual(['claude-code', 'opencode', 'codex']); // verifies R12
|
|
121
137
|
expect(report.providers.find((provider) => provider.id === 'codex'))
|
|
122
138
|
.toMatchObject({
|
|
139
|
+
tier: 'hooks-native', // Task 4.4
|
|
123
140
|
checks: expect.arrayContaining([
|
|
124
141
|
expect.objectContaining({ id: 'binary.version' }),
|
|
125
142
|
expect.objectContaining({ id: 'skills.global' }),
|
|
@@ -129,6 +146,14 @@ describe('runDoctor', () => {
|
|
|
129
146
|
}); // verifies R2, R7, R8, R18
|
|
130
147
|
expect(code).toBe(1);
|
|
131
148
|
});
|
|
149
|
+
it('includes tier for every provider in JSON output (Task 4.4)', () => {
|
|
150
|
+
writePrefs(prefsWith(['claude-code', 'opencode', 'codex']));
|
|
151
|
+
const code = (0, doctor_1.runDoctor)({ cwd: tmpWork, json: true });
|
|
152
|
+
const report = JSON.parse(stdout());
|
|
153
|
+
expect(report.providers.map((provider) => provider.tier))
|
|
154
|
+
.toEqual(['hooks-native', 'config-managed', 'hooks-native']);
|
|
155
|
+
expect(code).toBe(1);
|
|
156
|
+
});
|
|
132
157
|
it('reports a clear, specific error for an invalid --agent value, not a generic "internal error"', () => {
|
|
133
158
|
const errSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
134
159
|
try {
|