agentic-workflow-manager 3.13.5 → 3.13.6
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/add.js +5 -0
- package/dist/src/commands/doctor.js +1 -0
- package/dist/src/commands/hooks/claude.js +15 -2
- package/dist/src/commands/hooks/shared.js +6 -1
- package/dist/src/commands/init.js +1 -0
- package/dist/src/commands/pin.js +24 -4
- package/dist/src/commands/preflight/checks.js +3 -2
- package/dist/src/commands/registry/add.js +1 -1
- package/dist/src/commands/sensors/index.js +7 -0
- package/dist/src/commands/sensors/init.js +48 -7
- package/dist/src/commands/sensors/install.js +20 -11
- package/dist/src/commands/sensors/run.js +31 -29
- package/dist/src/commands/sync.js +29 -0
- package/dist/src/commands/watch/index.js +11 -1
- package/dist/src/core/artifact-name.js +80 -0
- package/dist/src/core/bundle-install.js +55 -12
- package/dist/src/core/bundles.js +72 -13
- package/dist/src/core/context/regenerate.js +21 -4
- package/dist/src/core/context/strategies/codex-agents.js +18 -2
- package/dist/src/core/diagnostics/checks.js +26 -2
- package/dist/src/core/diagnostics/context.js +91 -25
- package/dist/src/core/diagnostics/provider-checks.js +62 -14
- package/dist/src/core/discovery.js +2 -2
- package/dist/src/core/init/mutation-targets.js +11 -0
- package/dist/src/core/init/steps.js +70 -19
- package/dist/src/core/install-planner.js +62 -17
- package/dist/src/core/install-transaction.js +13 -1
- package/dist/src/core/journal/adapter.js +10 -1
- package/dist/src/core/paths.js +61 -8
- package/dist/src/core/profile.js +12 -0
- package/dist/src/core/provider-artifacts.js +38 -13
- package/dist/src/core/provider-version.js +12 -6
- package/dist/src/core/registries.js +40 -19
- package/dist/src/core/renderers/registry.js +37 -0
- package/dist/src/core/skill-integrity.js +71 -10
- package/dist/src/core/update-check.js +14 -2
- package/dist/src/core/versioning.js +17 -2
- package/dist/src/index.js +31 -23
- package/dist/src/providers/index.js +7 -2
- package/dist/src/utils/config.js +7 -8
- package/dist/tests/commands/doctor.test.js +6 -0
- package/dist/tests/commands/pin.test.js +29 -2
- package/dist/tests/commands/preflight/preflight.test.js +31 -17
- package/dist/tests/commands/registry/install-bundles.test.js +6 -6
- package/dist/tests/commands/sensors/init-pack-unavailable.test.js +96 -0
- package/dist/tests/commands/sensors/install-settings-safety.test.js +87 -0
- package/dist/tests/commands/sensors/run-is-read-only.test.js +0 -0
- package/dist/tests/commands/sensors/run.test.js +24 -51
- package/dist/tests/commands/sensors/status-windows.test.js +39 -22
- package/dist/tests/commands/sensors/status.test.js +27 -12
- package/dist/tests/core/artifact-name-containment.test.js +86 -0
- package/dist/tests/core/context/agents-md-single-slot.test.js +69 -0
- package/dist/tests/core/context/regenerate.test.js +39 -8
- package/dist/tests/core/diagnostics/checks.test.js +14 -6
- package/dist/tests/core/diagnostics/provider-tier.test.js +3 -3
- package/dist/tests/core/diagnostics/rendered-artifact-visibility.test.js +69 -0
- package/dist/tests/core/init/all-baseline-bundles.test.js +64 -0
- package/dist/tests/core/init/context-injection-no-project.test.js +117 -0
- package/dist/tests/core/init/orchestrator.test.js +1 -1
- package/dist/tests/core/init/steps-registry-sync.test.js +1 -1
- package/dist/tests/core/init/steps.test.js +11 -5
- package/dist/tests/core/path-resolution-no-shell.test.js +136 -0
- package/dist/tests/core/paths.test.js +7 -34
- package/dist/tests/core/project-skill-links.test.js +159 -0
- package/dist/tests/core/provider-artifacts.test.js +66 -14
- package/dist/tests/core/provider-capability-guards.test.js +92 -0
- package/dist/tests/core/reconciliation.test.js +15 -15
- package/dist/tests/core/registry-manifest.test.js +1 -1
- package/dist/tests/core/semver-fails-closed.test.js +30 -0
- package/dist/tests/core/skill-integrity.test.js +10 -10
- package/dist/tests/core/skill-repair-safety.test.js +92 -0
- package/dist/tests/core/sync-profile-atomicity.test.js +122 -0
- package/dist/tests/integration/copilot-init-isolated.test.js +2 -2
- package/dist/tests/providers/index.test.js +1 -1
- package/dist/tests/structural/renderer-table-is-single-source.test.js +95 -0
- package/dist/tests/structural/symlink-type-is-explicit.test.js +73 -0
- package/dist/tests/utils/registry-view-overrides.test.js +2 -2
- package/package.json +4 -1
|
@@ -10,6 +10,7 @@ exports.syncProfile = syncProfile;
|
|
|
10
10
|
// src/core/bundle-install.ts
|
|
11
11
|
const fs_1 = __importDefault(require("fs"));
|
|
12
12
|
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const artifact_name_1 = require("./artifact-name");
|
|
13
14
|
const bundles_1 = require("./bundles");
|
|
14
15
|
const providers_1 = require("../providers");
|
|
15
16
|
const profile_1 = require("./profile");
|
|
@@ -18,14 +19,24 @@ const config_1 = require("../utils/config");
|
|
|
18
19
|
const install_planner_1 = require("./install-planner");
|
|
19
20
|
const install_transaction_1 = require("./install-transaction");
|
|
20
21
|
function bundleArtifacts(b, contentDir) {
|
|
22
|
+
// Estos nombres vienen VERBATIM del `bundle.json` de un registry — contenido
|
|
23
|
+
// de terceros. Sin validar, `path.join(installDir, name)` con `../../.ssh`
|
|
24
|
+
// escapaba del directorio destino, y como el instalador hace
|
|
25
|
+
// `rmSync(target, {recursive:true})` antes de enlazar, eso BORRABA el ~/.ssh
|
|
26
|
+
// real del usuario. Se valida en el origen, donde el mensaje puede nombrar
|
|
27
|
+
// el artefacto culpable; `physicalTarget` ademas asegura contencion
|
|
28
|
+
// estructural sobre la ruta ya resuelta.
|
|
21
29
|
const refs = [];
|
|
22
30
|
for (const s of b.skills) {
|
|
31
|
+
(0, artifact_name_1.assertSafeArtifactName)(s.name, 'skill');
|
|
23
32
|
refs.push({ name: s.name, type: 'skill', installName: s.name, sourcePath: path_1.default.join(contentDir, 'skills', s.name) });
|
|
24
33
|
}
|
|
25
34
|
for (const w of b.workflows) {
|
|
35
|
+
(0, artifact_name_1.assertSafeArtifactName)(w, 'workflow');
|
|
26
36
|
refs.push({ name: w, type: 'workflow', installName: `${w}.md`, sourcePath: path_1.default.join(contentDir, 'workflows', `${w}.md`) });
|
|
27
37
|
}
|
|
28
38
|
for (const a of b.agents) {
|
|
39
|
+
(0, artifact_name_1.assertSafeArtifactName)(a, 'agent');
|
|
29
40
|
refs.push({ name: a, type: 'agent', installName: `${a}.md`, sourcePath: path_1.default.join(contentDir, 'agents', `${a}.md`) });
|
|
30
41
|
}
|
|
31
42
|
return refs;
|
|
@@ -109,30 +120,62 @@ function addBundle(opts) {
|
|
|
109
120
|
*/
|
|
110
121
|
function syncProfile(opts) {
|
|
111
122
|
const profile = (0, profile_1.readProfile)(opts.projectRoot);
|
|
112
|
-
const installed = [];
|
|
113
123
|
const skipped = [];
|
|
114
|
-
|
|
115
|
-
|
|
124
|
+
// ONE plan for the whole sync, not one per extension.
|
|
125
|
+
//
|
|
126
|
+
// This used to loop `installBundle` over `profile.extensions`, so N extensions
|
|
127
|
+
// meant N independent `applyInstallPlan` transactions. Each is atomic on its own;
|
|
128
|
+
// none is atomic with the others. A failure on the third extension left the first
|
|
129
|
+
// two committed and the tree in a state matching neither before nor after — and
|
|
130
|
+
// `awm sync` is the command a team runs to make a checkout match its profile, so
|
|
131
|
+
// "partially matches" is the one outcome it must not produce.
|
|
132
|
+
//
|
|
133
|
+
// Merging is safe because the two halves were already built for it: expanding a
|
|
134
|
+
// bundle (`expandBundleArtifacts`) is pure, and `planInstall` dedups by physical
|
|
135
|
+
// target across ALL the artifacts it is handed — which is also why two extensions
|
|
136
|
+
// sharing a dependency now collapse into one operation instead of racing to write
|
|
137
|
+
// the same path twice. `applyInstallPlan` backs up and rolls back per plan, so one
|
|
138
|
+
// plan is precisely what makes the sync atomic.
|
|
139
|
+
const intents = [];
|
|
116
140
|
for (const ext of profile.extensions) {
|
|
117
|
-
|
|
141
|
+
const bundle = opts.bundles.find((b) => b.name === ext);
|
|
142
|
+
if (!bundle) {
|
|
118
143
|
skipped.push(`${ext} (bundle not found in registry — remove with \`awm remove ${ext}\`)`);
|
|
119
144
|
continue;
|
|
120
145
|
}
|
|
121
|
-
|
|
146
|
+
intents.push(...expandBundleArtifacts({
|
|
122
147
|
bundleName: ext,
|
|
123
148
|
bundles: opts.bundles,
|
|
124
149
|
agents: opts.agents,
|
|
125
150
|
method: opts.method,
|
|
126
151
|
projectRoot: opts.projectRoot,
|
|
127
152
|
contentDir: opts.contentDir,
|
|
128
|
-
|
|
129
|
-
});
|
|
130
|
-
installed.push(...summary.installed);
|
|
131
|
-
skipped.push(...summary.skipped);
|
|
132
|
-
modifiedFiles.push(...summary.modifiedFiles);
|
|
133
|
-
transactionIds.push(summary.transactionId);
|
|
153
|
+
}));
|
|
134
154
|
}
|
|
135
155
|
if (profile.extensions.length > 0)
|
|
136
156
|
(0, profile_1.ensureSkillsGitignored)(opts.projectRoot, opts.agents);
|
|
137
|
-
|
|
157
|
+
// Nothing resolvable: no plan, no transaction, nothing to name to `awm backup
|
|
158
|
+
// restore`. An empty `transactionIds` is the honest answer, not a phantom id.
|
|
159
|
+
if (intents.length === 0) {
|
|
160
|
+
return { installed: [], skipped, extensions: profile.extensions, transactionIds: [], modifiedFiles: [] };
|
|
161
|
+
}
|
|
162
|
+
const plan = (0, install_planner_1.planInstall)({
|
|
163
|
+
artifacts: intents,
|
|
164
|
+
selectedAgents: opts.agents,
|
|
165
|
+
enabledAgents: (0, config_1.getPreferences)().enabledAgents,
|
|
166
|
+
// Every extension in the profile is a project-scope bundle by construction
|
|
167
|
+
// (`shouldRecordExtension` only records those), so the whole sync resolves to
|
|
168
|
+
// one scope — which is what lets it be one plan.
|
|
169
|
+
scope: 'local',
|
|
170
|
+
projectRoot: opts.projectRoot,
|
|
171
|
+
method: opts.method,
|
|
172
|
+
});
|
|
173
|
+
const summary = (opts.applyPlan ?? install_transaction_1.applyInstallPlan)(plan);
|
|
174
|
+
return {
|
|
175
|
+
installed: summary.installed,
|
|
176
|
+
skipped: [...skipped, ...summary.skipped],
|
|
177
|
+
extensions: profile.extensions,
|
|
178
|
+
transactionIds: [summary.transactionId],
|
|
179
|
+
modifiedFiles: summary.modifiedFiles,
|
|
180
|
+
};
|
|
138
181
|
}
|
package/dist/src/core/bundles.js
CHANGED
|
@@ -16,15 +16,59 @@ const registries_1 = require("./registries");
|
|
|
16
16
|
function catalogPath(contentDir) {
|
|
17
17
|
return path_1.default.join(contentDir, 'catalog.json');
|
|
18
18
|
}
|
|
19
|
+
/** Lee `catalog.json` de un registry.
|
|
20
|
+
*
|
|
21
|
+
* El contenido de un registry es INPUT NO CONFIABLE (un registry de equipo,
|
|
22
|
+
* interno, o agregado con `awm registry add`), asi que se valida forma antes de
|
|
23
|
+
* desreferenciar. Sin esto, un solo archivo malformado en CUALQUIER registry
|
|
24
|
+
* — incluido uno que llega por un `awm update` rutinario — hacia que `awm list`
|
|
25
|
+
* y `awm add` murieran con un stack trace crudo de Node y ningun mensaje
|
|
26
|
+
* accionable. Los demas lectores de contenido de registry de este repo
|
|
27
|
+
* (readRegistriesConfig, readRegistryManifest, readProfile) ya validaban; estos
|
|
28
|
+
* dos eran los que faltaban. */
|
|
19
29
|
function readCatalog(contentDir) {
|
|
20
30
|
const file = catalogPath(contentDir);
|
|
21
31
|
if (!fs_1.default.existsSync(file))
|
|
22
32
|
return [];
|
|
23
|
-
|
|
24
|
-
|
|
33
|
+
let parsed;
|
|
34
|
+
try {
|
|
35
|
+
parsed = JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new Error(`${file} is not valid JSON. Fix the file in its registry, then re-run \`awm update\`.`);
|
|
39
|
+
}
|
|
40
|
+
if (parsed === null || typeof parsed !== 'object') {
|
|
41
|
+
throw new Error(`${file} must contain a JSON object.`);
|
|
42
|
+
}
|
|
43
|
+
const bundles = parsed.bundles;
|
|
44
|
+
if (bundles === undefined)
|
|
45
|
+
return [];
|
|
46
|
+
if (!Array.isArray(bundles)) {
|
|
47
|
+
throw new Error(`${file}: "bundles" must be an array.`);
|
|
48
|
+
}
|
|
49
|
+
// Una entrada sin `source` usable haria que `path.join` tirara un
|
|
50
|
+
// TypeError opaco mas adelante; se descarta aca, nombrando el archivo.
|
|
51
|
+
return bundles.filter((entry) => entry !== null && typeof entry === 'object' && typeof entry.source === 'string');
|
|
25
52
|
}
|
|
53
|
+
/** Tolera `skills` ausente o mal formado: lo que no sea un array se trata como
|
|
54
|
+
* vacio en vez de tirar `(raw ?? []).map is not a function`, y cada entrada se
|
|
55
|
+
* valida individualmente. */
|
|
26
56
|
function normalizeSkillRefs(raw) {
|
|
27
|
-
|
|
57
|
+
if (!Array.isArray(raw))
|
|
58
|
+
return [];
|
|
59
|
+
return raw.flatMap((s) => {
|
|
60
|
+
if (typeof s === 'string')
|
|
61
|
+
return [{ name: s, onSignal: false }];
|
|
62
|
+
if (s !== null && typeof s === 'object' && typeof s.name === 'string') {
|
|
63
|
+
return [{ name: s.name, onSignal: s.onSignal === true }];
|
|
64
|
+
}
|
|
65
|
+
return [];
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** Array de strings, o vacio. Cualquier otra forma en el contenido del registry
|
|
69
|
+
* se ignora en vez de propagarse hasta un crash aguas abajo. */
|
|
70
|
+
function stringArray(raw) {
|
|
71
|
+
return Array.isArray(raw) ? raw.filter((v) => typeof v === 'string') : [];
|
|
28
72
|
}
|
|
29
73
|
function discoverBundles(contentDir) {
|
|
30
74
|
const entries = readCatalog(contentDir);
|
|
@@ -33,17 +77,32 @@ function discoverBundles(contentDir) {
|
|
|
33
77
|
const manifestPath = path_1.default.join(contentDir, entry.source, 'bundle.json');
|
|
34
78
|
if (!fs_1.default.existsSync(manifestPath))
|
|
35
79
|
continue;
|
|
36
|
-
|
|
80
|
+
let raw;
|
|
81
|
+
try {
|
|
82
|
+
raw = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf-8'));
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
throw new Error(`${manifestPath} is not valid JSON. Fix the file in its registry, then re-run \`awm update\`.`);
|
|
86
|
+
}
|
|
87
|
+
if (raw === null || typeof raw !== 'object') {
|
|
88
|
+
throw new Error(`${manifestPath} must contain a JSON object.`);
|
|
89
|
+
}
|
|
90
|
+
const m = raw;
|
|
91
|
+
// Un bundle sin nombre usable no es instalable y solo produciria errores
|
|
92
|
+
// opacos mas adelante — se nombra el archivo y se falla aca.
|
|
93
|
+
if (typeof m.name !== 'string' || m.name === '') {
|
|
94
|
+
throw new Error(`${manifestPath}: "name" must be a non-empty string.`);
|
|
95
|
+
}
|
|
37
96
|
bundles.push({
|
|
38
|
-
name:
|
|
39
|
-
description:
|
|
40
|
-
version:
|
|
41
|
-
scope:
|
|
42
|
-
visibility:
|
|
43
|
-
dependsOn:
|
|
44
|
-
skills: normalizeSkillRefs(
|
|
45
|
-
workflows:
|
|
46
|
-
agents:
|
|
97
|
+
name: m.name,
|
|
98
|
+
description: typeof m.description === 'string' ? m.description : '',
|
|
99
|
+
version: typeof m.version === 'string' ? m.version : '0.0.0',
|
|
100
|
+
scope: m.scope ?? 'project',
|
|
101
|
+
visibility: m.visibility ?? 'public',
|
|
102
|
+
dependsOn: stringArray(m.dependsOn),
|
|
103
|
+
skills: normalizeSkillRefs(m.skills),
|
|
104
|
+
workflows: stringArray(m.workflows),
|
|
105
|
+
agents: stringArray(m.agents),
|
|
47
106
|
contentRoot: contentDir,
|
|
48
107
|
});
|
|
49
108
|
}
|
|
@@ -5,7 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.regenerateGlobalContext = regenerateGlobalContext;
|
|
7
7
|
// W3 — Regeneración del contexto global tras `awm update`.
|
|
8
|
-
// Para cada agente con
|
|
8
|
+
// Para cada agente con un mecanismo de inyección aplicable:
|
|
9
9
|
// - 'stale' (sentinel presente, archivo materializado ausente/viejo) → re-materializa.
|
|
10
10
|
// - 'injected' (ya fresco) → no toca nada.
|
|
11
11
|
// - 'absent' (sentinel ausente) → no inyecta (eso es `awm init`).
|
|
@@ -21,10 +21,27 @@ function regenerateGlobalContext(targets = [...providers_1.AGENT_TARGETS], orch
|
|
|
21
21
|
const out = [];
|
|
22
22
|
for (const agent of targets) {
|
|
23
23
|
const inj = (0, providers_1.providerFor)(agent).injection;
|
|
24
|
-
if (!inj
|
|
25
|
-
continue;
|
|
26
|
-
if (!fs_1.default.existsSync(inj.configPath))
|
|
24
|
+
if (!inj)
|
|
27
25
|
continue;
|
|
26
|
+
// Antes esto exigia `type === 'config-instructions'`, o sea que SOLO se
|
|
27
|
+
// regeneraba OpenCode: para codex, cursor y copilot (`managed-agents-md`)
|
|
28
|
+
// `awm update` era un no-op silencioso — ni un aviso — y el unico modo
|
|
29
|
+
// de refrescar su contexto era volver a correr `awm init`. Peor: doctor
|
|
30
|
+
// marcaba ⚠ pero seguia reportando `status: healthy` / exit 0, asi que
|
|
31
|
+
// un AGENTS.md desactualizado era invisible para cualquier gate de CI
|
|
32
|
+
// que mirara el codigo de salida.
|
|
33
|
+
//
|
|
34
|
+
// El destino global de `managed-agents-md` puede ser null (cursor y
|
|
35
|
+
// copilot no tienen archivo de contexto global); ahi no hay nada global
|
|
36
|
+
// que regenerar y se saltea limpio.
|
|
37
|
+
if (inj.type === 'config-instructions') {
|
|
38
|
+
if (!fs_1.default.existsSync(inj.configPath))
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
else if (inj.type === 'managed-agents-md') {
|
|
42
|
+
if (inj.globalPath === null)
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
28
45
|
const op = {
|
|
29
46
|
agent,
|
|
30
47
|
scope: 'global',
|
|
@@ -101,9 +101,25 @@ class CodexAgentsStrategy {
|
|
|
101
101
|
// separate global file, so this is the only writer of the project AGENTS.md.
|
|
102
102
|
const injection = provider.injection;
|
|
103
103
|
const contextCoversThisFile = injection?.type === 'managed-agents-md' && injection.globalPath === null;
|
|
104
|
-
|
|
104
|
+
// La guarda de arriba solo mira a ESTE provider. Pero el bloque es uno
|
|
105
|
+
// solo por archivo, y OTRO provider ya habilitado (cursor/copilot, cuyo
|
|
106
|
+
// contexto ES este archivo) pudo haberlo escrito antes — con el contexto
|
|
107
|
+
// AWM completo, del que PROJECT_GUIDANCE es apenas la cola. Sin este
|
|
108
|
+
// chequeo, un `awm init -a codex` posterior lo reemplazaba por las 6
|
|
109
|
+
// lineas de PROJECT_GUIDANCE: 140 -> 6, y para Copilot ese archivo es su
|
|
110
|
+
// UNICO canal de entrega, asi que se quedaba sin nada. Y al reves otra
|
|
111
|
+
// vez en el siguiente init. Se compara contra el bloque real en disco en
|
|
112
|
+
// vez de asumir quien escribio: si ya cubre PROJECT_GUIDANCE, lo que hay
|
|
113
|
+
// es un superconjunto y pisarlo seria perder informacion.
|
|
114
|
+
const agentsMdPath = path_1.default.join(projectRoot, 'AGENTS.md');
|
|
115
|
+
const existingBody = fs_1.default.existsSync(agentsMdPath)
|
|
116
|
+
? (0, managed_block_1.managedBlockBody)(fs_1.default.readFileSync(agentsMdPath, 'utf-8'))
|
|
117
|
+
: null;
|
|
118
|
+
const alreadyCovered = existingBody !== null &&
|
|
119
|
+
(0, managed_block_1.normalizeManagedBody)(existingBody).includes((0, managed_block_1.normalizeManagedBody)(PROJECT_GUIDANCE));
|
|
120
|
+
const result = contextCoversThisFile || alreadyCovered
|
|
105
121
|
? 'unchanged'
|
|
106
|
-
: injectFile(
|
|
122
|
+
: injectFile(agentsMdPath, PROJECT_GUIDANCE);
|
|
107
123
|
let carrierResult = 'unchanged';
|
|
108
124
|
if (agent === 'cursor') {
|
|
109
125
|
// Cursor's Background/Cloud Agent does not reliably read AGENTS.md (open,
|
|
@@ -30,7 +30,15 @@ function machineChecks(m) {
|
|
|
30
30
|
detail: `git ${m.registryCache.gitState ?? 'unknown'}`, remedy: none });
|
|
31
31
|
}
|
|
32
32
|
// machine.hook
|
|
33
|
-
|
|
33
|
+
// Un provider sin mecanismo de hooks no puede "tener el hook instalado":
|
|
34
|
+
// no se emite fila alguna, igual que ya hacen el paso de init
|
|
35
|
+
// (init/steps.ts, "no hook mechanism for this agent") y el reporte por
|
|
36
|
+
// provider (provider-checks.ts, que descarta la fila). Emitir `missing`
|
|
37
|
+
// aca era lo que ponia `awm init` en exit 1 para 4 de los 6 providers.
|
|
38
|
+
if (!m.hook.applicable) {
|
|
39
|
+
// sin fila
|
|
40
|
+
}
|
|
41
|
+
else if (m.hook.present && !m.hook.degraded) {
|
|
34
42
|
out.push({ id: 'machine.hook', level: 'machine', label: 'hook SessionStart', status: 'ok', remedy: none });
|
|
35
43
|
}
|
|
36
44
|
else if (m.hook.present) {
|
|
@@ -106,6 +114,17 @@ function projectChecks(p) {
|
|
|
106
114
|
out.push({ id: 'project.activation', level: 'project', label: 'active bundles', status: 'missing',
|
|
107
115
|
detail: `${missingLinks.length} missing, ${p.activeBundles.broken.length} broken`, remedy: cmd('awm sync') });
|
|
108
116
|
}
|
|
117
|
+
// project.orphans — links colgantes que ya no pertenecen a ninguna extension.
|
|
118
|
+
// Fila propia y no parte de `project.activation`: ese check mide "falta lo que el
|
|
119
|
+
// profile pide", y esto es lo contrario — sobra lo que el profile ya no pide. Sin
|
|
120
|
+
// separarlos, un proyecto con todo lo esperado instalado reportaba `ok` mientras
|
|
121
|
+
// arrastraba skills muertas que el agente sigue intentando leer.
|
|
122
|
+
const orphans = p.orphanLinks.repairable.length + p.orphanLinks.dead.length;
|
|
123
|
+
if (orphans > 0) {
|
|
124
|
+
out.push({ id: 'project.orphans', level: 'project', label: 'orphaned skill links', status: 'warn',
|
|
125
|
+
detail: `${p.orphanLinks.repairable.length} repairable, ${p.orphanLinks.dead.length} dead`,
|
|
126
|
+
remedy: cmd('awm sync') });
|
|
127
|
+
}
|
|
109
128
|
// project.sensors
|
|
110
129
|
out.push(p.sensors.present
|
|
111
130
|
? { id: 'project.sensors', level: 'project', label: 'sensors', status: 'ok', remedy: none }
|
|
@@ -137,7 +156,12 @@ function projectChecks(p) {
|
|
|
137
156
|
// actionable-but-expected transient states (rendered with ◷/⚠, not ✖) and
|
|
138
157
|
// do not by themselves degrade `overall`, mirroring how a 'warn' CheckResult
|
|
139
158
|
// doesn't degrade the machine+project report either.
|
|
140
|
-
|
|
159
|
+
// `stale` cuenta como degradado: un contexto desactualizado es exactamente la
|
|
160
|
+
// clase de problema que un gate de CI tiene que ver. Antes doctor imprimia ⚠
|
|
161
|
+
// junto a `status: healthy` y exit 0, asi que un AGENTS.md viejo — o sobrescrito
|
|
162
|
+
// por otro provider — era invisible para cualquier script que mirara el codigo
|
|
163
|
+
// de salida. Un aviso que no cambia el veredicto no es un aviso, es ruido.
|
|
164
|
+
const DEGRADING_PROVIDER_STATES = ['missing', 'unsupported', 'broken', 'absent', 'conflict', 'stale'];
|
|
141
165
|
function computeProviderOverall(providers) {
|
|
142
166
|
const degraded = providers.some((p) => p.checks.some((c) => DEGRADING_PROVIDER_STATES.includes(c.state)));
|
|
143
167
|
return degraded ? 'degraded' : 'healthy';
|
|
@@ -9,6 +9,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
10
|
const child_process_1 = require("child_process");
|
|
11
11
|
const providers_1 = require("../../providers");
|
|
12
|
+
const registry_1 = require("../renderers/registry");
|
|
12
13
|
const registries_1 = require("../registries");
|
|
13
14
|
const orchestrator_1 = require("../context/orchestrator");
|
|
14
15
|
const status_1 = require("../../commands/hooks/status");
|
|
@@ -31,15 +32,29 @@ function linkState(dir, skill) {
|
|
|
31
32
|
return fs_1.default.existsSync(p) ? 'present' : 'broken';
|
|
32
33
|
return 'present'; // un dir/archivo real también cuenta como presente
|
|
33
34
|
}
|
|
34
|
-
|
|
35
|
+
/** Clasifica artefactos por su presencia en disco.
|
|
36
|
+
*
|
|
37
|
+
* `renderer` NO es opcional a proposito. El escritor (`physicalTarget`) estampa
|
|
38
|
+
* la extension del renderer sobre el nombre — `using-awm.mdc`,
|
|
39
|
+
* `using-awm.instructions.md` — y este lector buscaba el nombre pelado
|
|
40
|
+
* (`using-awm`), asi que para Cursor y Copilot NADA figuraba nunca como
|
|
41
|
+
* instalado: `awm init` reinstalaba el baseline completo en cada corrida (2
|
|
42
|
+
* directorios de backup sin podar por corrida) y `awm doctor` mostraba un rojo
|
|
43
|
+
* permanente cuyo remedio no podia satisfacerlo. Exigir el renderer obliga a
|
|
44
|
+
* cada caller a declarar de que artefacto habla, y `renderedFilename` es la
|
|
45
|
+
* misma funcion que usa el escritor — no pueden volver a discrepar.
|
|
46
|
+
*
|
|
47
|
+
* Los nombres devueltos son los LOGICOS (los de entrada), no los fisicos: los
|
|
48
|
+
* consumidores comparan contra listas de nombres de bundle. */
|
|
49
|
+
function classifyLinks(names, dir, renderer) {
|
|
35
50
|
const linked = [];
|
|
36
51
|
const broken = [];
|
|
37
|
-
for (const
|
|
38
|
-
const st = linkState(dir,
|
|
52
|
+
for (const name of names) {
|
|
53
|
+
const st = linkState(dir, (0, registry_1.renderedFilename)(name, renderer));
|
|
39
54
|
if (st === 'present')
|
|
40
|
-
linked.push(
|
|
55
|
+
linked.push(name);
|
|
41
56
|
else if (st === 'broken')
|
|
42
|
-
broken.push(
|
|
57
|
+
broken.push(name);
|
|
43
58
|
}
|
|
44
59
|
return { linked, broken };
|
|
45
60
|
}
|
|
@@ -91,7 +106,7 @@ function gatherContextInjection() {
|
|
|
91
106
|
}
|
|
92
107
|
return out;
|
|
93
108
|
}
|
|
94
|
-
function gatherMachine(bundles, agent = 'claude-code') {
|
|
109
|
+
function gatherMachine(bundles, agent = 'claude-code', projectRoot) {
|
|
95
110
|
// registryCache: estado del primer registry configurado (baseline en una máquina sembrada)
|
|
96
111
|
const regs = (0, registries_1.listRegistries)();
|
|
97
112
|
const first = regs[0];
|
|
@@ -105,21 +120,33 @@ function gatherMachine(bundles, agent = 'claude-code') {
|
|
|
105
120
|
// caught while building this task's real end-to-end init test).
|
|
106
121
|
let hookPresent = false;
|
|
107
122
|
let hookDegraded = false;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
123
|
+
// La aplicabilidad se computa DONDE el provider esta en alcance, no se
|
|
124
|
+
// infiere de que `computeHookStatus` haya tirado (un throw puede ser "no
|
|
125
|
+
// soporta hooks" o un error real, y confundirlos fue el bug).
|
|
126
|
+
const hookApplicable = (0, providers_1.providerFor)(agent).hooks !== undefined;
|
|
127
|
+
if (hookApplicable) {
|
|
128
|
+
try {
|
|
129
|
+
const hs = (0, status_1.computeHookStatus)(agent);
|
|
130
|
+
hookPresent = hs.checks.settingsEntry.ok;
|
|
131
|
+
hookDegraded = hs.overall === 'DEGRADED';
|
|
132
|
+
}
|
|
133
|
+
catch { /* soporta hooks pero no se pudo leer el estado → ausente */ }
|
|
112
134
|
}
|
|
113
|
-
catch { /* sin soporte de hooks → ausente */ }
|
|
114
135
|
// devCore (bundle baseline) — skillsDir is null only for providers with no global skill
|
|
115
136
|
// discovery mechanism (today: Copilot); for those, there is no global-scope devCore
|
|
116
137
|
// concept to satisfy at all, so it's treated as trivially satisfied (see below).
|
|
117
138
|
const skillsDir = (0, providers_1.providerFor)(agent).skill.global;
|
|
118
|
-
|
|
139
|
+
// TODOS los baseline, igual que `stepDevCore` los instala y que
|
|
140
|
+
// `reconciliation.ts` los reconcilia. Con `find`, el hecho que decide si el
|
|
141
|
+
// baseline esta satisfecho miraba solo el primero, asi que un segundo bundle
|
|
142
|
+
// baseline sin instalar reportaba `healthy` — y el step, que lee este mismo
|
|
143
|
+
// hecho, nunca lo instalaba.
|
|
144
|
+
const baselines = bundles.filter((b) => b.scope === 'baseline');
|
|
145
|
+
const baseline = baselines[0];
|
|
119
146
|
let devCorePresent = false;
|
|
120
147
|
let brokenLinks = [];
|
|
121
148
|
if (baseline) {
|
|
122
|
-
const skillNames = (0, bundles_1.resolveBundleSkills)(
|
|
149
|
+
const skillNames = [...new Set(baselines.flatMap((b) => (0, bundles_1.resolveBundleSkills)(b.name, bundles)))];
|
|
123
150
|
if (skillsDir === null) {
|
|
124
151
|
// No global skill directory for this agent (today: Copilot) — there is no
|
|
125
152
|
// global-scope devCore/baseline-bundle concept to satisfy for it at all, so
|
|
@@ -131,10 +158,28 @@ function gatherMachine(bundles, agent = 'claude-code') {
|
|
|
131
158
|
// (init/steps.ts) would fall through on every run and call installBundle at
|
|
132
159
|
// global scope, which throws (skill.global === null), rolling back the
|
|
133
160
|
// ENTIRE `awm init -a copilot` transaction, 100% of the time.
|
|
134
|
-
|
|
161
|
+
// Sin directorio global de skills (hoy: Copilot) el baseline se
|
|
162
|
+
// instala a nivel PROYECTO — `stepDevCore` lo hace con
|
|
163
|
+
// scopeOverride 'local'. Antes esto se declaraba "trivialmente
|
|
164
|
+
// satisfecho" incondicionalmente: eso evitaba el crash-loop del
|
|
165
|
+
// install global, pero significaba que Copilot terminaba con CERO
|
|
166
|
+
// skills mientras init imprimia `✔ dev-core` y doctor decia
|
|
167
|
+
// `healthy`. Ahora se mira donde el artefacto REALMENTE va.
|
|
168
|
+
if (projectRoot) {
|
|
169
|
+
const localDir = path_1.default.join(projectRoot, (0, providers_1.providerFor)(agent).skill.local);
|
|
170
|
+
const { linked, broken } = classifyLinks(skillNames, localDir, (0, providers_1.providerFor)(agent).skill.renderer);
|
|
171
|
+
const absent = skillNames.filter((s) => !linked.includes(s) && !broken.includes(s));
|
|
172
|
+
devCorePresent = skillNames.length > 0 && (linked.length + broken.length) > 0;
|
|
173
|
+
brokenLinks = [...broken, ...absent];
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
// Fuera de un proyecto no hay destino local: no hay nada que
|
|
177
|
+
// reclamar como faltante.
|
|
178
|
+
devCorePresent = true;
|
|
179
|
+
}
|
|
135
180
|
}
|
|
136
181
|
else {
|
|
137
|
-
const { linked, broken } = classifyLinks(skillNames, skillsDir);
|
|
182
|
+
const { linked, broken } = classifyLinks(skillNames, skillsDir, (0, providers_1.providerFor)(agent).skill.renderer);
|
|
138
183
|
const absent = skillNames.filter((s) => !linked.includes(s) && !broken.includes(s));
|
|
139
184
|
devCorePresent = skillNames.length > 0 && (linked.length + broken.length) > 0;
|
|
140
185
|
brokenLinks = [...broken, ...absent];
|
|
@@ -151,12 +196,24 @@ function gatherMachine(bundles, agent = 'claude-code') {
|
|
|
151
196
|
// (tests/integration/codex-provider-isolated.test.ts).
|
|
152
197
|
const agentConfig = (0, providers_1.providerFor)(agent).agent;
|
|
153
198
|
if (agentConfig && agentConfig.global !== null) {
|
|
154
|
-
const agentNames = (0, bundles_1.resolveBundleAgents)(
|
|
199
|
+
const agentNames = [...new Set(baselines.flatMap((b) => (0, bundles_1.resolveBundleAgents)(b.name, bundles)))];
|
|
155
200
|
if (agentNames.length > 0) {
|
|
156
|
-
|
|
157
|
-
|
|
201
|
+
// El installName real de un artefacto `agent` es `<n>.md`
|
|
202
|
+
// (bundle-install.ts), y el renderer decide la extension final:
|
|
203
|
+
// `.toml` para codex, `.md` tal cual para los renderers `link`.
|
|
204
|
+
// El ternario anterior devolvia `n` pelado para `link`, asi que
|
|
205
|
+
// `machine.devCore` quedaba permanentemente roto para
|
|
206
|
+
// claude-code — el provider por DEFECTO — contra el registry real.
|
|
207
|
+
const filenames = agentNames.map((n) => `${n}.md`);
|
|
208
|
+
const { linked: agentLinked, broken: agentBroken } = classifyLinks(filenames, agentConfig.global, agentConfig.renderer);
|
|
158
209
|
const agentAbsent = filenames.filter((f) => !agentLinked.includes(f) && !agentBroken.includes(f));
|
|
159
|
-
brokenLinks
|
|
210
|
+
// `brokenLinks` es de diagnostico: se le muestra al usuario para
|
|
211
|
+
// que vaya a buscar ese archivo. Por eso se reportan los nombres
|
|
212
|
+
// FISICOS (`development-process.toml`), aunque la clasificacion
|
|
213
|
+
// trabaje con los logicos — que es lo que necesitan las
|
|
214
|
+
// comparaciones por cantidad de arriba.
|
|
215
|
+
const physical = (n) => (0, registry_1.renderedFilename)(n, agentConfig.renderer);
|
|
216
|
+
brokenLinks = [...brokenLinks, ...agentBroken.map(physical), ...agentAbsent.map(physical)];
|
|
160
217
|
}
|
|
161
218
|
}
|
|
162
219
|
}
|
|
@@ -183,17 +240,17 @@ function gatherMachine(bundles, agent = 'claude-code') {
|
|
|
183
240
|
const skillNames = (0, bundles_1.resolveBundleSkills)(name, bundles);
|
|
184
241
|
if (skillNames.length === 0)
|
|
185
242
|
return false;
|
|
186
|
-
const { linked } = classifyLinks(skillNames, skillsDir);
|
|
243
|
+
const { linked } = classifyLinks(skillNames, skillsDir, (0, providers_1.providerFor)(agent).skill.renderer);
|
|
187
244
|
return linked.length === skillNames.length;
|
|
188
245
|
});
|
|
189
246
|
return {
|
|
190
247
|
registryCache: { present: cachePresent, gitState },
|
|
191
|
-
hook: { present: hookPresent, degraded: hookDegraded },
|
|
248
|
+
hook: { present: hookPresent, degraded: hookDegraded, applicable: hookApplicable },
|
|
192
249
|
devCore: { present: devCorePresent, brokenLinks },
|
|
193
250
|
ambient: { wanted, installed },
|
|
194
251
|
contextInjection: gatherContextInjection(),
|
|
195
252
|
globalSkills: skillsDir !== null
|
|
196
|
-
? (0, skill_integrity_1.
|
|
253
|
+
? (0, skill_integrity_1.classifySkillLinks)(skillsDir, (0, registries_1.contentRoots)())
|
|
197
254
|
: { valid: [], repairable: [], dead: [] },
|
|
198
255
|
};
|
|
199
256
|
}
|
|
@@ -207,7 +264,15 @@ function gatherProject(root, bundles, agent = 'claude-code') {
|
|
|
207
264
|
if (!expected.includes(s))
|
|
208
265
|
expected.push(s);
|
|
209
266
|
}
|
|
210
|
-
const { linked, broken } = classifyLinks(expected, localSkillsDir);
|
|
267
|
+
const { linked, broken } = classifyLinks(expected, localSkillsDir, (0, providers_1.providerFor)(agent).skill.renderer);
|
|
268
|
+
// Huerfanos: symlinks colgantes que el profile ya no reclama. Se clasifican sobre
|
|
269
|
+
// el dir REAL, no sobre la lista esperada — que es justamente lo que `linked`/
|
|
270
|
+
// `broken` de arriba no pueden ver. `awm sync` los cura o los poda.
|
|
271
|
+
const orphanScan = (0, skill_integrity_1.classifySkillLinks)(localSkillsDir, (0, registries_1.contentRoots)());
|
|
272
|
+
const orphanLinks = {
|
|
273
|
+
repairable: orphanScan.repairable.filter((n) => !expected.includes(n)),
|
|
274
|
+
dead: orphanScan.dead.filter((n) => !expected.includes(n)),
|
|
275
|
+
};
|
|
211
276
|
let context = { present: false };
|
|
212
277
|
if (fs_1.default.existsSync(path_1.default.join(root, 'CLAUDE.md')))
|
|
213
278
|
context = { present: true, file: 'CLAUDE.md' };
|
|
@@ -217,6 +282,7 @@ function gatherProject(root, bundles, agent = 'claude-code') {
|
|
|
217
282
|
root,
|
|
218
283
|
profile: { present: profilePresent, extensions: profile.extensions },
|
|
219
284
|
activeBundles: { expected, linked, broken },
|
|
285
|
+
orphanLinks,
|
|
220
286
|
sensors: { present: fs_1.default.existsSync(path_1.default.join(root, '.awm', 'sensors.json')) },
|
|
221
287
|
constitution: { present: fs_1.default.existsSync(path_1.default.join(root, 'CONSTITUTION.md')) },
|
|
222
288
|
context,
|
|
@@ -228,9 +294,9 @@ function gatherContext(opts = {}) {
|
|
|
228
294
|
const agent = opts.agent ?? 'claude-code';
|
|
229
295
|
const root = (0, profile_1.findProjectRoot)(cwd);
|
|
230
296
|
const agents = opts.agents ?? [agent];
|
|
231
|
-
const scanSkills = opts.scanSkills ?? ((dir) => (0, skill_integrity_1.
|
|
297
|
+
const scanSkills = opts.scanSkills ?? ((dir) => (0, skill_integrity_1.classifySkillLinks)(dir, (0, registries_1.contentRoots)()));
|
|
232
298
|
return {
|
|
233
|
-
machine: gatherMachine(bundles, agent),
|
|
299
|
+
machine: gatherMachine(bundles, agent, root ?? undefined),
|
|
234
300
|
project: root ? gatherProject(root, bundles, agent) : null,
|
|
235
301
|
providers: (0, provider_checks_1.gatherProviderChecks)(agents, scanSkills, root ?? undefined),
|
|
236
302
|
};
|