agentic-workflow-manager 3.13.5 → 3.13.7
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/scripts/support-matrix.js +138 -0
- 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 +34 -1
- 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/commands/watch/runner.test.js +10 -0
- 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/mutation-targets-canonical.test.js +81 -0
- package/dist/tests/core/init/mutation-targets.test.js +4 -4
- 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/support-matrix-is-current.test.js +100 -0
- package/dist/tests/structural/symlink-type-is-explicit.test.js +73 -0
- package/dist/tests/support/tmp.js +31 -0
- package/dist/tests/utils/registry-view-overrides.test.js +2 -2
- package/package.json +6 -2
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DOC_PATH = exports.END_MARKER = exports.BEGIN_MARKER = void 0;
|
|
7
|
+
exports.homeRelative = homeRelative;
|
|
8
|
+
exports.renderProviderTables = renderProviderTables;
|
|
9
|
+
exports.spliceGenerated = spliceGenerated;
|
|
10
|
+
// Generador de la matriz de soporte que vive en `docs/support-matrix.md`.
|
|
11
|
+
//
|
|
12
|
+
// Existe porque la tabla escrita a mano ya mintio: decia que Antigravity instalaba en
|
|
13
|
+
// `~/.agents/skills` y `.agents/skills` cuando el codigo dice `~/.gemini/antigravity/skills`
|
|
14
|
+
// y `.agent/skills` (singular), y omitia que es el unico provider con `global_workflows`.
|
|
15
|
+
// Una tabla de soporte que no coincide con el codigo es peor que no tenerla: se cita en
|
|
16
|
+
// una presentacion, se planifica encima, y nadie la vuelve a chequear.
|
|
17
|
+
//
|
|
18
|
+
// El bloque generado esta delimitado por marcadores en el .md, y
|
|
19
|
+
// `tests/structural/support-matrix-is-current.test.ts` regenera y compara — asi que un
|
|
20
|
+
// cambio en `providers/index.ts` que no se refleje en el doc pone la CI en rojo. La
|
|
21
|
+
// verificabilidad no depende de que alguien se acuerde.
|
|
22
|
+
const path_1 = __importDefault(require("path"));
|
|
23
|
+
const providers_1 = require("../src/providers");
|
|
24
|
+
const paths_1 = require("../src/core/paths");
|
|
25
|
+
exports.BEGIN_MARKER = '<!-- BEGIN GENERATED: provider-capabilities -->';
|
|
26
|
+
exports.END_MARKER = '<!-- END GENERATED: provider-capabilities -->';
|
|
27
|
+
/** Cualquier separador → `/`. Las rutas que produce `providers()` traen el separador
|
|
28
|
+
* nativo, y el `home` puede traer otro (en win32, un `HOME` de entorno con `/` frente a
|
|
29
|
+
* un `path.join` que devuelve `\`). Normalizar los dos lados ANTES de compararlos es lo
|
|
30
|
+
* unico que hace la comparacion valida. */
|
|
31
|
+
const toPosix = (p) => p.split(path_1.default.sep).join('/').split('\\').join('/');
|
|
32
|
+
/** Rutas absolutas → `~/…`, con separadores POSIX, para que la tabla sea la misma en
|
|
33
|
+
* cualquier maquina y en cualquier sistema operativo que la regenere.
|
|
34
|
+
*
|
|
35
|
+
* La version anterior comparaba con `startsWith` ANTES de normalizar, asi que en Windows
|
|
36
|
+
* —donde `path.join` devuelve `\` y el home podia venir con `/`— el prefijo no
|
|
37
|
+
* coincidia nunca, no abreviaba nada, y la tabla regenerada ahi no era la misma que la
|
|
38
|
+
* comiteada. Es decir: el documento prometia ser independiente de la maquina y no lo era.
|
|
39
|
+
* Lo encontro la CI de Windows, no el desarrollo en Linux. */
|
|
40
|
+
function homeRelative(p, home) {
|
|
41
|
+
const target = toPosix(p);
|
|
42
|
+
const prefix = toPosix(home);
|
|
43
|
+
return target.startsWith(prefix) ? `~${target.slice(prefix.length)}` : target;
|
|
44
|
+
}
|
|
45
|
+
function cell(value, absent, home) {
|
|
46
|
+
if (value === null)
|
|
47
|
+
return absent;
|
|
48
|
+
return `\`${homeRelative(value, home)}\``;
|
|
49
|
+
}
|
|
50
|
+
/** El tier declarado por la forma de la config — la misma derivacion que `providerTier`
|
|
51
|
+
* en `core/diagnostics/provider-checks.ts`, no una segunda opinion. */
|
|
52
|
+
function tier(c) {
|
|
53
|
+
if (c.hooks)
|
|
54
|
+
return 'hooks-native';
|
|
55
|
+
if (c.injection?.type === 'config-instructions')
|
|
56
|
+
return 'config-managed';
|
|
57
|
+
if (c.injection)
|
|
58
|
+
return 'agents-md-managed';
|
|
59
|
+
return 'context-only';
|
|
60
|
+
}
|
|
61
|
+
function injectionCell(c, home) {
|
|
62
|
+
const inj = c.injection;
|
|
63
|
+
if (!inj)
|
|
64
|
+
return '— (ninguna)';
|
|
65
|
+
if (inj.type === 'cc-settings-merge')
|
|
66
|
+
return 'hook `SessionStart`';
|
|
67
|
+
if (inj.type === 'config-instructions')
|
|
68
|
+
return `\`${homeRelative(inj.configPath, home)}\` → campo \`${inj.field}\``;
|
|
69
|
+
return inj.globalPath === null
|
|
70
|
+
? `\`${inj.localFile}\` del proyecto (sin equivalente global)`
|
|
71
|
+
: `\`${inj.localFile}\` + \`${homeRelative(inj.globalPath, home)}\``;
|
|
72
|
+
}
|
|
73
|
+
function renderProviderTables() {
|
|
74
|
+
// `homeDir()`, la MISMA funcion de la que `providers()` deriva sus rutas. Tomarlo
|
|
75
|
+
// por parametro daba dos fuentes para el mismo dato: el generador abreviaba contra
|
|
76
|
+
// el home real y el test contra uno inventado, asi que ninguna ruta empezaba con el
|
|
77
|
+
// prefijo esperado y la tabla salia con rutas absolutas de la maquina que la corrio.
|
|
78
|
+
const home = (0, paths_1.homeDir)();
|
|
79
|
+
const p = (0, providers_1.providers)();
|
|
80
|
+
const row = (a) => p[a];
|
|
81
|
+
const lines = [];
|
|
82
|
+
lines.push('### Dónde aterriza cada artefacto');
|
|
83
|
+
lines.push('');
|
|
84
|
+
lines.push('| Agente | Tier | Skills (global) | Skills (proyecto) | Formato |');
|
|
85
|
+
lines.push('|---|---|---|---|---|');
|
|
86
|
+
for (const a of providers_1.AGENT_TARGETS) {
|
|
87
|
+
const c = row(a);
|
|
88
|
+
lines.push(`| \`${a}\` | ${tier(c)} | ${cell(c.skill.global, '**no soportado**', home)} | \`${c.skill.local}\` | \`${c.skill.renderer}\` |`);
|
|
89
|
+
}
|
|
90
|
+
lines.push('');
|
|
91
|
+
lines.push('### Perfiles de agente, workflows, hooks y contexto');
|
|
92
|
+
lines.push('');
|
|
93
|
+
lines.push('| Agente | Perfiles de agente | Workflows | Hooks | Entrega de contexto | Versión mínima |');
|
|
94
|
+
lines.push('|---|---|---|---|---|---|');
|
|
95
|
+
for (const a of providers_1.AGENT_TARGETS) {
|
|
96
|
+
const c = row(a);
|
|
97
|
+
const agentCell = c.agent === null
|
|
98
|
+
? '— (no aplica)'
|
|
99
|
+
: `${cell(c.agent.global, '**no soportado**', home)} · \`${c.agent.renderer}\``;
|
|
100
|
+
const wfCell = c.workflow === null
|
|
101
|
+
? '— (no aplica)'
|
|
102
|
+
: cell(c.workflow.global, '**no soportado**', home);
|
|
103
|
+
lines.push(`| \`${a}\` | ${agentCell} | ${wfCell} | ${c.hooks ? `\`${c.hooks.type}\`` : '— (no tiene)'} ` +
|
|
104
|
+
`| ${injectionCell(c, home)} | ${c.minimumVersion ?? '— (sin gate)'} |`);
|
|
105
|
+
}
|
|
106
|
+
lines.push('');
|
|
107
|
+
lines.push('> Generado desde `cli/src/providers/index.ts`. **No editar a mano** — `npm run docs:matrix` lo regenera y');
|
|
108
|
+
lines.push('> `tests/structural/support-matrix-is-current.test.ts` falla si el documento y el código se separan.');
|
|
109
|
+
return lines.join('\n');
|
|
110
|
+
}
|
|
111
|
+
/** Reemplaza el bloque entre marcadores. Falla ruidosamente si faltan: un doc sin
|
|
112
|
+
* marcadores no se "arregla" agregando la tabla al final, se arregla avisando.
|
|
113
|
+
*
|
|
114
|
+
* Respeta el fin de linea del documento que recibe. Emitia LF siempre, asi que en un
|
|
115
|
+
* checkout de Windows —donde git entrega el .md con CRLF por defecto— regenerar producia
|
|
116
|
+
* un archivo de finales mezclados, y la comparacion del test fallaba por bytes que no
|
|
117
|
+
* tienen nada que ver con el contenido de la tabla. */
|
|
118
|
+
function spliceGenerated(markdown, generated) {
|
|
119
|
+
const begin = markdown.indexOf(exports.BEGIN_MARKER);
|
|
120
|
+
const end = markdown.indexOf(exports.END_MARKER);
|
|
121
|
+
if (begin === -1 || end === -1 || end < begin) {
|
|
122
|
+
throw new Error(`support-matrix.md no tiene los marcadores ${exports.BEGIN_MARKER} / ${exports.END_MARKER}`);
|
|
123
|
+
}
|
|
124
|
+
const eol = markdown.includes('\r\n') ? '\r\n' : '\n';
|
|
125
|
+
const block = generated.split('\n').join(eol);
|
|
126
|
+
return markdown.slice(0, begin + exports.BEGIN_MARKER.length)
|
|
127
|
+
+ eol + eol + block + eol + eol
|
|
128
|
+
+ markdown.slice(end);
|
|
129
|
+
}
|
|
130
|
+
exports.DOC_PATH = path_1.default.join(__dirname, '..', '..', 'docs', 'support-matrix.md');
|
|
131
|
+
/* istanbul ignore next — entrypoint de CLI, ejercitado por el test via las funciones puras */
|
|
132
|
+
if (require.main === module) {
|
|
133
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
134
|
+
const fs = require('fs');
|
|
135
|
+
const current = fs.readFileSync(exports.DOC_PATH, 'utf-8');
|
|
136
|
+
fs.writeFileSync(exports.DOC_PATH, spliceGenerated(current, renderProviderTables()), 'utf-8');
|
|
137
|
+
process.stdout.write(`support-matrix.md regenerado desde providers/index.ts\n`);
|
|
138
|
+
}
|
package/dist/src/commands/add.js
CHANGED
|
@@ -80,5 +80,10 @@ function runAddBundleCore(options, prefs, bundles, deps = {}) {
|
|
|
80
80
|
? `\n\n${picocolors_1.default.dim('Recorded as a project extension in .awm/profile.json (commit it; symlinks are gitignored).')}`
|
|
81
81
|
: '';
|
|
82
82
|
console.log(`✅ Installed bundle ${picocolors_1.default.cyan(matchedBundle.name)}:\n ${lines}${recordNote}`);
|
|
83
|
+
// Same reason as in sync.ts: without printing it, `awm backup restore` has no
|
|
84
|
+
// name to be given and the transaction may as well not have an id.
|
|
85
|
+
if (result.transactionId) {
|
|
86
|
+
console.log(picocolors_1.default.dim(` transaction ${result.transactionId} — undo with \`awm backup restore ${result.transactionId}\``));
|
|
87
|
+
}
|
|
83
88
|
return { code: 0, selectedAgents, result };
|
|
84
89
|
}
|
|
@@ -64,6 +64,7 @@ const CHECK_LABELS = {
|
|
|
64
64
|
'binary.version': 'binary/version',
|
|
65
65
|
'skills.global': 'global skills',
|
|
66
66
|
'agents.native': 'native agents',
|
|
67
|
+
'workflows.global': 'global workflows',
|
|
67
68
|
'context.global': 'global context',
|
|
68
69
|
'hook.trust': 'hook SessionStart',
|
|
69
70
|
'guidance.project': 'project guidance',
|
|
@@ -46,7 +46,7 @@ function installClaudeHook(options) {
|
|
|
46
46
|
}
|
|
47
47
|
catch { /* not exists */ }
|
|
48
48
|
try {
|
|
49
|
-
fs_1.default.symlinkSync(sourceSkill, skillDest);
|
|
49
|
+
fs_1.default.symlinkSync(sourceSkill, skillDest, 'file'); // ver shared.ts: el tipo no se infiere
|
|
50
50
|
}
|
|
51
51
|
catch {
|
|
52
52
|
// best-effort: copy the single skill file; 'awm update' will not auto-propagate
|
|
@@ -180,7 +180,20 @@ function resyncClaudeHookFiles(config, registryRoot, method) {
|
|
|
180
180
|
fs_1.default.unlinkSync(skillDest);
|
|
181
181
|
}
|
|
182
182
|
catch { /* not exists */ }
|
|
183
|
-
|
|
183
|
+
// Mismo fallback a copia que `installClaudeHook` (arriba). Sin el, en
|
|
184
|
+
// Windows sin Developer Mode este symlink tira EPERM, `resyncInstalledHooks`
|
|
185
|
+
// propaga el throw y `awm update` devuelve 1 — PARA SIEMPRE: el install
|
|
186
|
+
// funcionaba (tenia el fallback) y el update no, en una plataforma que la
|
|
187
|
+
// matriz de soporte declara verificada en CI. Dos escritores del mismo
|
|
188
|
+
// archivo, solo uno endurecido.
|
|
189
|
+
try {
|
|
190
|
+
fs_1.default.symlinkSync(sourceSkill, skillDest, 'file'); // ver shared.ts: el tipo no se infiere
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// best-effort: `awm update` no auto-propagara cambios de esta skill,
|
|
194
|
+
// pero el hook queda funcional en vez de dejar el comando inservible.
|
|
195
|
+
fs_1.default.copyFileSync(sourceSkill, skillDest);
|
|
196
|
+
}
|
|
184
197
|
}
|
|
185
198
|
/** True when the registry has everything needed to resync the Claude hook files. */
|
|
186
199
|
function claudeResyncSourcesExist(registryRoot) {
|
|
@@ -30,7 +30,12 @@ function syncExecutable(source, dest, method) {
|
|
|
30
30
|
fs_1.default.mkdirSync(path_1.default.dirname(dest), { recursive: true });
|
|
31
31
|
if (method === 'symlink') {
|
|
32
32
|
try {
|
|
33
|
-
|
|
33
|
+
// 'file' explicito: el destino es un archivo. Sin el tipo, Node lo INFIERE
|
|
34
|
+
// del target y en Windows puede crear un symlink de DIRECTORIO, que
|
|
35
|
+
// exige SeCreateSymbolicLinkPrivilege. El fallback a copia de abajo lo
|
|
36
|
+
// cubria, pero el tipo correcto en la llamada no depende de que alguien
|
|
37
|
+
// conserve el try/catch al editarla.
|
|
38
|
+
fs_1.default.symlinkSync(source, dest, 'file');
|
|
34
39
|
}
|
|
35
40
|
catch {
|
|
36
41
|
// best-effort: a FILE symlink needs SeCreateSymbolicLinkPrivilege on
|
|
@@ -218,6 +218,7 @@ async function runInit(opts = {}) {
|
|
|
218
218
|
contentDir: (0, registries_1.contentRoots)()[0] ?? '',
|
|
219
219
|
sensorPacksRoot: (0, registries_1.capabilityRoot)('sensor-packs') ?? '',
|
|
220
220
|
confirmExtensions,
|
|
221
|
+
machineOnly: !!opts.machineOnly,
|
|
221
222
|
actions: mergedActions,
|
|
222
223
|
});
|
|
223
224
|
pipelineOutcome = outcome;
|
package/dist/src/commands/pin.js
CHANGED
|
@@ -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.canonicalRegistryName = canonicalRegistryName;
|
|
6
7
|
exports.setPin = setPin;
|
|
7
8
|
exports.removePin = removePin;
|
|
8
9
|
exports.registerPinCommands = registerPinCommands;
|
|
@@ -11,8 +12,24 @@ const config_1 = require("../utils/config");
|
|
|
11
12
|
const registries_1 = require("../core/registries");
|
|
12
13
|
const versioning_1 = require("../core/versioning");
|
|
13
14
|
const VERSION_RE = /^v?\d+\.\d+\.\d+$/;
|
|
15
|
+
/** El registry base se llama `baseline` en disco (core/registries.ts), pero la
|
|
16
|
+
* ayuda del comando siempre documento `awm pin base <v>` — y `base` era
|
|
17
|
+
* aceptado y persistido como una clave de pin que NADA lee. El pin reportaba
|
|
18
|
+
* exito y no hacia absolutamente nada, incluso cuando es la salida documentada
|
|
19
|
+
* de un bloqueo por `minCliVersion`. Se acepta como ALIAS y se normaliza. */
|
|
20
|
+
const BASE_ALIAS = 'base';
|
|
21
|
+
const BASE_REGISTRY = 'baseline';
|
|
22
|
+
/** Nombre real en disco para lo que el usuario escribio. */
|
|
23
|
+
function canonicalRegistryName(name) {
|
|
24
|
+
return name === BASE_ALIAS ? BASE_REGISTRY : name;
|
|
25
|
+
}
|
|
14
26
|
function knownRegistryNames() {
|
|
15
|
-
|
|
27
|
+
// Ambos nombres son validos: el alias historico que documenta la ayuda del
|
|
28
|
+
// comando, y el nombre real en disco — que antes se rechazaba si todavia no
|
|
29
|
+
// habia registries.json, o sea justo cuando un usuario bloqueado intenta
|
|
30
|
+
// salir del paso.
|
|
31
|
+
const names = [BASE_ALIAS, BASE_REGISTRY, ...(0, registries_1.readRegistriesConfig)().map((r) => r.name)];
|
|
32
|
+
return Array.from(new Set(names));
|
|
16
33
|
}
|
|
17
34
|
function assertKnownRegistry(name) {
|
|
18
35
|
const known = knownRegistryNames();
|
|
@@ -28,17 +45,20 @@ function setPin(name, version) {
|
|
|
28
45
|
}
|
|
29
46
|
const normalized = (0, versioning_1.normalizePin)(version);
|
|
30
47
|
const prefs = (0, config_1.getPreferences)();
|
|
31
|
-
|
|
48
|
+
// Se persiste bajo el nombre REAL del registry; si no, el pin queda en una
|
|
49
|
+
// clave que el resolutor de versiones nunca consulta.
|
|
50
|
+
prefs.pins = { ...(prefs.pins ?? {}), [canonicalRegistryName(name)]: normalized };
|
|
32
51
|
(0, config_1.savePreferences)(prefs);
|
|
33
52
|
return normalized;
|
|
34
53
|
}
|
|
35
54
|
/** Borra pins[name]; devuelve true si existía. */
|
|
36
55
|
function removePin(name) {
|
|
37
56
|
assertKnownRegistry(name);
|
|
57
|
+
const key = canonicalRegistryName(name);
|
|
38
58
|
const prefs = (0, config_1.getPreferences)();
|
|
39
|
-
if (!prefs.pins || !(
|
|
59
|
+
if (!prefs.pins || !(key in prefs.pins))
|
|
40
60
|
return false;
|
|
41
|
-
delete prefs.pins[
|
|
61
|
+
delete prefs.pins[key];
|
|
42
62
|
(0, config_1.savePreferences)(prefs);
|
|
43
63
|
return true;
|
|
44
64
|
}
|
|
@@ -130,8 +130,9 @@ function checkTools(cwd) {
|
|
|
130
130
|
/**
|
|
131
131
|
* A manifest pinned to `generic` on a tree that clearly has a stack means the real
|
|
132
132
|
* sensors for that stack are simply absent — the gate runs, reports green, and has
|
|
133
|
-
* checked almost nothing.
|
|
134
|
-
*
|
|
133
|
+
* checked almost nothing. Nothing heals this on its own: `awm sensors run` reports the
|
|
134
|
+
* same drift (`packDrift`) but never rewrites the manifest, so this is the blocking
|
|
135
|
+
* surface, and `awm sensors init` is the only thing that adopts the real pack.
|
|
135
136
|
*/
|
|
136
137
|
function checkPack(cwd, manifest) {
|
|
137
138
|
if (!manifest)
|
|
@@ -38,7 +38,7 @@ async function addRegistry(remote, nameOverride) {
|
|
|
38
38
|
if (fs_1.default.existsSync(dest)) {
|
|
39
39
|
return { ok: false, name, error: `Destination already exists on disk: ${dest}` };
|
|
40
40
|
}
|
|
41
|
-
fs_1.default.mkdirSync(registries_1.
|
|
41
|
+
fs_1.default.mkdirSync((0, registries_1.registriesDir)(), { recursive: true });
|
|
42
42
|
try {
|
|
43
43
|
await (0, simple_git_1.default)().clone(remote, dest);
|
|
44
44
|
}
|
|
@@ -52,6 +52,13 @@ function registerSensorsCommand(program) {
|
|
|
52
52
|
try {
|
|
53
53
|
const result = (0, init_1.initSensors)({ configure: opts.configure, registryRoot, pack: opts.pack });
|
|
54
54
|
prompts_1.log.success(`Detected: ${result.detection.pack} (${result.detection.indicators.join(', ') || 'fallback'})`);
|
|
55
|
+
// Said BEFORE "Wrote .awm/sensors.json": the manifest about to be
|
|
56
|
+
// reported as written is not the one the detection implied.
|
|
57
|
+
if (result.unavailablePack) {
|
|
58
|
+
prompts_1.log.warn(`No '${result.unavailablePack}' sensor-pack in the registry — wrote the `
|
|
59
|
+
+ `'${result.manifest.pack}' pack instead (${Object.keys(result.manifest.sensors).length} sensors). `
|
|
60
|
+
+ 'Run `awm update`, or add a registry that ships it, then re-run `awm sensors init`.');
|
|
61
|
+
}
|
|
55
62
|
prompts_1.log.success('Wrote .awm/sensors.json');
|
|
56
63
|
result.configured.forEach((f) => prompts_1.log.info(` Installed ${f}`));
|
|
57
64
|
}
|
|
@@ -120,6 +120,44 @@ function buildManifest(pack, existing, registryRoot, cwd = process.cwd()) {
|
|
|
120
120
|
}
|
|
121
121
|
return { pack, sensors };
|
|
122
122
|
}
|
|
123
|
+
/** Pack names present as directories under `<registryRoot>/sensor-packs/`, sorted. */
|
|
124
|
+
function availablePacks(registryRoot) {
|
|
125
|
+
const packsDir = path_1.default.join(registryRoot, 'sensor-packs');
|
|
126
|
+
if (!fs_1.default.existsSync(packsDir) || !fs_1.default.statSync(packsDir).isDirectory())
|
|
127
|
+
return [];
|
|
128
|
+
return fs_1.default.readdirSync(packsDir, { withFileTypes: true })
|
|
129
|
+
.filter(e => e.isDirectory())
|
|
130
|
+
.map(e => e.name)
|
|
131
|
+
.sort();
|
|
132
|
+
}
|
|
133
|
+
/** The last-resort pack, used when the detected one is absent from the registry. */
|
|
134
|
+
const FALLBACK_PACK = 'generic';
|
|
135
|
+
/**
|
|
136
|
+
* Resolve the detected pack against what the registry actually ships.
|
|
137
|
+
*
|
|
138
|
+
* `--pack <name>` has always thrown here (`assertPackExists`), but auto-detection had
|
|
139
|
+
* no such check: it wrote `{"pack":"python","sensors":{}}` and said nothing, so the
|
|
140
|
+
* operator learned the gate was empty from an unrelated command days later. The two
|
|
141
|
+
* paths now reach the same conclusion; only the remedy differs, because an explicit
|
|
142
|
+
* `--pack` is a typo to correct while a detection is a fact about the tree that the
|
|
143
|
+
* registry simply cannot serve yet.
|
|
144
|
+
*
|
|
145
|
+
* Falling back to `generic` keeps the gate measuring *something* real rather than
|
|
146
|
+
* nothing. When the registry has no `generic` either, the detected pack is kept: the
|
|
147
|
+
* manifest is then honestly empty, and preflight's `manifest`/`tools` checks both fail
|
|
148
|
+
* on `total === 0` with a remedy pointing at the registry.
|
|
149
|
+
*/
|
|
150
|
+
function resolvePack(detected, registryRoot) {
|
|
151
|
+
if (!registryRoot)
|
|
152
|
+
return { pack: detected }; // nothing to validate against
|
|
153
|
+
const available = availablePacks(registryRoot);
|
|
154
|
+
if (available.length === 0 || available.includes(detected))
|
|
155
|
+
return { pack: detected };
|
|
156
|
+
return {
|
|
157
|
+
pack: available.includes(FALLBACK_PACK) ? FALLBACK_PACK : detected,
|
|
158
|
+
unavailablePack: detected,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
123
161
|
/**
|
|
124
162
|
* Validate that `pack` exists as a directory under `<registryRoot>/sensor-packs/`.
|
|
125
163
|
* Throws (not a swallow-and-return) so `awm sensors init --pack bogus` actually stops
|
|
@@ -131,10 +169,7 @@ function assertPackExists(pack, registryRoot) {
|
|
|
131
169
|
if (!fs_1.default.existsSync(packsDir) || !fs_1.default.statSync(packsDir).isDirectory()) {
|
|
132
170
|
throw new Error('registry has no sensor-packs directory');
|
|
133
171
|
}
|
|
134
|
-
const available =
|
|
135
|
-
.filter(e => e.isDirectory())
|
|
136
|
-
.map(e => e.name)
|
|
137
|
-
.sort();
|
|
172
|
+
const available = availablePacks(registryRoot);
|
|
138
173
|
if (!available.includes(pack)) {
|
|
139
174
|
throw new Error(`pack '${pack}' not found in registry (available: ${available.join(', ')})`);
|
|
140
175
|
}
|
|
@@ -163,14 +198,20 @@ function initSensors(opts = {}) {
|
|
|
163
198
|
}
|
|
164
199
|
catch { /* ignore corrupt manifest */ }
|
|
165
200
|
}
|
|
166
|
-
|
|
201
|
+
// `detection` keeps saying what the tree IS; `pack` is what the registry can serve
|
|
202
|
+
// for it. They diverge only when the registry lacks the detected pack.
|
|
203
|
+
const { pack: resolvedPack, unavailablePack } = resolvePack(detection.pack, opts.registryRoot);
|
|
204
|
+
const manifest = buildManifest(resolvedPack, existing, opts.registryRoot, cwd);
|
|
167
205
|
fs_1.default.mkdirSync(path_1.default.join(cwd, '.awm'), { recursive: true });
|
|
168
206
|
const tmpPath = manifestPath + '.tmp';
|
|
169
207
|
fs_1.default.writeFileSync(tmpPath, JSON.stringify(manifest, null, 2), 'utf-8');
|
|
170
208
|
fs_1.default.renameSync(tmpPath, manifestPath);
|
|
171
209
|
const configured = [];
|
|
172
210
|
if (configure && opts.registryRoot) {
|
|
173
|
-
|
|
211
|
+
// The pack whose defaults the manifest was actually built from — copying the
|
|
212
|
+
// detected pack's config files here would drop files for sensors that are not
|
|
213
|
+
// in the manifest, and miss the ones that are.
|
|
214
|
+
const packDir = path_1.default.join(opts.registryRoot, 'sensor-packs', resolvedPack);
|
|
174
215
|
if (fs_1.default.existsSync(packDir)) {
|
|
175
216
|
for (const file of fs_1.default.readdirSync(packDir).filter(f => f !== 'pack.json')) {
|
|
176
217
|
const dst = path_1.default.join(cwd, file);
|
|
@@ -181,5 +222,5 @@ function initSensors(opts = {}) {
|
|
|
181
222
|
}
|
|
182
223
|
}
|
|
183
224
|
}
|
|
184
|
-
return { manifest, detection, configured };
|
|
225
|
+
return { manifest, detection, configured, ...(unavailablePack ? { unavailablePack } : {}) };
|
|
185
226
|
}
|
|
@@ -8,21 +8,30 @@ exports.uninstallSensorHook = uninstallSensorHook;
|
|
|
8
8
|
const fs_1 = __importDefault(require("fs"));
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
10
|
const paths_1 = require("../../core/paths");
|
|
11
|
+
const shared_1 = require("../hooks/shared");
|
|
12
|
+
const atomic_file_1 = require("../../core/atomic-file");
|
|
13
|
+
const providers_1 = require("../../providers");
|
|
11
14
|
const POST_TOOL_USE_EVENT = 'PostToolUse';
|
|
12
15
|
const POST_TOOL_USE_MATCHER = 'Write|Edit|MultiEdit';
|
|
13
16
|
const AWM_SENSOR_CMD = 'awm sensors run --fast';
|
|
14
17
|
function defaultSettingsPath() {
|
|
15
|
-
|
|
18
|
+
// Se toma de la config del provider, no de una ruta hardcodeada: era la
|
|
19
|
+
// cuarta copia de este path en el codigo.
|
|
20
|
+
return (0, providers_1.getSettingsMergeHookConfig)('claude-code').settingsPath;
|
|
16
21
|
}
|
|
22
|
+
/** Lectura ESTRICTA, compartida con los demas escritores de este archivo.
|
|
23
|
+
*
|
|
24
|
+
* Antes esto era `try { JSON.parse(...) } catch { return {} }` y el `{}` se
|
|
25
|
+
* escribia de vuelta — asi que un JSON malformado (una coma de mas, el error
|
|
26
|
+
* de edicion a mano mas comun) BORRABA el settings.json entero del usuario y
|
|
27
|
+
* la operacion reportaba exito. Verificado destruyendo `model`, `permissions`
|
|
28
|
+
* y el propio hook SessionStart de AWM. El escritor hermano
|
|
29
|
+
* (`installClaudeHook`) ya se negaba correctamente ante el mismo archivo.
|
|
30
|
+
*
|
|
31
|
+
* AWM hace MERGE sobre archivos que son del usuario; nunca los clobberea. Ante
|
|
32
|
+
* un archivo que no se puede parsear, la unica accion segura es negarse. */
|
|
17
33
|
function readSettings(p) {
|
|
18
|
-
|
|
19
|
-
return {};
|
|
20
|
-
try {
|
|
21
|
-
return JSON.parse(fs_1.default.readFileSync(p, 'utf-8'));
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return {};
|
|
25
|
-
}
|
|
34
|
+
return (0, shared_1.readStrictJson)(p);
|
|
26
35
|
}
|
|
27
36
|
function isAwmEntry(e) {
|
|
28
37
|
return e.matcher === POST_TOOL_USE_MATCHER &&
|
|
@@ -56,7 +65,7 @@ function installSensorHook(settingsPath = defaultSettingsPath()) {
|
|
|
56
65
|
},
|
|
57
66
|
};
|
|
58
67
|
fs_1.default.mkdirSync(path_1.default.dirname(settingsPath), { recursive: true });
|
|
59
|
-
|
|
68
|
+
(0, atomic_file_1.writeFileAtomic)(settingsPath, `${JSON.stringify(updated, null, 2)}\n`);
|
|
60
69
|
return { status: 'installed', backupPath };
|
|
61
70
|
}
|
|
62
71
|
function uninstallSensorHook(settingsPath = defaultSettingsPath()) {
|
|
@@ -72,6 +81,6 @@ function uninstallSensorHook(settingsPath = defaultSettingsPath()) {
|
|
|
72
81
|
delete updated.hooks[POST_TOOL_USE_EVENT];
|
|
73
82
|
if (Object.keys(updated.hooks).length === 0)
|
|
74
83
|
delete updated.hooks;
|
|
75
|
-
|
|
84
|
+
(0, atomic_file_1.writeFileAtomic)(settingsPath, `${JSON.stringify(updated, null, 2)}\n`);
|
|
76
85
|
return { status: 'removed' };
|
|
77
86
|
}
|
|
@@ -4,7 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.applyBaseline = applyBaseline;
|
|
7
|
-
exports.
|
|
7
|
+
exports.detectPackDrift = detectPackDrift;
|
|
8
8
|
exports.findManifestDir = findManifestDir;
|
|
9
9
|
exports.resolveConcurrency = resolveConcurrency;
|
|
10
10
|
exports.runSensors = runSensors;
|
|
@@ -22,8 +22,9 @@ const ruff_1 = require("./formatters/ruff");
|
|
|
22
22
|
const shellcheck_1 = require("./formatters/shellcheck");
|
|
23
23
|
const baseline_1 = require("./baseline");
|
|
24
24
|
const changed_1 = require("./changed");
|
|
25
|
+
// Solo `detectStack` (puro, lee el arbol) — NO `initSensors`, que escribe. `run` es un
|
|
26
|
+
// verbo de lectura: no debe tener a mano ninguna funcion capaz de mutar el proyecto.
|
|
25
27
|
const init_1 = require("./init");
|
|
26
|
-
const registries_1 = require("../../core/registries");
|
|
27
28
|
const paths_1 = require("../../core/paths");
|
|
28
29
|
const MANIFEST_FILE = '.awm/sensors.json';
|
|
29
30
|
const DEFAULT_FAST_TIMEOUT = 10_000;
|
|
@@ -67,31 +68,32 @@ function readManifest(cwd) {
|
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
70
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* pyproject.toml
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
71
|
+
* Detect — and only detect — that the manifest's pack no longer describes the tree:
|
|
72
|
+
* it sits on the `generic` fallback while real stack indicators (package.json,
|
|
73
|
+
* pyproject.toml, a root `*.sh`…) are present, so the gate is measuring almost
|
|
74
|
+
* nothing. Pure: reads the tree, writes nothing, consults no registry.
|
|
75
|
+
*
|
|
76
|
+
* It used to *heal* this, by calling `initSensors(..., configure: true)` — which
|
|
77
|
+
* overwrites the committed `.awm/sensors.json` and copies the pack's config files
|
|
78
|
+
* into the project root. That made `awm sensors run`, a read verb, leave a dirty
|
|
79
|
+
* working tree, and swapped in sensors whose tools the project never installed
|
|
80
|
+
* (one root `deploy.sh` pulls in the whole shell pack), turning a green run red.
|
|
81
|
+
* Rewriting the manifest is `awm sensors init`'s job — the verb whose name says so.
|
|
82
|
+
* Here the drift is only named: reported as `packDrift` on the run's own output,
|
|
83
|
+
* and failed on by preflight's `pack` check.
|
|
76
84
|
*/
|
|
77
|
-
function
|
|
78
|
-
if (manifest.pack !== 'generic') {
|
|
79
|
-
const detection = (0, init_1.detectStack)(manifestDir);
|
|
80
|
-
return { manifest, detection };
|
|
81
|
-
}
|
|
85
|
+
function detectPackDrift(manifestDir, manifest) {
|
|
82
86
|
const detection = (0, init_1.detectStack)(manifestDir);
|
|
83
|
-
if (detection.pack === 'generic')
|
|
84
|
-
return {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return { manifest, detection }; // never abort the run on a reconcile failure
|
|
94
|
-
}
|
|
87
|
+
if (manifest.pack !== 'generic' || detection.pack === 'generic')
|
|
88
|
+
return { detection };
|
|
89
|
+
return {
|
|
90
|
+
detection,
|
|
91
|
+
drift: {
|
|
92
|
+
manifest: manifest.pack,
|
|
93
|
+
detected: detection.pack,
|
|
94
|
+
remedy: `run \`awm sensors init\` to adopt the '${detection.pack}' pack for this stack`,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
95
97
|
}
|
|
96
98
|
/**
|
|
97
99
|
* Walk up from `startCwd` looking for the nearest ancestor that contains
|
|
@@ -280,8 +282,8 @@ async function runSensors(opts = {}) {
|
|
|
280
282
|
const manifest = readManifest(manifestDir);
|
|
281
283
|
if (!manifest)
|
|
282
284
|
return { sensors: [], overall: 'not_certified' };
|
|
283
|
-
const
|
|
284
|
-
const activeManifest =
|
|
285
|
+
const drift = detectPackDrift(manifestDir, manifest);
|
|
286
|
+
const activeManifest = manifest; // el manifest COMITEADO es lo que se corre; `run` no lo reescribe
|
|
285
287
|
const cwd = manifestDir; // ejecutar sensores y baseline desde donde vive el manifest
|
|
286
288
|
// Baseline suppresses already-accepted findings so sensors fail only on NEW
|
|
287
289
|
// ones (essential on repos with a large pre-existing baseline). Absent file or
|
|
@@ -382,13 +384,13 @@ async function runSensors(opts = {}) {
|
|
|
382
384
|
: 'pass';
|
|
383
385
|
// Honest floor: a benign-green 'skipped' over a tree that clearly HAS a stack
|
|
384
386
|
// (indicators present) is a false green — the gate ran nothing real. Never green.
|
|
385
|
-
if (overall === 'skipped' &&
|
|
387
|
+
if (overall === 'skipped' && drift.detection.pack !== 'generic') {
|
|
386
388
|
overall = 'not_certified';
|
|
387
389
|
}
|
|
388
390
|
return {
|
|
389
391
|
sensors: results,
|
|
390
392
|
overall,
|
|
391
|
-
...(
|
|
393
|
+
...(drift.drift ? { packDrift: drift.drift } : {}),
|
|
392
394
|
// Always emitted on a --changed run, including when the scope failed to
|
|
393
395
|
// resolve: a green that came back from an unscoped fallback and a green from a
|
|
394
396
|
// genuinely scoped run are different claims, and the caller cannot tell them
|
|
@@ -24,12 +24,27 @@ const bundle_install_1 = require("../core/bundle-install");
|
|
|
24
24
|
const profile_pins_1 = require("../core/profile-pins");
|
|
25
25
|
const config_1 = require("../utils/config");
|
|
26
26
|
const agent_targets_1 = require("../core/agent-targets");
|
|
27
|
+
const skill_integrity_1 = require("../core/skill-integrity");
|
|
28
|
+
const registries_2 = require("../core/registries");
|
|
27
29
|
const defaultDeps = {
|
|
28
30
|
syncRegistries: registries_1.syncRegistries,
|
|
29
31
|
verifyMinCliVersions: registries_1.verifyMinCliVersions,
|
|
30
32
|
verifyProjectPins: profile_pins_1.verifyProjectPins,
|
|
31
33
|
syncProfile: bundle_install_1.syncProfile,
|
|
34
|
+
reconcileProjectSkillLinks: skill_integrity_1.reconcileProjectSkillLinks,
|
|
32
35
|
};
|
|
36
|
+
/** Un link curado o podado es un cambio en el arbol del usuario: se dice siempre.
|
|
37
|
+
* El silencio es lo que dejo este mantenimiento invisible durante todo su ciclo. */
|
|
38
|
+
function reportProjectLinkRepair(results) {
|
|
39
|
+
for (const { agent, result } of results) {
|
|
40
|
+
for (const n of result.relinked)
|
|
41
|
+
console.log(picocolors_1.default.green(` ↻ Re-linked ${n} (${agent}, project scope)`));
|
|
42
|
+
for (const n of result.pruned)
|
|
43
|
+
console.log(picocolors_1.default.yellow(` ✂ Pruned dangling ${n} (${agent}, project scope)`));
|
|
44
|
+
for (const n of result.failed)
|
|
45
|
+
console.warn(picocolors_1.default.yellow(` ⚠ Could not repair ${n} (${agent}, project scope)`));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
33
48
|
/** Core, UI-free `awm sync` logic — see `runSync` for the Commander-facing wrapper. */
|
|
34
49
|
async function runSyncCore(options, deps = {}) {
|
|
35
50
|
// Fires at most once per `awm sync` run, native Windows only — the single
|
|
@@ -93,6 +108,14 @@ async function runSyncCore(options, deps = {}) {
|
|
|
93
108
|
return { code: 1, selectedAgents };
|
|
94
109
|
}
|
|
95
110
|
}
|
|
111
|
+
// Antes de instalar: sanear los links de skills que YA estan en el proyecto. Es lo
|
|
112
|
+
// simetrico de `stepGlobalSkillsRepair` en `awm init`, que solo cubria el dir
|
|
113
|
+
// global — un link colgante de proyecto (registry re-clonado, skill renombrada
|
|
114
|
+
// upstream, bundle sacado del profile) no se curaba ni se podaba nunca. Corre
|
|
115
|
+
// ANTES del early-return de "sin extensiones": un profile vacio es precisamente el
|
|
116
|
+
// caso donde quedan huerfanos de una extension retirada, y era el unico camino que
|
|
117
|
+
// salia sin tocar nada. Solo toca symlinks colgantes (`classifySkillLinks`).
|
|
118
|
+
reportProjectLinkRepair(d.reconcileProjectSkillLinks(projectRoot, selectedAgents, (0, registries_2.contentRoots)()));
|
|
96
119
|
if (profile.extensions.length === 0) {
|
|
97
120
|
console.log(picocolors_1.default.yellow('No extensions in .awm/profile.json — nothing to sync. Use `awm add <bundle>` first.'));
|
|
98
121
|
return { code: 0, selectedAgents };
|
|
@@ -113,5 +136,11 @@ async function runSyncCore(options, deps = {}) {
|
|
|
113
136
|
const lines = result.installed.map((n) => picocolors_1.default.green(n)).join('\n ');
|
|
114
137
|
const installedNote = lines ? `\n ${lines}` : picocolors_1.default.dim(' (all up to date)');
|
|
115
138
|
console.log(`✅ Synced extensions [${result.extensions.join(', ')}]:${installedNote}`);
|
|
139
|
+
// The transaction id is the ONLY handle `awm backup restore` accepts. It was
|
|
140
|
+
// computed, returned in `transactionIds`, and then dropped on the floor by every
|
|
141
|
+
// caller — so the operator who wanted to undo a sync had no name to give it.
|
|
142
|
+
for (const id of result.transactionIds) {
|
|
143
|
+
console.log(picocolors_1.default.dim(` transaction ${id} — undo with \`awm backup restore ${id}\``));
|
|
144
|
+
}
|
|
116
145
|
return { code: 0, selectedAgents, result };
|
|
117
146
|
}
|