agentic-workflow-manager 3.13.4 → 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 +15 -23
- package/dist/src/core/export/transform.js +30 -25
- package/dist/src/core/frontmatter.js +223 -0
- 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/canonical-agent.js +33 -2
- package/dist/src/core/renderers/registry.js +37 -0
- package/dist/src/core/renderers/skill-source.js +11 -19
- 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/ui/text.js +10 -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/discovery.test.js +10 -1
- package/dist/tests/core/export/transform.test.js +110 -21
- package/dist/tests/core/frontmatter-description-vs-yaml.test.js +118 -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/renderers/canonical-agent.test.js +36 -0
- package/dist/tests/core/renderers/cursor-mdc.test.js +37 -7
- package/dist/tests/core/renderers/skill-source-block-scalar.test.js +117 -0
- 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/ui/text.test.js +9 -0
- package/dist/tests/utils/registry-view-overrides.test.js +2 -2
- package/package.json +6 -1
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
|
}
|
|
@@ -5,6 +5,7 @@ const child_process_1 = require("child_process");
|
|
|
5
5
|
const init_1 = require("./init");
|
|
6
6
|
const supervisor_1 = require("./supervisor");
|
|
7
7
|
const process_1 = require("../../core/journal/process");
|
|
8
|
+
const adapter_1 = require("../../core/journal/adapter");
|
|
8
9
|
function currentBranch(cwd) {
|
|
9
10
|
// stdio explicito (ver EXEC_STDIO en journal/process.ts): evita el relay
|
|
10
11
|
// default de execFileSync del stderr de git hacia el stderr del llamante,
|
|
@@ -25,7 +26,7 @@ function registerWatchCommand(program) {
|
|
|
25
26
|
.command('watch')
|
|
26
27
|
.description('supervisor durable: ejecuta jobs, releva controladores caidos, nunca mata trabajo vivo')
|
|
27
28
|
.option('--init', 'bootstrap: crea el journal de la rama actual, detecta verificadores y sale')
|
|
28
|
-
.option('--provider <p>', '
|
|
29
|
+
.option('--provider <p>', adapter_1.WATCH_PROVIDERS.join(' | '), 'codex')
|
|
29
30
|
.option('--heartbeat-timeout <min>', 'minutos de silencio de heartbeat', '5')
|
|
30
31
|
.option('--activity-window <min>', 'minutos extra sin actividad de proceso', '10')
|
|
31
32
|
.action(async (opts) => {
|
|
@@ -36,6 +37,15 @@ function registerWatchCommand(program) {
|
|
|
36
37
|
process.stdout.write(`journal inicializado para ${branch}; verificadores requeridos: ${JSON.stringify(out.requiredVerifiers)}\n`);
|
|
37
38
|
return;
|
|
38
39
|
}
|
|
40
|
+
// Validado ACA, antes de tocar nada: `adapterFor` ya rechazaba lo
|
|
41
|
+
// desconocido, pero recien en el primer tick — con el journal escrito y el
|
|
42
|
+
// lock tomado, y el error saliendo del supervisor en vez de del flag que lo
|
|
43
|
+
// causo. Un typo en `--provider` tiene que costar un mensaje, no un ciclo.
|
|
44
|
+
if (!(0, adapter_1.isWatchProvider)(opts.provider)) {
|
|
45
|
+
process.stderr.write(`--provider invalido: ${String(opts.provider)} (validos: ${adapter_1.WATCH_PROVIDERS.join(', ')})\n`);
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
39
49
|
const cfg = {
|
|
40
50
|
...supervisor_1.DEFAULT_SUPERVISOR_CONFIG,
|
|
41
51
|
provider: opts.provider,
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/core/artifact-name.ts
|
|
3
|
+
//
|
|
4
|
+
// Modulo HOJA (sin imports de otros modulos del proyecto): validacion de los
|
|
5
|
+
// nombres de artefacto que llegan desde CONTENIDO DE REGISTRY.
|
|
6
|
+
//
|
|
7
|
+
// Por que existe. Los nombres de `bundle.json` (`skills[]`, `workflows[]`,
|
|
8
|
+
// `agents[]`) se usaban verbatim para construir rutas de instalacion:
|
|
9
|
+
//
|
|
10
|
+
// path.join('~/.claude/skills', '../../.ssh/authorized_keys')
|
|
11
|
+
// => '~/.ssh/authorized_keys'
|
|
12
|
+
//
|
|
13
|
+
// y como `replaceArtifact` hace `fs.rmSync(targetPath, {recursive:true})` antes
|
|
14
|
+
// de enlazar, un nombre como `../../.ssh` BORRABA recursivamente el ~/.ssh real
|
|
15
|
+
// del usuario. Con `../../.config/autostart/x.desktop` se consigue ejecucion al
|
|
16
|
+
// siguiente login. Confirmado end-to-end contra el binario real.
|
|
17
|
+
//
|
|
18
|
+
// El registry es contenido de terceros: un registry de equipo, uno interno, o
|
|
19
|
+
// uno que alguien agrego con `awm registry add`. Todos los demas lectores de
|
|
20
|
+
// contenido de registry de este repo (readRegistriesConfig, readRegistryManifest,
|
|
21
|
+
// readProfile) ya rechazaban `..` y separadores — este camino era el unico sin
|
|
22
|
+
// la guarda.
|
|
23
|
+
//
|
|
24
|
+
// Esta validacion es la PRIMERA de dos capas. La segunda es la asercion de
|
|
25
|
+
// contencion en `physicalTarget` (install-planner.ts), que verifica que la ruta
|
|
26
|
+
// resuelta caiga realmente dentro del directorio destino. Se mantienen las dos
|
|
27
|
+
// a proposito: esta da un mensaje accionable que nombra el artefacto culpable;
|
|
28
|
+
// aquella es el ancla estructural que atrapa cualquier camino futuro que
|
|
29
|
+
// construya rutas sin pasar por aca.
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
exports.isSafeArtifactName = isSafeArtifactName;
|
|
32
|
+
exports.assertSafeArtifactName = assertSafeArtifactName;
|
|
33
|
+
/** Nombres reservados de Windows: no pueden ser un archivo ni un directorio.
|
|
34
|
+
* Se rechazan en TODA plataforma a proposito — el registry es contenido
|
|
35
|
+
* compartido, y un nombre que solo rompe en las maquinas Windows del equipo es
|
|
36
|
+
* peor que uno rechazado de forma consistente en todas. */
|
|
37
|
+
const WINDOWS_RESERVED = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\..*)?$/i;
|
|
38
|
+
/** ¿Es seguro usar este nombre como UN componente de ruta dentro del directorio
|
|
39
|
+
* de instalacion? Sin `..`, sin separadores, sin rutas absolutas, sin bytes de
|
|
40
|
+
* control, sin nombres reservados. */
|
|
41
|
+
function isSafeArtifactName(name) {
|
|
42
|
+
if (typeof name !== 'string')
|
|
43
|
+
return false;
|
|
44
|
+
const value = name.trim();
|
|
45
|
+
if (value === '' || value !== name)
|
|
46
|
+
return false; // vacio, o con espacios al borde
|
|
47
|
+
// Windows recorta en silencio el punto/espacio final, asi que `evil.` y
|
|
48
|
+
// `evil` terminan en el MISMO archivo — un nombre que apunta a otro destino
|
|
49
|
+
// del que aparenta.
|
|
50
|
+
if (/[. ]$/.test(value))
|
|
51
|
+
return false;
|
|
52
|
+
// Bytes de control y NUL: truncan la ruta a nivel syscall en algunos SO.
|
|
53
|
+
// eslint-disable-next-line no-control-regex
|
|
54
|
+
if (/[\u0000-\u001f\u007f]/.test(value))
|
|
55
|
+
return false;
|
|
56
|
+
// Cualquier separador (de ambas plataformas) convierte esto en una ruta,
|
|
57
|
+
// no en un nombre. Idem `..`/`.` como componente completo.
|
|
58
|
+
if (value.includes('/') || value.includes('\\'))
|
|
59
|
+
return false;
|
|
60
|
+
if (value === '.' || value === '..')
|
|
61
|
+
return false;
|
|
62
|
+
// Absolutos de Windows (`C:\...`, `C:algo`) y de UNC ya quedan cubiertos por
|
|
63
|
+
// el chequeo de separador, pero un `C:` pelado no — y sigue siendo una
|
|
64
|
+
// referencia de unidad, no un nombre.
|
|
65
|
+
if (/^[a-zA-Z]:/.test(value))
|
|
66
|
+
return false;
|
|
67
|
+
if (WINDOWS_RESERVED.test(value))
|
|
68
|
+
return false;
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
/** Forma asertiva: lanza con un mensaje que nombra el tipo y el valor ofensivo,
|
|
72
|
+
* para que el operador pueda ubicarlo en el `bundle.json` del registry. */
|
|
73
|
+
function assertSafeArtifactName(name, type) {
|
|
74
|
+
if (!isSafeArtifactName(name)) {
|
|
75
|
+
throw new Error(`unsafe ${type} name from registry content: ${JSON.stringify(name)}. ` +
|
|
76
|
+
`Artifact names must be a single path component — no "..", no path separators, ` +
|
|
77
|
+
`no absolute paths, no control characters, and not a Windows reserved name.`);
|
|
78
|
+
}
|
|
79
|
+
return name;
|
|
80
|
+
}
|