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.
Files changed (90) hide show
  1. package/dist/src/commands/add.js +5 -0
  2. package/dist/src/commands/doctor.js +1 -0
  3. package/dist/src/commands/hooks/claude.js +15 -2
  4. package/dist/src/commands/hooks/shared.js +6 -1
  5. package/dist/src/commands/init.js +1 -0
  6. package/dist/src/commands/pin.js +24 -4
  7. package/dist/src/commands/preflight/checks.js +3 -2
  8. package/dist/src/commands/registry/add.js +1 -1
  9. package/dist/src/commands/sensors/index.js +7 -0
  10. package/dist/src/commands/sensors/init.js +48 -7
  11. package/dist/src/commands/sensors/install.js +20 -11
  12. package/dist/src/commands/sensors/run.js +31 -29
  13. package/dist/src/commands/sync.js +29 -0
  14. package/dist/src/commands/watch/index.js +11 -1
  15. package/dist/src/core/artifact-name.js +80 -0
  16. package/dist/src/core/bundle-install.js +55 -12
  17. package/dist/src/core/bundles.js +72 -13
  18. package/dist/src/core/context/regenerate.js +21 -4
  19. package/dist/src/core/context/strategies/codex-agents.js +18 -2
  20. package/dist/src/core/diagnostics/checks.js +26 -2
  21. package/dist/src/core/diagnostics/context.js +91 -25
  22. package/dist/src/core/diagnostics/provider-checks.js +62 -14
  23. package/dist/src/core/discovery.js +15 -23
  24. package/dist/src/core/export/transform.js +30 -25
  25. package/dist/src/core/frontmatter.js +223 -0
  26. package/dist/src/core/init/mutation-targets.js +11 -0
  27. package/dist/src/core/init/steps.js +70 -19
  28. package/dist/src/core/install-planner.js +62 -17
  29. package/dist/src/core/install-transaction.js +13 -1
  30. package/dist/src/core/journal/adapter.js +10 -1
  31. package/dist/src/core/paths.js +61 -8
  32. package/dist/src/core/profile.js +12 -0
  33. package/dist/src/core/provider-artifacts.js +38 -13
  34. package/dist/src/core/provider-version.js +12 -6
  35. package/dist/src/core/registries.js +40 -19
  36. package/dist/src/core/renderers/canonical-agent.js +33 -2
  37. package/dist/src/core/renderers/registry.js +37 -0
  38. package/dist/src/core/renderers/skill-source.js +11 -19
  39. package/dist/src/core/skill-integrity.js +71 -10
  40. package/dist/src/core/update-check.js +14 -2
  41. package/dist/src/core/versioning.js +17 -2
  42. package/dist/src/index.js +31 -23
  43. package/dist/src/providers/index.js +7 -2
  44. package/dist/src/ui/text.js +10 -2
  45. package/dist/src/utils/config.js +7 -8
  46. package/dist/tests/commands/doctor.test.js +6 -0
  47. package/dist/tests/commands/pin.test.js +29 -2
  48. package/dist/tests/commands/preflight/preflight.test.js +31 -17
  49. package/dist/tests/commands/registry/install-bundles.test.js +6 -6
  50. package/dist/tests/commands/sensors/init-pack-unavailable.test.js +96 -0
  51. package/dist/tests/commands/sensors/install-settings-safety.test.js +87 -0
  52. package/dist/tests/commands/sensors/run-is-read-only.test.js +0 -0
  53. package/dist/tests/commands/sensors/run.test.js +24 -51
  54. package/dist/tests/commands/sensors/status-windows.test.js +39 -22
  55. package/dist/tests/commands/sensors/status.test.js +27 -12
  56. package/dist/tests/core/artifact-name-containment.test.js +86 -0
  57. package/dist/tests/core/context/agents-md-single-slot.test.js +69 -0
  58. package/dist/tests/core/context/regenerate.test.js +39 -8
  59. package/dist/tests/core/diagnostics/checks.test.js +14 -6
  60. package/dist/tests/core/diagnostics/provider-tier.test.js +3 -3
  61. package/dist/tests/core/diagnostics/rendered-artifact-visibility.test.js +69 -0
  62. package/dist/tests/core/discovery.test.js +10 -1
  63. package/dist/tests/core/export/transform.test.js +110 -21
  64. package/dist/tests/core/frontmatter-description-vs-yaml.test.js +118 -0
  65. package/dist/tests/core/init/all-baseline-bundles.test.js +64 -0
  66. package/dist/tests/core/init/context-injection-no-project.test.js +117 -0
  67. package/dist/tests/core/init/orchestrator.test.js +1 -1
  68. package/dist/tests/core/init/steps-registry-sync.test.js +1 -1
  69. package/dist/tests/core/init/steps.test.js +11 -5
  70. package/dist/tests/core/path-resolution-no-shell.test.js +136 -0
  71. package/dist/tests/core/paths.test.js +7 -34
  72. package/dist/tests/core/project-skill-links.test.js +159 -0
  73. package/dist/tests/core/provider-artifacts.test.js +66 -14
  74. package/dist/tests/core/provider-capability-guards.test.js +92 -0
  75. package/dist/tests/core/reconciliation.test.js +15 -15
  76. package/dist/tests/core/registry-manifest.test.js +1 -1
  77. package/dist/tests/core/renderers/canonical-agent.test.js +36 -0
  78. package/dist/tests/core/renderers/cursor-mdc.test.js +37 -7
  79. package/dist/tests/core/renderers/skill-source-block-scalar.test.js +117 -0
  80. package/dist/tests/core/semver-fails-closed.test.js +30 -0
  81. package/dist/tests/core/skill-integrity.test.js +10 -10
  82. package/dist/tests/core/skill-repair-safety.test.js +92 -0
  83. package/dist/tests/core/sync-profile-atomicity.test.js +122 -0
  84. package/dist/tests/integration/copilot-init-isolated.test.js +2 -2
  85. package/dist/tests/providers/index.test.js +1 -1
  86. package/dist/tests/structural/renderer-table-is-single-source.test.js +95 -0
  87. package/dist/tests/structural/symlink-type-is-explicit.test.js +73 -0
  88. package/dist/tests/ui/text.test.js +9 -0
  89. package/dist/tests/utils/registry-view-overrides.test.js +2 -2
  90. package/package.json +6 -1
@@ -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
- fs_1.default.symlinkSync(sourceSkill, skillDest);
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
- fs_1.default.symlinkSync(source, dest);
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;
@@ -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
- return ['base', ...(0, registries_1.readRegistriesConfig)().map((r) => r.name)];
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
- prefs.pins = { ...(prefs.pins ?? {}), [name]: normalized };
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 || !(name in prefs.pins))
59
+ if (!prefs.pins || !(key in prefs.pins))
40
60
  return false;
41
- delete prefs.pins[name];
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. `runSensors` self-heals this at run time via `reconcilePack`,
134
- * but only when a registry is reachable; saying it out loud here costs nothing.
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.REGISTRIES_DIR, { recursive: true });
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 = fs_1.default.readdirSync(packsDir, { withFileTypes: true })
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
- const manifest = buildManifest(detection.pack, existing, opts.registryRoot, cwd);
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
- const packDir = path_1.default.join(opts.registryRoot, 'sensor-packs', detection.pack);
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
- return path_1.default.join((0, paths_1.homeDir)(), '.claude', 'settings.json');
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
- if (!fs_1.default.existsSync(p))
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
- fs_1.default.writeFileSync(settingsPath, JSON.stringify(updated, null, 2), 'utf-8');
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
- fs_1.default.writeFileSync(settingsPath, JSON.stringify(updated, null, 2), 'utf-8');
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.reconcilePack = reconcilePack;
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
- * Upgrade-only, idempotent pack reconciliation. If the manifest sits on the
71
- * `generic` fallback but the tree now has real stack indicators (package.json,
72
- * pyproject.toml…), re-detect and rebuild via initSensors which merges existing
73
- * custom sensors and copies the pack's config files. Never downgrades, never
74
- * touches a real pack. FS/registry failures degrade to a no-op (the honest floor
75
- * in runSensors covers the gap).
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 reconcilePack(manifestDir, manifest, registryRoot) {
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 { manifest, detection }; // truly generic — stay honest
85
- const root = registryRoot ?? (0, registries_1.capabilityRoot)('sensor-packs');
86
- if (!root || !fs_1.default.existsSync(root))
87
- return { manifest, detection }; // can't rebuild without registry
88
- try {
89
- const { manifest: rebuilt } = (0, init_1.initSensors)({ cwd: manifestDir, registryRoot: root, configure: true });
90
- return { manifest: rebuilt, upgradedFrom: 'generic', detection };
91
- }
92
- catch {
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 reconciled = reconcilePack(manifestDir, manifest);
284
- const activeManifest = reconciled.manifest;
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' && reconciled.detection.pack !== 'generic') {
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
- ...(reconciled.upgradedFrom ? { packUpgraded: `${reconciled.upgradedFrom}→${activeManifest.pack}` } : {}),
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>', 'codex | claude-code', 'codex')
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
+ }