agentic-workflow-manager 3.11.0 → 3.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/commands/doctor.js +1 -1
- package/dist/src/commands/preflight/checks.js +63 -5
- package/dist/src/commands/preflight/index.js +6 -1
- package/dist/src/commands/sensors/baseline.js +4 -3
- 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/commands/preflight/preflight.test.js +95 -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('');
|
|
@@ -9,6 +9,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
10
|
const status_1 = require("../sensors/status");
|
|
11
11
|
const init_1 = require("../sensors/init");
|
|
12
|
+
const baseline_1 = require("../sensors/baseline");
|
|
12
13
|
const paths_1 = require("../../core/paths");
|
|
13
14
|
const MANIFEST = path_1.default.join('.awm', 'sensors.json');
|
|
14
15
|
/**
|
|
@@ -36,6 +37,18 @@ function readManifest(cwd) {
|
|
|
36
37
|
return null;
|
|
37
38
|
}
|
|
38
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Total sensor entries and how many are enabled (`enabled !== false`, so a missing
|
|
42
|
+
* `enabled` field defaults to counted-as-enabled). Shared by `checkManifest` and
|
|
43
|
+
* `checkSensorsBaseline` — they were previously two copies of the identical
|
|
44
|
+
* predicate, which post-implementation-qa flagged: a hardening applied to one (e.g.
|
|
45
|
+
* guarding a malformed sensor entry) would silently NOT apply to the other unless
|
|
46
|
+
* someone remembered to edit both. One function, two callers, one place to harden.
|
|
47
|
+
*/
|
|
48
|
+
function countEnabledSensors(manifest) {
|
|
49
|
+
const entries = Object.values(manifest.sensors ?? {});
|
|
50
|
+
return { total: entries.length, enabled: entries.filter(s => s.enabled !== false).length };
|
|
51
|
+
}
|
|
39
52
|
/**
|
|
40
53
|
* A repo may legitimately have no sensors — but it has to SAY so, in a committed file.
|
|
41
54
|
*
|
|
@@ -62,8 +75,7 @@ function checkManifest(cwd, manifest) {
|
|
|
62
75
|
remedy: 'fix or regenerate it with `awm sensors init`',
|
|
63
76
|
};
|
|
64
77
|
}
|
|
65
|
-
const total =
|
|
66
|
-
const enabled = Object.values(manifest.sensors ?? {}).filter(s => s.enabled !== false).length;
|
|
78
|
+
const { total, enabled } = countEnabledSensors(manifest);
|
|
67
79
|
// total === 0 is NOT an opt-out: a deliberate opt-out lists every known sensor NAME
|
|
68
80
|
// explicitly with `enabled: false` (total > 0, enabled === 0). Zero entries means
|
|
69
81
|
// nothing was ever configured — most commonly because the registry had no pack.json
|
|
@@ -135,6 +147,51 @@ function checkPack(cwd, manifest) {
|
|
|
135
147
|
}
|
|
136
148
|
return { id: 'pack', ok: true, detail: `${manifest.pack} matches the detected stack` };
|
|
137
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Advisory only — `ok` is ALWAYS `true`, same contract as `checkHost` below. A team
|
|
152
|
+
* adopting AWM on a legacy repo starts with pre-existing sensor findings; the ratchet
|
|
153
|
+
* (`awm sensors baseline`, `.awm/sensors.baseline.json`) exists precisely to snapshot
|
|
154
|
+
* those as accepted debt so the gate only fails on genuinely NEW findings. But nothing
|
|
155
|
+
* today surfaces that the mechanism exists — the team discovers it only after hitting a
|
|
156
|
+
* wall of red findings and going looking. This nudges them toward it before that
|
|
157
|
+
* happens. It never blocks preflight: a repo can legitimately have zero debt to
|
|
158
|
+
* snapshot (sensors enabled from day one), and "no baseline yet" is not itself a
|
|
159
|
+
* failure — only the operator's lack of awareness that baselining is an option is the
|
|
160
|
+
* problem this addresses.
|
|
161
|
+
*
|
|
162
|
+
* Only called when a manifest exists (see the conditional spread in `preflight()`) —
|
|
163
|
+
* there is nothing to baseline without sensors configured in the first place, so this
|
|
164
|
+
* mirrors how `checkTools`/`checkPack` are skipped entirely rather than reported on a
|
|
165
|
+
* repo that was never set up.
|
|
166
|
+
*
|
|
167
|
+
* Also requires at least one ENABLED sensor, via the same `countEnabledSensors` helper
|
|
168
|
+
* `checkManifest` uses — a deliberate opt-out (every sensor `enabled: false`) or an
|
|
169
|
+
* unparseable/empty manifest has nothing to baseline either, and nudging "run `awm
|
|
170
|
+
* sensors baseline`" there would be actively misleading rather than merely unnecessary.
|
|
171
|
+
*
|
|
172
|
+
* Presence is checked via `readBaseline` (same function `partition()` uses at gate time
|
|
173
|
+
* to decide suppression), not a raw `fs.existsSync` — a baseline PATH that exists but
|
|
174
|
+
* isn't a readable JSON file (e.g. a stray directory at that path) is treated by the
|
|
175
|
+
* real gate as "no baseline, nothing suppressed"; `existsSync` alone would have reported
|
|
176
|
+
* "baseline present" for that same case, reassuring the operator that debt is being
|
|
177
|
+
* suppressed when it silently is not.
|
|
178
|
+
*/
|
|
179
|
+
function checkSensorsBaseline(cwd, manifest) {
|
|
180
|
+
const enabled = manifest ? countEnabledSensors(manifest).enabled : 0;
|
|
181
|
+
if (enabled === 0) {
|
|
182
|
+
return { id: 'sensors-baseline', ok: true, detail: 'no enabled sensors — nothing to baseline' };
|
|
183
|
+
}
|
|
184
|
+
if ((0, baseline_1.readBaseline)(cwd) !== null) {
|
|
185
|
+
return { id: 'sensors-baseline', ok: true, detail: 'baseline present' };
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
id: 'sensors-baseline',
|
|
189
|
+
ok: true,
|
|
190
|
+
detail: 'sensors configured, no baseline yet — awm sensors baseline',
|
|
191
|
+
remedy: 'run `awm sensors baseline` to snapshot pre-existing findings as accepted debt, '
|
|
192
|
+
+ 'so the gate only chases new problems',
|
|
193
|
+
};
|
|
194
|
+
}
|
|
138
195
|
/**
|
|
139
196
|
* Extract just the hostname portion of a git remote URL — never match against the
|
|
140
197
|
* full URL string. A bare substring check against the whole remote (`remote.includes
|
|
@@ -224,9 +281,10 @@ function preflight(cwd = process.cwd()) {
|
|
|
224
281
|
const checks = [
|
|
225
282
|
checkContext(cwd),
|
|
226
283
|
checkManifest(cwd, manifest),
|
|
227
|
-
// Skipped when there is no manifest: reporting "tools broken"
|
|
228
|
-
//
|
|
229
|
-
|
|
284
|
+
// Skipped when there is no manifest: reporting "tools broken" (or nudging toward
|
|
285
|
+
// a baseline that has nothing to snapshot) on a repo that was never set up
|
|
286
|
+
// buries the one thing the operator needs to read.
|
|
287
|
+
...(manifestExists ? [checkTools(cwd), checkPack(cwd, manifest), checkSensorsBaseline(cwd, manifest)] : []),
|
|
230
288
|
// Runs unconditionally — orthogonal to sensor configuration entirely, this is
|
|
231
289
|
// about PR/MR tooling, not sensors.
|
|
232
290
|
checkHost(cwd),
|
|
@@ -20,7 +20,12 @@ function exitCodeFor(report) {
|
|
|
20
20
|
return report.status === 'ready' ? 0 : 1;
|
|
21
21
|
}
|
|
22
22
|
function formatReport(report) {
|
|
23
|
-
|
|
23
|
+
// Computed from the actual ids present, not hardcoded to the widest id THIS
|
|
24
|
+
// report happens to have — a fixed literal here silently misaligns the moment a
|
|
25
|
+
// longer `PreflightCheck['id']` is added (confirmed: 'sensors-baseline', at 16
|
|
26
|
+
// chars, broke a hardcoded 9-char pad).
|
|
27
|
+
const idWidth = Math.max(0, ...report.checks.map(c => c.id.length));
|
|
28
|
+
const lines = report.checks.map(c => ` ${c.ok ? picocolors_1.default.green('✔') : picocolors_1.default.red('✘')} ${c.id.padEnd(idWidth)} ${c.detail}`
|
|
24
29
|
+ (c.remedy ? `\n ${picocolors_1.default.dim('→ ' + c.remedy)}` : ''));
|
|
25
30
|
if (report.status === 'ready') {
|
|
26
31
|
return `${picocolors_1.default.green('✔')} Harness ready — this project can be gated.\n${lines.join('\n')}\n`;
|
|
@@ -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.BASELINE_FILE = void 0;
|
|
6
7
|
exports.fingerprint = fingerprint;
|
|
7
8
|
exports.readBaseline = readBaseline;
|
|
8
9
|
exports.writeBaseline = writeBaseline;
|
|
@@ -11,7 +12,7 @@ exports.buildBaseline = buildBaseline;
|
|
|
11
12
|
const crypto_1 = __importDefault(require("crypto"));
|
|
12
13
|
const fs_1 = __importDefault(require("fs"));
|
|
13
14
|
const path_1 = __importDefault(require("path"));
|
|
14
|
-
|
|
15
|
+
exports.BASELINE_FILE = path_1.default.join('.awm', 'sensors.baseline.json');
|
|
15
16
|
/**
|
|
16
17
|
* Mask runs of digits so location noise embedded in messages (e.g. the tsc
|
|
17
18
|
* formatter writes "... line 199 ...") doesn't change the fingerprint when code
|
|
@@ -43,7 +44,7 @@ function fingerprint(sensor, e) {
|
|
|
43
44
|
return crypto_1.default.createHash('sha1').update(basis).digest('hex');
|
|
44
45
|
}
|
|
45
46
|
function readBaseline(cwd) {
|
|
46
|
-
const p = path_1.default.join(cwd, BASELINE_FILE);
|
|
47
|
+
const p = path_1.default.join(cwd, exports.BASELINE_FILE);
|
|
47
48
|
if (!fs_1.default.existsSync(p))
|
|
48
49
|
return null;
|
|
49
50
|
try {
|
|
@@ -55,7 +56,7 @@ function readBaseline(cwd) {
|
|
|
55
56
|
}
|
|
56
57
|
function writeBaseline(cwd, baseline) {
|
|
57
58
|
fs_1.default.mkdirSync(path_1.default.join(cwd, '.awm'), { recursive: true });
|
|
58
|
-
fs_1.default.writeFileSync(path_1.default.join(cwd, BASELINE_FILE), JSON.stringify(baseline, null, 2), 'utf-8');
|
|
59
|
+
fs_1.default.writeFileSync(path_1.default.join(cwd, exports.BASELINE_FILE), JSON.stringify(baseline, null, 2), 'utf-8');
|
|
59
60
|
}
|
|
60
61
|
/**
|
|
61
62
|
* Split a sensor's findings into new vs baseline-suppressed. With no accepted
|
|
@@ -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
|
}
|