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
|
@@ -101,7 +101,7 @@ function renderProviderReport(report) {
|
|
|
101
101
|
lines.push(picocolors_1.default.bold('AWM · harness status'));
|
|
102
102
|
lines.push('');
|
|
103
103
|
for (const provider of report.providers) {
|
|
104
|
-
lines.push(`Provider: ${provider.label}`);
|
|
104
|
+
lines.push(`Provider: ${provider.label} ${picocolors_1.default.dim(`(${provider.tier})`)}`);
|
|
105
105
|
for (const check of provider.checks)
|
|
106
106
|
lines.push(providerCheckLine(check));
|
|
107
107
|
lines.push('');
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.globalContextPath = globalContextPath;
|
|
7
|
+
exports.projectContextPath = projectContextPath;
|
|
7
8
|
exports.materialize = materialize;
|
|
8
9
|
// cli/src/core/context/materializer.ts
|
|
9
10
|
const fs_1 = __importDefault(require("fs"));
|
|
@@ -13,6 +14,12 @@ const paths_1 = require("../paths");
|
|
|
13
14
|
function globalContextPath() {
|
|
14
15
|
return path_1.default.join((0, paths_1.awmHome)(), 'context', 'awm-context.md');
|
|
15
16
|
}
|
|
17
|
+
/** Project-scoped materialized-source path — mirrors globalContextPath()'s shape,
|
|
18
|
+
* rooted in the project instead of ~/.awm. This is the MATERIALIZED SOURCE
|
|
19
|
+
* content path, distinct from the injection TARGET file (<projectRoot>/AGENTS.md). */
|
|
20
|
+
function projectContextPath(projectRoot) {
|
|
21
|
+
return path_1.default.join(projectRoot, '.awm', 'context', 'awm-context.md');
|
|
22
|
+
}
|
|
16
23
|
function materialize(ctx, absPath, scope) {
|
|
17
24
|
let onDisk = null;
|
|
18
25
|
try {
|
|
@@ -28,25 +28,42 @@ class InjectionOrchestrator {
|
|
|
28
28
|
case 'managed-agents-md': return new codex_agents_1.CodexAgentsStrategy();
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
/** Materialized-source content path for `op.scope` — global under ~/.awm, local under the project. */
|
|
32
|
+
contextPathFor(op) {
|
|
33
|
+
if (this.overrides.contextPathOverride)
|
|
34
|
+
return this.overrides.contextPathOverride;
|
|
35
|
+
if (op.scope === 'local') {
|
|
36
|
+
if (!op.projectRoot)
|
|
37
|
+
throw new Error('projectRoot is required for local-scope context operations');
|
|
38
|
+
return (0, materializer_1.projectContextPath)(op.projectRoot);
|
|
39
|
+
}
|
|
40
|
+
return (0, materializer_1.globalContextPath)();
|
|
41
|
+
}
|
|
31
42
|
/** Full input: builds context from registry and materializes to disk. Used by installContext only. */
|
|
32
43
|
inputFor(op) {
|
|
33
44
|
const ctx = (0, provider_1.buildContext)({ registryRoot: op.registryRoot, profileExtensions: op.profileExtensions });
|
|
34
|
-
const absPath = this.
|
|
45
|
+
const absPath = this.contextPathFor(op);
|
|
35
46
|
const ref = (0, materializer_1.materialize)(ctx, absPath, op.scope);
|
|
36
|
-
return {
|
|
47
|
+
return {
|
|
48
|
+
ref, registryRoot: op.registryRoot, installMethod: op.installMethod,
|
|
49
|
+
agent: op.agent, scope: op.scope, projectRoot: op.projectRoot,
|
|
50
|
+
};
|
|
37
51
|
}
|
|
38
52
|
/** Path-only input: no buildContext, no materialize. Safe for remove() which never reads contentHash. */
|
|
39
53
|
pathInputFor(op) {
|
|
40
|
-
const absPath = this.
|
|
54
|
+
const absPath = this.contextPathFor(op);
|
|
41
55
|
const ref = { absPath, scope: op.scope, contentHash: '' };
|
|
42
|
-
return {
|
|
56
|
+
return {
|
|
57
|
+
ref, registryRoot: op.registryRoot, installMethod: op.installMethod,
|
|
58
|
+
agent: op.agent, scope: op.scope, projectRoot: op.projectRoot,
|
|
59
|
+
};
|
|
43
60
|
}
|
|
44
61
|
/**
|
|
45
62
|
* Status input: builds context from registry (to get expected hash) but does NOT materialize.
|
|
46
63
|
* Avoids silently correcting a stale file before the strategy can observe it.
|
|
47
64
|
*/
|
|
48
65
|
statusInputFor(op) {
|
|
49
|
-
const absPath = this.
|
|
66
|
+
const absPath = this.contextPathFor(op);
|
|
50
67
|
let contentHash = '';
|
|
51
68
|
try {
|
|
52
69
|
const ctx = (0, provider_1.buildContext)({ registryRoot: op.registryRoot, profileExtensions: op.profileExtensions });
|
|
@@ -59,7 +76,10 @@ class InjectionOrchestrator {
|
|
|
59
76
|
throw err;
|
|
60
77
|
}
|
|
61
78
|
const ref = { absPath, scope: op.scope, contentHash };
|
|
62
|
-
return {
|
|
79
|
+
return {
|
|
80
|
+
ref, registryRoot: op.registryRoot, installMethod: op.installMethod,
|
|
81
|
+
agent: op.agent, scope: op.scope, projectRoot: op.projectRoot,
|
|
82
|
+
};
|
|
63
83
|
}
|
|
64
84
|
installContext(op) {
|
|
65
85
|
const provider = this.provider(op.agent);
|
|
@@ -6,7 +6,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.CodexAgentsStrategy = void 0;
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
|
-
const paths_1 = require("../../paths");
|
|
10
9
|
const atomic_file_1 = require("../../atomic-file");
|
|
11
10
|
const managed_block_1 = require("../managed-block");
|
|
12
11
|
const provider_1 = require("../provider");
|
|
@@ -26,6 +25,17 @@ function injectFile(file, markdown) {
|
|
|
26
25
|
(0, atomic_file_1.writeFileAtomic)(file, merged);
|
|
27
26
|
return 'injected';
|
|
28
27
|
}
|
|
28
|
+
/** For local-scope providers (Cursor/Copilot), context injection (`inject`) and project
|
|
29
|
+
* constitution injection (`injectProject`) target the SAME file — `targetFile`'s local
|
|
30
|
+
* branch resolves both to `<projectRoot>/<localFile>` (AGENTS.md). Global-scope providers
|
|
31
|
+
* (Codex) don't have this collision: context goes to a separate global file, constitution
|
|
32
|
+
* stays project-local. `mergeManagedBlock` supports only one managed slot per file, so for
|
|
33
|
+
* local scope the two payloads must be combined into a single write — `inject`/`status`
|
|
34
|
+
* append `PROJECT_GUIDANCE` here, and `injectProject` skips its own AGENTS.md write for
|
|
35
|
+
* these providers (see its `contextCoversThisFile` check) so there is exactly one writer. */
|
|
36
|
+
function withProjectGuidance(markdown, scope) {
|
|
37
|
+
return scope === 'local' ? `${markdown}\n\n${PROJECT_GUIDANCE}` : markdown;
|
|
38
|
+
}
|
|
29
39
|
class CodexAgentsStrategy {
|
|
30
40
|
globalPath(provider) {
|
|
31
41
|
const injection = provider.injection;
|
|
@@ -38,16 +48,16 @@ class CodexAgentsStrategy {
|
|
|
38
48
|
return injection.globalPath;
|
|
39
49
|
}
|
|
40
50
|
inject(input, provider) {
|
|
41
|
-
this.
|
|
51
|
+
const scope = this.assertSupportedScope(input, provider);
|
|
42
52
|
if (!fs_1.default.existsSync(input.ref.absPath)) {
|
|
43
53
|
throw new Error(`materialized context not found at ${input.ref.absPath}`);
|
|
44
54
|
}
|
|
45
55
|
const markdown = fs_1.default.readFileSync(input.ref.absPath, 'utf8');
|
|
46
|
-
return injectFile(this.
|
|
56
|
+
return injectFile(this.targetFile(provider, scope, input.projectRoot), withProjectGuidance(markdown, scope));
|
|
47
57
|
}
|
|
48
58
|
remove(input, provider) {
|
|
49
|
-
this.
|
|
50
|
-
const file = this.
|
|
59
|
+
const scope = this.assertSupportedScope(input, provider);
|
|
60
|
+
const file = this.targetFile(provider, scope, input.projectRoot);
|
|
51
61
|
if (!fs_1.default.existsSync(file))
|
|
52
62
|
return;
|
|
53
63
|
const original = read(file);
|
|
@@ -56,8 +66,8 @@ class CodexAgentsStrategy {
|
|
|
56
66
|
(0, atomic_file_1.writeFileAtomic)(file, removed);
|
|
57
67
|
}
|
|
58
68
|
status(input, provider) {
|
|
59
|
-
this.
|
|
60
|
-
const file = this.
|
|
69
|
+
const scope = this.assertSupportedScope(input, provider);
|
|
70
|
+
const file = this.targetFile(provider, scope, input.projectRoot);
|
|
61
71
|
if (!fs_1.default.existsSync(file))
|
|
62
72
|
return 'absent';
|
|
63
73
|
const body = (0, managed_block_1.managedBlockBody)(read(file));
|
|
@@ -68,33 +78,77 @@ class CodexAgentsStrategy {
|
|
|
68
78
|
const expected = fs_1.default.readFileSync(input.ref.absPath, 'utf8');
|
|
69
79
|
if ((0, provider_1.sha256)(expected) !== input.ref.contentHash)
|
|
70
80
|
return 'stale';
|
|
71
|
-
return body === (0, managed_block_1.normalizeManagedBody)(expected) ? 'injected' : 'stale';
|
|
81
|
+
return body === (0, managed_block_1.normalizeManagedBody)(withProjectGuidance(expected, scope)) ? 'injected' : 'stale';
|
|
72
82
|
}
|
|
73
|
-
injectGlobal(context) {
|
|
83
|
+
injectGlobal(context, provider) {
|
|
74
84
|
if (typeof context !== 'object' || context === null) {
|
|
75
85
|
throw new Error('context must be an object');
|
|
76
86
|
}
|
|
77
87
|
if (typeof context.markdown !== 'string' || context.markdown.length === 0) {
|
|
78
88
|
throw new Error('markdown must be a non-empty string');
|
|
79
89
|
}
|
|
80
|
-
return injectFile(
|
|
90
|
+
return injectFile(this.globalPath(provider), context.markdown);
|
|
81
91
|
}
|
|
82
|
-
injectProject(projectRoot) {
|
|
92
|
+
injectProject(projectRoot, provider, agent) {
|
|
83
93
|
if (typeof projectRoot !== 'string' || projectRoot.length === 0) {
|
|
84
94
|
throw new Error('projectRoot must be a non-empty string');
|
|
85
95
|
}
|
|
86
|
-
|
|
96
|
+
// A provider whose context injection is ITSELF local-scope (globalPath === null —
|
|
97
|
+
// today: Cursor, Copilot) already owns this exact file's managed block via `inject()`
|
|
98
|
+
// (see withProjectGuidance) — writing PROJECT_GUIDANCE here too would be a second
|
|
99
|
+
// writer of the same single-slot block, silently overwriting whichever one runs last.
|
|
100
|
+
// Global-scope providers (Codex) have no such collision: their context targets a
|
|
101
|
+
// separate global file, so this is the only writer of the project AGENTS.md.
|
|
102
|
+
const injection = provider.injection;
|
|
103
|
+
const contextCoversThisFile = injection?.type === 'managed-agents-md' && injection.globalPath === null;
|
|
104
|
+
const result = contextCoversThisFile
|
|
105
|
+
? 'unchanged'
|
|
106
|
+
: injectFile(path_1.default.join(projectRoot, 'AGENTS.md'), PROJECT_GUIDANCE);
|
|
107
|
+
let carrierResult = 'unchanged';
|
|
108
|
+
if (agent === 'cursor') {
|
|
109
|
+
// Cursor's Background/Cloud Agent does not reliably read AGENTS.md (open,
|
|
110
|
+
// staff-acknowledged bug on Cursor's own community forum, unresolved as of
|
|
111
|
+
// this research — see docs/plans/2026-08-07-team-rollout-hardening-design.md,
|
|
112
|
+
// D4 correction note). Interactive Agent mode DOES read AGENTS.md, unaffected.
|
|
113
|
+
// Write the same guidance as a redundant .mdc carrier with alwaysApply: true
|
|
114
|
+
// so the managed context survives regardless of which Cursor mode is active.
|
|
115
|
+
carrierResult = injectFile(path_1.default.join(projectRoot, '.cursor', 'rules', 'awm.mdc'), `---\ndescription: AWM project guidance (redundant carrier — see AGENTS.md)\nglobs:\nalwaysApply: true\n---\n\n${PROJECT_GUIDANCE}`);
|
|
116
|
+
}
|
|
117
|
+
return result === 'injected' || carrierResult === 'injected' ? 'injected' : 'unchanged';
|
|
118
|
+
}
|
|
119
|
+
/** The scope this provider's managed-agents-md injection operates at: 'local'
|
|
120
|
+
* when it has no global AGENTS.md-equivalent (globalPath === null), else 'global'. */
|
|
121
|
+
requiredScope(provider) {
|
|
122
|
+
const injection = provider.injection;
|
|
123
|
+
if (!injection || injection.type !== 'managed-agents-md') {
|
|
124
|
+
throw new Error('CodexAgentsStrategy requires a managed-agents-md provider');
|
|
125
|
+
}
|
|
126
|
+
return injection.globalPath === null ? 'local' : 'global';
|
|
127
|
+
}
|
|
128
|
+
/** The actual file this strategy injects/removes/checks, given a scope. */
|
|
129
|
+
targetFile(provider, scope, projectRoot) {
|
|
130
|
+
if (scope === 'global')
|
|
131
|
+
return this.globalPath(provider);
|
|
132
|
+
if (!projectRoot)
|
|
133
|
+
throw new Error('projectRoot is required for local-scope injection');
|
|
134
|
+
const injection = provider.injection;
|
|
135
|
+
if (!injection || injection.type !== 'managed-agents-md') {
|
|
136
|
+
throw new Error('CodexAgentsStrategy requires a managed-agents-md provider');
|
|
137
|
+
}
|
|
138
|
+
return path_1.default.join(projectRoot, injection.localFile);
|
|
87
139
|
}
|
|
88
|
-
|
|
140
|
+
assertSupportedScope(input, provider) {
|
|
89
141
|
if (typeof input !== 'object' || input === null) {
|
|
90
142
|
throw new Error('input must be an object');
|
|
91
143
|
}
|
|
92
|
-
|
|
93
|
-
|
|
144
|
+
const required = this.requiredScope(provider);
|
|
145
|
+
if (input.scope !== required || input.ref?.scope !== required) {
|
|
146
|
+
throw new Error(`CodexAgentsStrategy for ${provider.label} supports only ${required} injection`);
|
|
94
147
|
}
|
|
95
148
|
if (typeof input.ref.absPath !== 'string' || input.ref.absPath.length === 0) {
|
|
96
149
|
throw new Error('input.ref.absPath must be a non-empty string');
|
|
97
150
|
}
|
|
151
|
+
return required;
|
|
98
152
|
}
|
|
99
153
|
}
|
|
100
154
|
exports.CodexAgentsStrategy = CodexAgentsStrategy;
|
|
@@ -111,14 +111,17 @@ function gatherMachine(bundles, agent = 'claude-code') {
|
|
|
111
111
|
hookDegraded = hs.overall === 'DEGRADED';
|
|
112
112
|
}
|
|
113
113
|
catch { /* sin soporte de hooks → ausente */ }
|
|
114
|
-
// devCore (bundle baseline)
|
|
114
|
+
// devCore (bundle baseline) — skillsDir is null only for providers with no global skill
|
|
115
|
+
// discovery mechanism (today: Copilot); treated the same as "nothing linked here yet".
|
|
115
116
|
const skillsDir = (0, providers_1.providerFor)(agent).skill.global;
|
|
116
117
|
const baseline = bundles.find((b) => b.scope === 'baseline');
|
|
117
118
|
let devCorePresent = false;
|
|
118
119
|
let brokenLinks = [];
|
|
119
120
|
if (baseline) {
|
|
120
121
|
const skillNames = (0, bundles_1.resolveBundleSkills)(baseline.name, bundles);
|
|
121
|
-
const { linked, broken } =
|
|
122
|
+
const { linked, broken } = skillsDir !== null
|
|
123
|
+
? classifyLinks(skillNames, skillsDir)
|
|
124
|
+
: { linked: [], broken: [] };
|
|
122
125
|
const absent = skillNames.filter((s) => !linked.includes(s) && !broken.includes(s));
|
|
123
126
|
devCorePresent = skillNames.length > 0 && (linked.length + broken.length) > 0;
|
|
124
127
|
brokenLinks = [...broken, ...absent];
|
|
@@ -133,7 +136,7 @@ function gatherMachine(bundles, agent = 'claude-code') {
|
|
|
133
136
|
// the real Codex+OpenCode coexistence E2E test
|
|
134
137
|
// (tests/integration/codex-provider-isolated.test.ts).
|
|
135
138
|
const agentConfig = (0, providers_1.providerFor)(agent).agent;
|
|
136
|
-
if (agentConfig) {
|
|
139
|
+
if (agentConfig && agentConfig.global !== null) {
|
|
137
140
|
const agentNames = (0, bundles_1.resolveBundleAgents)(baseline.name, bundles);
|
|
138
141
|
if (agentNames.length > 0) {
|
|
139
142
|
const filenames = agentNames.map((n) => agentConfig.renderer === 'codex-agent-toml' ? `${n}.toml` : n);
|
|
@@ -154,7 +157,7 @@ function gatherMachine(bundles, agent = 'claude-code') {
|
|
|
154
157
|
catch { /* sin config → ningún ambient deseado */ }
|
|
155
158
|
const installed = wanted.filter((name) => {
|
|
156
159
|
const skillNames = (0, bundles_1.resolveBundleSkills)(name, bundles);
|
|
157
|
-
if (skillNames.length === 0)
|
|
160
|
+
if (skillNames.length === 0 || skillsDir === null)
|
|
158
161
|
return false;
|
|
159
162
|
const { linked } = classifyLinks(skillNames, skillsDir);
|
|
160
163
|
return linked.length === skillNames.length;
|
|
@@ -165,7 +168,9 @@ function gatherMachine(bundles, agent = 'claude-code') {
|
|
|
165
168
|
devCore: { present: devCorePresent, brokenLinks },
|
|
166
169
|
ambient: { wanted, installed },
|
|
167
170
|
contextInjection: gatherContextInjection(),
|
|
168
|
-
globalSkills:
|
|
171
|
+
globalSkills: skillsDir !== null
|
|
172
|
+
? (0, skill_integrity_1.classifyGlobalSkills)(skillsDir, (0, registries_1.contentRoots)())
|
|
173
|
+
: { valid: [], repairable: [], dead: [] },
|
|
169
174
|
};
|
|
170
175
|
}
|
|
171
176
|
function gatherProject(root, bundles, agent = 'claude-code') {
|
|
@@ -203,6 +208,6 @@ function gatherContext(opts = {}) {
|
|
|
203
208
|
return {
|
|
204
209
|
machine: gatherMachine(bundles, agent),
|
|
205
210
|
project: root ? gatherProject(root, bundles, agent) : null,
|
|
206
|
-
providers: (0, provider_checks_1.gatherProviderChecks)(agents, scanSkills),
|
|
211
|
+
providers: (0, provider_checks_1.gatherProviderChecks)(agents, scanSkills, root ?? undefined),
|
|
207
212
|
};
|
|
208
213
|
}
|
|
@@ -3,6 +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.providerTier = providerTier;
|
|
6
7
|
exports.gatherProviderChecks = gatherProviderChecks;
|
|
7
8
|
// src/core/diagnostics/provider-checks.ts
|
|
8
9
|
//
|
|
@@ -20,6 +21,26 @@ const provider_version_1 = require("../provider-version");
|
|
|
20
21
|
const status_1 = require("../../commands/hooks/status");
|
|
21
22
|
const orchestrator_1 = require("../context/orchestrator");
|
|
22
23
|
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
|
+
};
|
|
33
|
+
/** Structural classification, computed purely from `provider`'s config shape — see
|
|
34
|
+
* `ProviderTier`'s doc comment in `types.ts` for what each tier means. */
|
|
35
|
+
function providerTier(provider) {
|
|
36
|
+
if (provider.hooks)
|
|
37
|
+
return 'hooks-native';
|
|
38
|
+
if (provider.injection?.type === 'config-instructions')
|
|
39
|
+
return 'config-managed';
|
|
40
|
+
if (provider.injection)
|
|
41
|
+
return 'agents-md-managed';
|
|
42
|
+
return 'context-only';
|
|
43
|
+
}
|
|
23
44
|
function binaryVersionCheck(agent) {
|
|
24
45
|
const provider = (0, providers_1.providerFor)(agent);
|
|
25
46
|
if (!provider.versionCommand || !provider.minimumVersion) {
|
|
@@ -41,7 +62,52 @@ function binaryVersionCheck(agent) {
|
|
|
41
62
|
};
|
|
42
63
|
}
|
|
43
64
|
}
|
|
44
|
-
|
|
65
|
+
/** Returns `null` (dropped, same convention as `agentsNativeCheck`/`hookTrustCheck`) when
|
|
66
|
+
* `dir` is null — i.e. the provider has no global skill discovery mechanism at all
|
|
67
|
+
* (today: Copilot, see `globalUnsupportedReason` in providers/index.ts).
|
|
68
|
+
*
|
|
69
|
+
* `renderer` gates which verification is possible: `classifyGlobalSkills` (via `integrity`)
|
|
70
|
+
* only ever sees symlinks — `if (!lst.isSymbolicLink()) continue;` — so for a rendered
|
|
71
|
+
* format (`cursor-mdc`, `copilot-instructions`) it scans a directory of real files and
|
|
72
|
+
* finds nothing, which would make `broken` silently read 0 regardless of whether the
|
|
73
|
+
* rendered files are actually well-formed. Reporting `'healthy'` from a scan that
|
|
74
|
+
* structurally can't see the files it's supposed to check would be a false green, so for
|
|
75
|
+
* any non-`'link'` renderer this reports presence only, honestly, via `detail`. */
|
|
76
|
+
function skillsGlobalCheck(dir, owners, integrity, renderer) {
|
|
77
|
+
if (dir === null)
|
|
78
|
+
return null;
|
|
79
|
+
if (renderer !== 'link') {
|
|
80
|
+
let entries;
|
|
81
|
+
try {
|
|
82
|
+
entries = fs_1.default.readdirSync(dir);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// best-effort: any readdirSync failure (absent dir, EACCES, …) reads the
|
|
86
|
+
// same as "nothing installed" here — a permissions error surfacing as
|
|
87
|
+
// `remediationCode: 'awm-init'` is a worse remedy than none, but this
|
|
88
|
+
// check has no channel to report "can't tell" separately from "absent"
|
|
89
|
+
// (same tradeoff already made by this file's agentsNativeCheck and by
|
|
90
|
+
// skill-integrity.ts's classifyGlobalSkills — a systemic, pre-existing
|
|
91
|
+
// pattern in this codebase, not introduced here).
|
|
92
|
+
entries = [];
|
|
93
|
+
}
|
|
94
|
+
// Require at least one entry with the extension this renderer actually produces,
|
|
95
|
+
// not just ANY file — a directory non-empty only because of the user's own
|
|
96
|
+
// pre-existing, unrelated rule/instructions file must not read as "AWM installed".
|
|
97
|
+
// Still not full integrity verification (a stray file with the right extension but
|
|
98
|
+
// wrong content still passes) — that residual gap is the same honest tradeoff this
|
|
99
|
+
// function's doc comment already accepts for the non-`'link'` branch generally.
|
|
100
|
+
const ext = RENDERED_SKILL_EXTENSIONS[renderer];
|
|
101
|
+
const present = ext ? entries.some((e) => e.endsWith(ext)) : entries.length > 0;
|
|
102
|
+
return {
|
|
103
|
+
id: 'skills.global',
|
|
104
|
+
state: present ? 'supported' : 'absent',
|
|
105
|
+
target: dir,
|
|
106
|
+
owners: owners.length > 1 ? owners : undefined,
|
|
107
|
+
detail: present ? 'rendered install — content integrity not verified' : undefined,
|
|
108
|
+
remediationCode: present ? undefined : 'awm-init',
|
|
109
|
+
};
|
|
110
|
+
}
|
|
45
111
|
const shared = owners.length > 1;
|
|
46
112
|
const broken = integrity.repairable.length + integrity.dead.length;
|
|
47
113
|
// Broken links are checked BEFORE shared: 'shared' is a non-degrading/OK state
|
|
@@ -101,7 +167,7 @@ function tomlAgentsHealthy(dir, entries) {
|
|
|
101
167
|
*/
|
|
102
168
|
function agentsNativeCheck(agent) {
|
|
103
169
|
const provider = (0, providers_1.providerFor)(agent);
|
|
104
|
-
if (!provider.agent)
|
|
170
|
+
if (!provider.agent || provider.agent.global === null)
|
|
105
171
|
return null;
|
|
106
172
|
const dir = provider.agent.global;
|
|
107
173
|
let entries;
|
|
@@ -152,21 +218,34 @@ function hookTrustCheck(agent) {
|
|
|
152
218
|
/** R7: reflects the provider's global context-delivery mechanism (config-instructions /
|
|
153
219
|
* managed-agents-md). claude-code's context rides the SessionStart hook — already
|
|
154
220
|
* covered by hook.trust, so this check is OMITTED (returns null) rather than reported,
|
|
155
|
-
* to avoid double-reporting the same fact as a separate, redundant row.
|
|
156
|
-
|
|
221
|
+
* to avoid double-reporting the same fact as a separate, redundant row.
|
|
222
|
+
*
|
|
223
|
+
* Scope mirrors `init/steps.ts`'s `stepContextInjection` exactly: a `managed-agents-md`
|
|
224
|
+
* provider with `globalPath === null` (today: Cursor, Copilot) has no user-level
|
|
225
|
+
* AGENTS.md-equivalent file, so its context is legitimately delivered at LOCAL (project)
|
|
226
|
+
* scope instead — asking `contextStatus` about 'global' for these providers always
|
|
227
|
+
* resolved to 'absent' regardless of whether the local injection actually succeeded. The
|
|
228
|
+
* check's `id` stays `'context.global'` either way (stable JSON field); only the
|
|
229
|
+
* underlying scope resolution changes. `projectRoot` is only meaningful when the resolved
|
|
230
|
+
* scope is 'local' — if it's needed but missing, `contextStatus` throws cleanly
|
|
231
|
+
* (`InjectionOrchestrator`'s `contextPathFor`) and the catch below falls back to 'absent',
|
|
232
|
+
* same as any other failure to resolve status. */
|
|
233
|
+
function contextGlobalCheck(agent, projectRoot) {
|
|
157
234
|
const injection = (0, providers_1.providerFor)(agent).injection;
|
|
158
235
|
if (!injection || injection.type === 'cc-settings-merge') {
|
|
159
236
|
return null;
|
|
160
237
|
}
|
|
238
|
+
const scope = injection.type === 'managed-agents-md' && injection.globalPath === null ? 'local' : 'global';
|
|
161
239
|
let state = 'absent';
|
|
162
240
|
try {
|
|
163
241
|
const orchestrator = new orchestrator_1.InjectionOrchestrator();
|
|
164
242
|
const result = orchestrator.contextStatus({
|
|
165
243
|
agent,
|
|
166
|
-
scope
|
|
244
|
+
scope,
|
|
167
245
|
registryRoot: (0, registries_1.capabilityRoot)('skills') ?? '',
|
|
168
246
|
installMethod: 'symlink',
|
|
169
247
|
profileExtensions: [],
|
|
248
|
+
projectRoot,
|
|
170
249
|
});
|
|
171
250
|
state = result === 'injected' ? 'delivered' : result === 'stale' ? 'stale' : 'absent';
|
|
172
251
|
}
|
|
@@ -190,10 +269,12 @@ function contextGlobalCheck(agent) {
|
|
|
190
269
|
* snapshot for rollback comparison — a different shape, a different purpose). No file
|
|
191
270
|
* currently imports both, but the names are close enough to trip up a future reader/editor.
|
|
192
271
|
*/
|
|
193
|
-
function gatherProviderChecks(agents, scanSkills) {
|
|
272
|
+
function gatherProviderChecks(agents, scanSkills, projectRoot) {
|
|
194
273
|
const ownersByDir = new Map();
|
|
195
274
|
for (const agent of agents) {
|
|
196
275
|
const dir = (0, providers_1.providerFor)(agent).skill.global;
|
|
276
|
+
if (dir === null)
|
|
277
|
+
continue;
|
|
197
278
|
const owners = ownersByDir.get(dir) ?? [];
|
|
198
279
|
owners.push(agent);
|
|
199
280
|
ownersByDir.set(dir, owners);
|
|
@@ -205,15 +286,15 @@ function gatherProviderChecks(agents, scanSkills) {
|
|
|
205
286
|
return agents.map((agent) => {
|
|
206
287
|
const provider = (0, providers_1.providerFor)(agent);
|
|
207
288
|
const dir = provider.skill.global;
|
|
208
|
-
const owners = ownersByDir.get(dir) ?? [agent];
|
|
209
|
-
const integrity = scansByDir.get(dir) ?? { valid: [], repairable: [], dead: [] };
|
|
289
|
+
const owners = (dir !== null ? ownersByDir.get(dir) : undefined) ?? [agent];
|
|
290
|
+
const integrity = (dir !== null ? scansByDir.get(dir) : undefined) ?? { valid: [], repairable: [], dead: [] };
|
|
210
291
|
const checks = [
|
|
211
292
|
binaryVersionCheck(agent),
|
|
212
|
-
skillsGlobalCheck(dir, owners, integrity),
|
|
293
|
+
skillsGlobalCheck(dir, owners, integrity, provider.skill.renderer),
|
|
213
294
|
agentsNativeCheck(agent),
|
|
214
295
|
hookTrustCheck(agent),
|
|
215
|
-
contextGlobalCheck(agent),
|
|
296
|
+
contextGlobalCheck(agent, projectRoot),
|
|
216
297
|
].filter((check) => check !== null);
|
|
217
|
-
return { id: agent, label: provider.label, checks };
|
|
298
|
+
return { id: agent, label: provider.label, tier: providerTier(provider), checks };
|
|
218
299
|
});
|
|
219
300
|
}
|
|
@@ -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
|
-
|
|
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
|
|
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');
|