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.
Files changed (39) hide show
  1. package/dist/src/commands/doctor.js +1 -1
  2. package/dist/src/core/context/materializer.js +7 -0
  3. package/dist/src/core/context/orchestrator.js +26 -6
  4. package/dist/src/core/context/strategies/codex-agents.js +69 -15
  5. package/dist/src/core/diagnostics/context.js +11 -6
  6. package/dist/src/core/diagnostics/provider-checks.js +92 -11
  7. package/dist/src/core/init/mutation-targets.js +18 -2
  8. package/dist/src/core/init/provider-facts.js +5 -4
  9. package/dist/src/core/init/steps.js +16 -2
  10. package/dist/src/core/install-planner.js +56 -6
  11. package/dist/src/core/install-transaction.js +55 -6
  12. package/dist/src/core/provider-artifacts.js +1 -1
  13. package/dist/src/core/renderers/copilot-instructions.js +28 -0
  14. package/dist/src/core/renderers/cursor-mdc.js +49 -0
  15. package/dist/src/core/renderers/skill-source.js +50 -0
  16. package/dist/src/core/skill-integrity.js +1 -1
  17. package/dist/src/index.js +8 -0
  18. package/dist/src/providers/index.js +69 -2
  19. package/dist/tests/commands/add.test.js +96 -0
  20. package/dist/tests/commands/doctor.test.js +25 -0
  21. package/dist/tests/commands/init.test.js +56 -0
  22. package/dist/tests/core/bundle-install.test.js +63 -0
  23. package/dist/tests/core/context/materializer.test.js +8 -0
  24. package/dist/tests/core/context/orchestrator.test.js +51 -0
  25. package/dist/tests/core/context/strategies/codex-agents.test.js +157 -20
  26. package/dist/tests/core/diagnostics/checks.test.js +1 -0
  27. package/dist/tests/core/diagnostics/provider-tier.test.js +292 -0
  28. package/dist/tests/core/init/mutation-targets.test.js +63 -0
  29. package/dist/tests/core/init/provider-facts.test.js +16 -0
  30. package/dist/tests/core/init/steps.test.js +37 -0
  31. package/dist/tests/core/install-planner.test.js +118 -0
  32. package/dist/tests/core/install-transaction.test.js +109 -0
  33. package/dist/tests/core/provider-artifacts.test.js +11 -0
  34. package/dist/tests/core/renderers/copilot-instructions.test.js +47 -0
  35. package/dist/tests/core/renderers/cursor-mdc.test.js +137 -0
  36. package/dist/tests/core/skill-integrity.test.js +18 -0
  37. package/dist/tests/providers/index.test.js +45 -1
  38. package/dist/tests/providers/injection-config.test.js +16 -0
  39. 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.overrides.contextPathOverride ?? (0, materializer_1.globalContextPath)();
45
+ const absPath = this.contextPathFor(op);
35
46
  const ref = (0, materializer_1.materialize)(ctx, absPath, op.scope);
36
- return { ref, registryRoot: op.registryRoot, installMethod: op.installMethod, agent: op.agent, scope: op.scope };
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.overrides.contextPathOverride ?? (0, materializer_1.globalContextPath)();
54
+ const absPath = this.contextPathFor(op);
41
55
  const ref = { absPath, scope: op.scope, contentHash: '' };
42
- return { ref, registryRoot: op.registryRoot, installMethod: op.installMethod, agent: op.agent, scope: op.scope };
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.overrides.contextPathOverride ?? (0, materializer_1.globalContextPath)();
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 { ref, registryRoot: op.registryRoot, installMethod: op.installMethod, agent: op.agent, scope: op.scope };
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.assertGlobalInput(input);
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.globalPath(provider), markdown);
56
+ return injectFile(this.targetFile(provider, scope, input.projectRoot), withProjectGuidance(markdown, scope));
47
57
  }
48
58
  remove(input, provider) {
49
- this.assertGlobalInput(input);
50
- const file = this.globalPath(provider);
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.assertGlobalInput(input);
60
- const file = this.globalPath(provider);
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(path_1.default.join((0, paths_1.homeDir)(), '.codex', 'AGENTS.md'), context.markdown);
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
- return injectFile(path_1.default.join(projectRoot, 'AGENTS.md'), PROJECT_GUIDANCE);
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
- assertGlobalInput(input) {
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
- if (input.scope !== 'global' || input.ref?.scope !== 'global') {
93
- throw new Error('CodexAgentsStrategy supports only global injection');
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 } = classifyLinks(skillNames, skillsDir);
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: (0, skill_integrity_1.classifyGlobalSkills)(skillsDir, (0, registries_1.contentRoots)()),
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
- function skillsGlobalCheck(dir, owners, integrity) {
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
- function contextGlobalCheck(agent) {
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: 'global',
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
- 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');