agentic-workflow-manager 3.13.5 → 3.13.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/commands/add.js +5 -0
- package/dist/src/commands/doctor.js +1 -0
- package/dist/src/commands/hooks/claude.js +15 -2
- package/dist/src/commands/hooks/shared.js +6 -1
- package/dist/src/commands/init.js +1 -0
- package/dist/src/commands/pin.js +24 -4
- package/dist/src/commands/preflight/checks.js +3 -2
- package/dist/src/commands/registry/add.js +1 -1
- package/dist/src/commands/sensors/index.js +7 -0
- package/dist/src/commands/sensors/init.js +48 -7
- package/dist/src/commands/sensors/install.js +20 -11
- package/dist/src/commands/sensors/run.js +31 -29
- package/dist/src/commands/sync.js +29 -0
- package/dist/src/commands/watch/index.js +11 -1
- package/dist/src/core/artifact-name.js +80 -0
- package/dist/src/core/bundle-install.js +55 -12
- package/dist/src/core/bundles.js +72 -13
- package/dist/src/core/context/regenerate.js +21 -4
- package/dist/src/core/context/strategies/codex-agents.js +18 -2
- package/dist/src/core/diagnostics/checks.js +26 -2
- package/dist/src/core/diagnostics/context.js +91 -25
- package/dist/src/core/diagnostics/provider-checks.js +62 -14
- package/dist/src/core/discovery.js +2 -2
- package/dist/src/core/init/mutation-targets.js +11 -0
- package/dist/src/core/init/steps.js +70 -19
- package/dist/src/core/install-planner.js +62 -17
- package/dist/src/core/install-transaction.js +13 -1
- package/dist/src/core/journal/adapter.js +10 -1
- package/dist/src/core/paths.js +61 -8
- package/dist/src/core/profile.js +12 -0
- package/dist/src/core/provider-artifacts.js +38 -13
- package/dist/src/core/provider-version.js +12 -6
- package/dist/src/core/registries.js +40 -19
- package/dist/src/core/renderers/registry.js +37 -0
- package/dist/src/core/skill-integrity.js +71 -10
- package/dist/src/core/update-check.js +14 -2
- package/dist/src/core/versioning.js +17 -2
- package/dist/src/index.js +31 -23
- package/dist/src/providers/index.js +7 -2
- package/dist/src/utils/config.js +7 -8
- package/dist/tests/commands/doctor.test.js +6 -0
- package/dist/tests/commands/pin.test.js +29 -2
- package/dist/tests/commands/preflight/preflight.test.js +31 -17
- package/dist/tests/commands/registry/install-bundles.test.js +6 -6
- package/dist/tests/commands/sensors/init-pack-unavailable.test.js +96 -0
- package/dist/tests/commands/sensors/install-settings-safety.test.js +87 -0
- package/dist/tests/commands/sensors/run-is-read-only.test.js +0 -0
- package/dist/tests/commands/sensors/run.test.js +24 -51
- package/dist/tests/commands/sensors/status-windows.test.js +39 -22
- package/dist/tests/commands/sensors/status.test.js +27 -12
- package/dist/tests/core/artifact-name-containment.test.js +86 -0
- package/dist/tests/core/context/agents-md-single-slot.test.js +69 -0
- package/dist/tests/core/context/regenerate.test.js +39 -8
- package/dist/tests/core/diagnostics/checks.test.js +14 -6
- package/dist/tests/core/diagnostics/provider-tier.test.js +3 -3
- package/dist/tests/core/diagnostics/rendered-artifact-visibility.test.js +69 -0
- package/dist/tests/core/init/all-baseline-bundles.test.js +64 -0
- package/dist/tests/core/init/context-injection-no-project.test.js +117 -0
- package/dist/tests/core/init/orchestrator.test.js +1 -1
- package/dist/tests/core/init/steps-registry-sync.test.js +1 -1
- package/dist/tests/core/init/steps.test.js +11 -5
- package/dist/tests/core/path-resolution-no-shell.test.js +136 -0
- package/dist/tests/core/paths.test.js +7 -34
- package/dist/tests/core/project-skill-links.test.js +159 -0
- package/dist/tests/core/provider-artifacts.test.js +66 -14
- package/dist/tests/core/provider-capability-guards.test.js +92 -0
- package/dist/tests/core/reconciliation.test.js +15 -15
- package/dist/tests/core/registry-manifest.test.js +1 -1
- package/dist/tests/core/semver-fails-closed.test.js +30 -0
- package/dist/tests/core/skill-integrity.test.js +10 -10
- package/dist/tests/core/skill-repair-safety.test.js +92 -0
- package/dist/tests/core/sync-profile-atomicity.test.js +122 -0
- package/dist/tests/integration/copilot-init-isolated.test.js +2 -2
- package/dist/tests/providers/index.test.js +1 -1
- package/dist/tests/structural/renderer-table-is-single-source.test.js +95 -0
- package/dist/tests/structural/symlink-type-is-explicit.test.js +73 -0
- package/dist/tests/utils/registry-view-overrides.test.js +2 -2
- package/package.json +4 -1
|
@@ -17,19 +17,12 @@ exports.gatherProviderChecks = gatherProviderChecks;
|
|
|
17
17
|
const fs_1 = __importDefault(require("fs"));
|
|
18
18
|
const path_1 = __importDefault(require("path"));
|
|
19
19
|
const providers_1 = require("../../providers");
|
|
20
|
+
const skill_integrity_1 = require("../skill-integrity");
|
|
20
21
|
const provider_version_1 = require("../provider-version");
|
|
21
22
|
const status_1 = require("../../commands/hooks/status");
|
|
22
23
|
const orchestrator_1 = require("../context/orchestrator");
|
|
23
24
|
const registries_1 = require("../registries");
|
|
24
|
-
|
|
25
|
-
* used by `skillsGlobalCheck` to require AWM-shaped evidence, not just an arbitrary
|
|
26
|
-
* non-empty directory (a user's own unrelated file in `~/.cursor/rules` would otherwise
|
|
27
|
-
* read as `'supported'`). Renderers absent from this map (i.e. `'link'`) never reach the
|
|
28
|
-
* branch that reads it. */
|
|
29
|
-
const RENDERED_SKILL_EXTENSIONS = {
|
|
30
|
-
'cursor-mdc': '.mdc',
|
|
31
|
-
'copilot-instructions': '.instructions.md',
|
|
32
|
-
};
|
|
25
|
+
const registry_1 = require("../renderers/registry");
|
|
33
26
|
/** Structural classification, computed purely from `provider`'s config shape — see
|
|
34
27
|
* `ProviderTier`'s doc comment in `types.ts` for what each tier means. */
|
|
35
28
|
function providerTier(provider) {
|
|
@@ -66,7 +59,7 @@ function binaryVersionCheck(agent) {
|
|
|
66
59
|
* `dir` is null — i.e. the provider has no global skill discovery mechanism at all
|
|
67
60
|
* (today: Copilot, see `globalUnsupportedReason` in providers/index.ts).
|
|
68
61
|
*
|
|
69
|
-
* `renderer` gates which verification is possible: `
|
|
62
|
+
* `renderer` gates which verification is possible: `classifySkillLinks` (via `integrity`)
|
|
70
63
|
* only ever sees symlinks — `if (!lst.isSymbolicLink()) continue;` — so for a rendered
|
|
71
64
|
* format (`cursor-mdc`, `copilot-instructions`) it scans a directory of real files and
|
|
72
65
|
* finds nothing, which would make `broken` silently read 0 regardless of whether the
|
|
@@ -87,7 +80,7 @@ function skillsGlobalCheck(dir, owners, integrity, renderer) {
|
|
|
87
80
|
// `remediationCode: 'awm-init'` is a worse remedy than none, but this
|
|
88
81
|
// check has no channel to report "can't tell" separately from "absent"
|
|
89
82
|
// (same tradeoff already made by this file's agentsNativeCheck and by
|
|
90
|
-
// skill-integrity.ts's
|
|
83
|
+
// skill-integrity.ts's classifySkillLinks — a systemic, pre-existing
|
|
91
84
|
// pattern in this codebase, not introduced here).
|
|
92
85
|
entries = [];
|
|
93
86
|
}
|
|
@@ -97,7 +90,12 @@ function skillsGlobalCheck(dir, owners, integrity, renderer) {
|
|
|
97
90
|
// Still not full integrity verification (a stray file with the right extension but
|
|
98
91
|
// wrong content still passes) — that residual gap is the same honest tradeoff this
|
|
99
92
|
// function's doc comment already accepts for the non-`'link'` branch generally.
|
|
100
|
-
|
|
93
|
+
// `rendererExtension` (core/renderers/registry.ts) is the ONE table mapping a
|
|
94
|
+
// renderer to the file it produces. This site used to keep its own partial copy —
|
|
95
|
+
// the fourth such copy in the codebase, and the exact drift that made a rendered
|
|
96
|
+
// artifact invisible to the "is it installed" check for a whole release. A new
|
|
97
|
+
// renderer must not be able to be added without this reading it.
|
|
98
|
+
const ext = (0, registry_1.rendererExtension)(renderer);
|
|
101
99
|
const present = ext ? entries.some((e) => e.endsWith(ext)) : entries.length > 0;
|
|
102
100
|
return {
|
|
103
101
|
id: 'skills.global',
|
|
@@ -165,20 +163,69 @@ function tomlAgentsHealthy(dir, entries) {
|
|
|
165
163
|
* run (e.g. claude-code-only, opencode-only, antigravity-only) would always render
|
|
166
164
|
* inapplicable rows as red ✖ and `overall` could never be 'healthy'.
|
|
167
165
|
*/
|
|
166
|
+
/**
|
|
167
|
+
* Workflows installed at machine scope. Today only Antigravity declares a `workflow`
|
|
168
|
+
* config at all (`~/.gemini/antigravity/global_workflows`), and nothing verified it:
|
|
169
|
+
* `awm init` installed the baseline's workflows there and every diagnostic looked only
|
|
170
|
+
* at skills and native agents, so a broken or emptied workflow directory reported
|
|
171
|
+
* `healthy` forever on the one provider that uses it.
|
|
172
|
+
*
|
|
173
|
+
* Same N/A discipline as `agentsNativeCheck`: a provider with no workflow config, or a
|
|
174
|
+
* registry that ships no `workflows/`, emits NO row rather than a red one nobody can
|
|
175
|
+
* act on — an absent row means "nothing to verify", not "verified fine".
|
|
176
|
+
*/
|
|
177
|
+
function workflowsGlobalCheck(agent) {
|
|
178
|
+
const provider = (0, providers_1.providerFor)(agent);
|
|
179
|
+
if (!provider.workflow || provider.workflow.global === null)
|
|
180
|
+
return null;
|
|
181
|
+
const dir = provider.workflow.global;
|
|
182
|
+
let entries;
|
|
183
|
+
try {
|
|
184
|
+
entries = fs_1.default.readdirSync(dir);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
if (entries.length === 0)
|
|
190
|
+
return null;
|
|
191
|
+
// Los workflows se instalan con el renderer `link`, asi que un symlink colgante es
|
|
192
|
+
// exactamente la misma clase de rotura que en skills — y se clasifica con la misma
|
|
193
|
+
// funcion, no con una copia local que pueda divergir.
|
|
194
|
+
const integrity = (0, skill_integrity_1.classifySkillLinks)(dir, (0, registries_1.contentRoots)());
|
|
195
|
+
const broken = integrity.repairable.length + integrity.dead.length;
|
|
196
|
+
return {
|
|
197
|
+
id: 'workflows.global',
|
|
198
|
+
state: broken > 0 ? 'broken' : 'healthy',
|
|
199
|
+
target: dir,
|
|
200
|
+
detail: broken > 0 ? `${broken} broken link(s)` : undefined,
|
|
201
|
+
remediationCode: broken > 0 ? 'awm-init' : undefined,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
168
204
|
function agentsNativeCheck(agent) {
|
|
169
205
|
const provider = (0, providers_1.providerFor)(agent);
|
|
170
206
|
if (!provider.agent || provider.agent.global === null)
|
|
171
207
|
return null;
|
|
172
208
|
const dir = provider.agent.global;
|
|
173
209
|
let entries;
|
|
210
|
+
// `absent` degrada el estado global, asi que sin `remediationCode` doctor
|
|
211
|
+
// salia 1 sin decir que hacer — y para un registry que simplemente no trae
|
|
212
|
+
// `agents/` (lo normal) ese rojo no tiene accion posible. Se reporta como
|
|
213
|
+
// `unsupported`, que describe la realidad: no hay artefactos nativos que
|
|
214
|
+
// verificar, y no es culpa de la instalacion.
|
|
215
|
+
// Un registry que simplemente no trae `agents/` es lo normal, no un defecto
|
|
216
|
+
// de la instalacion — y no hay accion que el usuario pueda tomar. Antes esto
|
|
217
|
+
// devolvia `absent`, un estado que DEGRADA, y sin `remediationCode`: doctor
|
|
218
|
+
// salia 1 mostrando `✖ native agents` sin decir que hacer, justo despues de
|
|
219
|
+
// un `awm init` exitoso. Cuando no hay nada nativo que verificar, no se
|
|
220
|
+
// emite fila — el mismo criterio que ya usan los demas casos N/A de aca.
|
|
174
221
|
try {
|
|
175
222
|
entries = fs_1.default.readdirSync(dir);
|
|
176
223
|
}
|
|
177
224
|
catch {
|
|
178
|
-
return
|
|
225
|
+
return null;
|
|
179
226
|
}
|
|
180
227
|
if (entries.length === 0)
|
|
181
|
-
return
|
|
228
|
+
return null;
|
|
182
229
|
if (provider.agent.renderer === 'codex-agent-toml') {
|
|
183
230
|
const { broken } = tomlAgentsHealthy(dir, entries);
|
|
184
231
|
return {
|
|
@@ -292,6 +339,7 @@ function gatherProviderChecks(agents, scanSkills, projectRoot) {
|
|
|
292
339
|
binaryVersionCheck(agent),
|
|
293
340
|
skillsGlobalCheck(dir, owners, integrity, provider.skill.renderer),
|
|
294
341
|
agentsNativeCheck(agent),
|
|
342
|
+
workflowsGlobalCheck(agent),
|
|
295
343
|
hookTrustCheck(agent),
|
|
296
344
|
contextGlobalCheck(agent, projectRoot),
|
|
297
345
|
].filter((check) => check !== null);
|
|
@@ -94,7 +94,7 @@ function discoverWorkflows(roots = (0, registries_1.contentRoots)()) {
|
|
|
94
94
|
for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
95
95
|
if (entry.isDirectory() || !entry.name.endsWith('.md'))
|
|
96
96
|
continue;
|
|
97
|
-
const name = entry.name.replace(
|
|
97
|
+
const name = entry.name.replace(/\.md$/, '');
|
|
98
98
|
const filePath = path_1.default.join(dir, entry.name);
|
|
99
99
|
mergeEntry('workflow', byName, {
|
|
100
100
|
name,
|
|
@@ -120,7 +120,7 @@ function discoverAgents(roots = (0, registries_1.contentRoots)()) {
|
|
|
120
120
|
for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
121
121
|
if (entry.isDirectory() || !entry.name.endsWith('.md'))
|
|
122
122
|
continue;
|
|
123
|
-
const name = entry.name.replace(
|
|
123
|
+
const name = entry.name.replace(/\.md$/, '');
|
|
124
124
|
const filePath = path_1.default.join(dir, entry.name);
|
|
125
125
|
mergeEntry('agent', byName, {
|
|
126
126
|
name,
|
|
@@ -110,6 +110,17 @@ function planInitMutationTargets(params) {
|
|
|
110
110
|
for (const b of machineBundles) {
|
|
111
111
|
addBundleTargets(targets, b.name, bundles, agent, 'global', cwd, contentDir);
|
|
112
112
|
}
|
|
113
|
+
// Providers whose context injection is itself project-scope (managed-agents-md
|
|
114
|
+
// with a null globalPath — Cursor, Copilot) write AGENTS.md and the materialized
|
|
115
|
+
// .awm/context/ file. `stepContextInjection` now refuses to write those without a
|
|
116
|
+
// discovered project root, but enumerating them against `cwd` unconditionally
|
|
117
|
+
// costs nothing (a target that goes unwritten is a no-op backup entry, per this
|
|
118
|
+
// module's opening note) and keeps the guarantee from resting on a single site:
|
|
119
|
+
// under-enumeration here is the one failure mode that silently defeats rollback.
|
|
120
|
+
if (injection?.type === 'managed-agents-md' && injection.globalPath === null) {
|
|
121
|
+
targets.add(path_1.default.join(cwd, path_1.default.basename(injection.localFile)));
|
|
122
|
+
targets.add((0, materializer_1.projectContextPath)(cwd));
|
|
123
|
+
}
|
|
113
124
|
// project-level: profile, sensors manifest, project injection, and every
|
|
114
125
|
// extension currently recorded in .awm/profile.json
|
|
115
126
|
const projectRoot = (0, profile_1.findProjectRoot)(cwd);
|
|
@@ -55,6 +55,7 @@ exports.defaultActions = {
|
|
|
55
55
|
method: o.method,
|
|
56
56
|
projectRoot: o.projectRoot,
|
|
57
57
|
contentDir: o.contentDir,
|
|
58
|
+
scopeOverride: o.scopeOverride,
|
|
58
59
|
}),
|
|
59
60
|
// Same as installBundle above — defaults to the real applyInstallPlan.
|
|
60
61
|
syncProfile: (o) => (0, bundle_install_1.syncProfile)({
|
|
@@ -66,14 +67,21 @@ exports.defaultActions = {
|
|
|
66
67
|
}),
|
|
67
68
|
initSensors: (o) => {
|
|
68
69
|
const result = (0, init_1.initSensors)({ cwd: o.cwd, registryRoot: o.registryRoot, configure: o.configure });
|
|
69
|
-
|
|
70
|
+
// `unavailablePack`/`manifest` viajan a proposito: `stepSensors` los reporta.
|
|
71
|
+
// Este wrapper proyectaba solo `detection`, asi que cualquier hallazgo nuevo de
|
|
72
|
+
// initSensors moria aca en silencio antes de llegar al step que lo muestra.
|
|
73
|
+
return {
|
|
74
|
+
detection: result.detection,
|
|
75
|
+
manifest: { pack: result.manifest.pack },
|
|
76
|
+
...(result.unavailablePack ? { unavailablePack: result.unavailablePack } : {}),
|
|
77
|
+
};
|
|
70
78
|
},
|
|
71
79
|
addExtension: (root, name) => { (0, profile_1.addExtension)(root, name); },
|
|
72
80
|
ensureProfile: (root) => { (0, profile_1.ensureProfile)(root); },
|
|
73
81
|
gatherProject: (cwd, bundles, agent) => (0, context_1.gatherContext)({ cwd, bundles, agent }).project,
|
|
74
82
|
contextStatus: (op) => realInjectionOrchestrator.contextStatus(op),
|
|
75
83
|
installContext: (op) => { realInjectionOrchestrator.installContext(op); },
|
|
76
|
-
repairGlobalSkills: (skillsDir, registryContentDirs) => (0, skill_integrity_1.
|
|
84
|
+
repairGlobalSkills: (skillsDir, registryContentDirs) => (0, skill_integrity_1.repairSkillLinks)(skillsDir, registryContentDirs),
|
|
77
85
|
injectProjectConstitution: (o) => {
|
|
78
86
|
if ((0, providers_1.getInjection)(o.agent)?.type === 'managed-agents-md') {
|
|
79
87
|
return new codex_agents_1.CodexAgentsStrategy().injectProject(o.projectRoot, (0, providers_1.providerFor)(o.agent), o.agent) === 'injected' ? 'injected' : 'already';
|
|
@@ -113,8 +121,12 @@ function failed(id, level, error) {
|
|
|
113
121
|
* physical operation and simply re-confirms ownership — it does not change
|
|
114
122
|
* the co-owner's installed content.
|
|
115
123
|
*/
|
|
116
|
-
function sharedInstallAgents(d) {
|
|
117
|
-
|
|
124
|
+
function sharedInstallAgents(d, scope = 'global') {
|
|
125
|
+
// El scope tiene que ser el MISMO con el que se va a instalar: para un
|
|
126
|
+
// provider sin directorio global (Copilot) el baseline va a scope local, y
|
|
127
|
+
// preguntar por el grupo compartido en 'global' devolvia un grupo calculado
|
|
128
|
+
// sobre un destino que para el no existe.
|
|
129
|
+
const group = (0, install_planner_1.agentsSharingSkillTarget)(d.agent, d.enabledAgents, scope, d.cwd);
|
|
118
130
|
return group.includes(d.agent) ? group : [d.agent, ...group];
|
|
119
131
|
}
|
|
120
132
|
/**
|
|
@@ -188,18 +200,34 @@ function stepDevCore(d) {
|
|
|
188
200
|
if (devCore.present && devCore.brokenLinks.length === 0) {
|
|
189
201
|
return ok('machine.devCore', 'machine', 'skipped');
|
|
190
202
|
}
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
+
// TODOS los bundles baseline, no el primero. `scope: 'baseline'` es una propiedad
|
|
204
|
+
// que un bundle TIENE, no un lugar que solo uno puede ocupar — y
|
|
205
|
+
// `reconciliation.ts` (`awm update`) siempre los filtro todos. Con `find` aca, un
|
|
206
|
+
// registry con dos baselines instalaba uno en `awm init` y aparecia el segundo, de
|
|
207
|
+
// la nada, en el siguiente `awm update`; nada antes lo habia reportado como
|
|
208
|
+
// faltante. El fallback a 'dev' se conserva para un registry sin ningun baseline.
|
|
209
|
+
const baselineNames = d.bundles.filter((b) => b.scope === 'baseline').map((b) => b.name);
|
|
210
|
+
const toInstall = baselineNames.length > 0 ? baselineNames : ['dev'];
|
|
211
|
+
// Un provider sin directorio global de skills (hoy: Copilot) no tiene donde
|
|
212
|
+
// poner el baseline a nivel maquina — y el hecho `devCore` lo reporta como
|
|
213
|
+
// "trivialmente satisfecho" para no volver a intentar un install global
|
|
214
|
+
// condenado a tirar. Pero nada instalaba nunca nada en su lugar: Copilot
|
|
215
|
+
// terminaba con CERO skills mientras `awm init` imprimia `✔ dev-core` y
|
|
216
|
+
// `awm doctor` decia `healthy`. Su propio `globalUnsupportedReason` ya dice
|
|
217
|
+
// que hay que instalar por proyecto, asi que eso es lo que se hace.
|
|
218
|
+
const localOnly = (0, providers_1.providerFor)(d.agent).skill.global === null;
|
|
219
|
+
for (const bundleName of toInstall) {
|
|
220
|
+
d.actions.installBundle({
|
|
221
|
+
bundleName,
|
|
222
|
+
bundles: d.bundles,
|
|
223
|
+
agents: sharedInstallAgents(d, localOnly ? 'local' : 'global'),
|
|
224
|
+
method: d.installMethod,
|
|
225
|
+
projectRoot: d.cwd,
|
|
226
|
+
contentDir: d.contentDir,
|
|
227
|
+
...(localOnly ? { scopeOverride: 'local' } : {}),
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
return ok('machine.devCore', 'machine', 'applied', localOnly ? 'installed at project scope (no global skill dir)' : undefined);
|
|
203
231
|
}
|
|
204
232
|
/** Step 3.5 – Repair broken global skill symlinks (orphans outside the baseline). */
|
|
205
233
|
function stepGlobalSkillsRepair(d) {
|
|
@@ -294,7 +322,14 @@ function stepSensors(d) {
|
|
|
294
322
|
return ok('project.sensors', 'project', 'skipped', 'no project');
|
|
295
323
|
if (proj.sensors.present)
|
|
296
324
|
return ok('project.sensors', 'project', 'skipped');
|
|
297
|
-
d.actions.initSensors({ cwd: proj.root, registryRoot: d.sensorPacksRoot, configure: true });
|
|
325
|
+
const res = d.actions.initSensors({ cwd: proj.root, registryRoot: d.sensorPacksRoot, configure: true });
|
|
326
|
+
// A registry without the detected pack yields a fallback (or empty) manifest. The
|
|
327
|
+
// step still succeeded — a manifest was written — but reporting a bare 'applied'
|
|
328
|
+
// would let `awm init` hand back a quality gate that measures less than the
|
|
329
|
+
// operator has any reason to expect. Say which pack was missing, here, once.
|
|
330
|
+
if (res.unavailablePack) {
|
|
331
|
+
return ok('project.sensors', 'project', 'applied', `registry has no '${res.unavailablePack}' sensor-pack — wrote '${res.manifest?.pack ?? 'fallback'}' instead; run \`awm update\``);
|
|
332
|
+
}
|
|
298
333
|
return ok('project.sensors', 'project', 'applied');
|
|
299
334
|
}
|
|
300
335
|
/** Step 8 – Signal that the agent should run the project-constitution skill. */
|
|
@@ -348,8 +383,24 @@ function stepContextInjection(d) {
|
|
|
348
383
|
// AGENTS.md backup target via the same findProjectRoot(cwd) call, so using d.cwd here
|
|
349
384
|
// whenever it differs from the walked-up project root (e.g. `awm init` run from a
|
|
350
385
|
// subdirectory) would write to a path the backup session never snapshotted — a failed
|
|
351
|
-
// init couldn't roll it back.
|
|
352
|
-
//
|
|
386
|
+
// init couldn't roll it back.
|
|
387
|
+
//
|
|
388
|
+
// The `?? d.cwd` fallback below is the legitimate case: a provider whose ONLY
|
|
389
|
+
// delivery channel is project-scope (Copilot), run in a directory the user means
|
|
390
|
+
// as their project but which carries no marker yet (no .git, no package.json).
|
|
391
|
+
// Removing it would leave Copilot with no context at all there. What made it a bug
|
|
392
|
+
// was that mutation-targets.ts enumerated the AGENTS.md / .awm/context/ backup
|
|
393
|
+
// targets only inside `if (projectRoot)`, so this write landed outside the backup
|
|
394
|
+
// session and survived a rollback that reported a clean restore. That is fixed
|
|
395
|
+
// where it belongs — in the enumeration, which now covers `cwd` unconditionally
|
|
396
|
+
// for these providers.
|
|
397
|
+
//
|
|
398
|
+
// `--machine-only` is the case that genuinely must not write. It nulls
|
|
399
|
+
// `ctx.project` on purpose, and this MACHINE-level step read that as "no project
|
|
400
|
+
// found" and wrote into cwd anyway — under the one flag that promises not to.
|
|
401
|
+
if (scope === 'local' && d.machineOnly) {
|
|
402
|
+
return ok('machine.contextInjection', 'machine', 'skipped', `${(0, providers_1.providerFor)(d.agent).label} delivers context at project scope — skipped by --machine-only`);
|
|
403
|
+
}
|
|
353
404
|
const op = {
|
|
354
405
|
agent: d.agent,
|
|
355
406
|
scope,
|
|
@@ -23,6 +23,7 @@ exports.planRemoval = planRemoval;
|
|
|
23
23
|
// persistence (artifact-state.ts). This module only computes the plan.
|
|
24
24
|
const path_1 = __importDefault(require("path"));
|
|
25
25
|
const providers_1 = require("../providers");
|
|
26
|
+
const registry_1 = require("./renderers/registry");
|
|
26
27
|
/** Resolves the single physical filesystem location an intent renders to for one agent. */
|
|
27
28
|
/**
|
|
28
29
|
* Resolves the physical target path + renderer for one artifact intent on one
|
|
@@ -44,21 +45,27 @@ function physicalTarget(intent, agent, scope, projectRoot) {
|
|
|
44
45
|
// trailing `.md` (if any) on intent.installName, so e.g. `using-awm`
|
|
45
46
|
// (skills carry no extension) or `using-awm.md` both become
|
|
46
47
|
// `using-awm.instructions.md`, never `using-awm.md.instructions.md`.
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
48
|
+
// `renderedFilename` (core/renderers/registry.ts) es la unica fuente de
|
|
49
|
+
// verdad de este mapeo. Antes vivia inline aca y re-derivado a mano en tres
|
|
50
|
+
// lectores mas, uno de los cuales lo omitia por completo — y ese era
|
|
51
|
+
// justamente el que decidia si un artefacto estaba instalado.
|
|
52
|
+
const filename = (0, registry_1.renderedFilename)(intent.installName, config.renderer);
|
|
53
|
+
const targetPath = path_1.default.join(dir, filename);
|
|
54
|
+
// Asercion de contencion: el destino resuelto DEBE caer dentro del
|
|
55
|
+
// directorio del provider. `dir` es el ancla de confianza real (sale de la
|
|
56
|
+
// config del provider, no del contenido), asi que verificarlo aca atrapa
|
|
57
|
+
// cualquier camino — presente o futuro — que construya un `installName` sin
|
|
58
|
+
// pasar por `assertSafeArtifactName`. Los nombres llegan desde el
|
|
59
|
+
// `bundle.json` de un registry, y el instalador borra el destino de forma
|
|
60
|
+
// recursiva antes de enlazar: un escape aca no es un archivo mal ubicado,
|
|
61
|
+
// es borrado arbitrario fuera del sandbox.
|
|
62
|
+
const resolvedDir = path_1.default.resolve(dir);
|
|
63
|
+
const resolved = path_1.default.resolve(targetPath);
|
|
64
|
+
if (resolved !== resolvedDir && !resolved.startsWith(resolvedDir + path_1.default.sep)) {
|
|
65
|
+
throw new Error(`refusing to install outside the target directory: ${JSON.stringify(intent.installName)} ` +
|
|
66
|
+
`resolves to ${resolved}, which is not inside ${resolvedDir}`);
|
|
67
|
+
}
|
|
68
|
+
return { targetPath, renderer: config.renderer };
|
|
62
69
|
}
|
|
63
70
|
/**
|
|
64
71
|
* The physical directory `agent`'s skill artifacts resolve to at `scope` —
|
|
@@ -121,7 +128,19 @@ function assertCompleteSharedGroup(intent, selected, enabled, scope, projectRoot
|
|
|
121
128
|
if (intent.type !== 'skill')
|
|
122
129
|
return;
|
|
123
130
|
for (const agent of selected) {
|
|
124
|
-
|
|
131
|
+
// El `.filter()` de abajo ya tenia esta guarda; esta linea no — y es la
|
|
132
|
+
// que corre PRIMERO. Con copilot en `selected` (que es lo que hace
|
|
133
|
+
// `awm update`, que apunta a todos los agentes habilitados) tiraba aca,
|
|
134
|
+
// antes de llegar al loop de planificacion, rompiendo el comando para
|
|
135
|
+
// TODOS los demas agentes de la maquina. Un agente que no soporta este
|
|
136
|
+
// scope no tiene grupo compartido que verificar: no es parte del grupo.
|
|
137
|
+
let target;
|
|
138
|
+
try {
|
|
139
|
+
target = physicalTarget(intent, agent, scope, projectRoot).targetPath;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
125
144
|
// Same reasoning as agentsSharingSkillTarget above: a candidate in
|
|
126
145
|
// `enabled` that doesn't support this scope (e.g. Copilot at `global`)
|
|
127
146
|
// can't be part of the shared-target group — it just isn't a
|
|
@@ -172,7 +191,33 @@ function planInstall(params) {
|
|
|
172
191
|
}
|
|
173
192
|
const groups = new Map();
|
|
174
193
|
for (const intent of artifacts) {
|
|
175
|
-
|
|
194
|
+
// Agentes que estructuralmente NO pueden recibir este artefacto en este
|
|
195
|
+
// scope — hoy: copilot en scope global, que no tiene mecanismo de
|
|
196
|
+
// descubrimiento de skills a nivel usuario.
|
|
197
|
+
//
|
|
198
|
+
// Se saltean, pero SOLO si algun otro agente seleccionado si puede. La
|
|
199
|
+
// distincion es deliberada:
|
|
200
|
+
// - `awm update` / `awm add <bundle>` corren sobre TODOS los agentes
|
|
201
|
+
// habilitados. Que uno no pueda no es un error del usuario, y hacer
|
|
202
|
+
// tirar todo el plan rompia el comando para TODOS los demas agentes
|
|
203
|
+
// de la maquina — incluido el re-sync de hooks, que ni se alcanzaba.
|
|
204
|
+
// - `awm add x -a copilot -s global` es una peticion EXPLICITA. Ahi no
|
|
205
|
+
// hay a quien saltear, asi que sigue tirando con la explicacion
|
|
206
|
+
// (`globalUnsupportedReason`), que es justo lo que el usuario
|
|
207
|
+
// necesita leer.
|
|
208
|
+
const capable = selectedAgents.filter((agent) => {
|
|
209
|
+
if (!(0, providers_1.providerFor)(agent)[intent.type])
|
|
210
|
+
return false;
|
|
211
|
+
try {
|
|
212
|
+
physicalTarget(intent, agent, scope, projectRoot);
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
const effectiveAgents = capable.length > 0 ? capable : selectedAgents;
|
|
220
|
+
for (const agent of effectiveAgents) {
|
|
176
221
|
const config = (0, providers_1.providerFor)(agent)[intent.type];
|
|
177
222
|
if (!config)
|
|
178
223
|
continue;
|
|
@@ -41,8 +41,20 @@ const executor_1 = require("./executor");
|
|
|
41
41
|
* accepts. Strips both `:` and `.` (an ISO timestamp has both) so the result
|
|
42
42
|
* is filesystem- and regex-safe either way.
|
|
43
43
|
*/
|
|
44
|
+
/** Id de transaccion: timestamp + sufijo aleatorio.
|
|
45
|
+
*
|
|
46
|
+
* El timestamp solo tiene resolucion de 1 ms y NO era unico. Dos
|
|
47
|
+
* transacciones en el mismo milisegundo (p. ej. `syncProfile`, que corre una
|
|
48
|
+
* por extension en un loop) producian el mismo id, y como cada una reescribe
|
|
49
|
+
* `manifest.json` con SUS entradas y numera los slots de backup desde 0, la
|
|
50
|
+
* segunda pisaba el backup de la primera: el respaldo del archivo original del
|
|
51
|
+
* usuario quedaba irrecuperable y `awm backup restore <id>` restauraba el
|
|
52
|
+
* target equivocado. Es un agujero en el unico mecanismo del que depende toda
|
|
53
|
+
* la promesa de "siempre se puede revertir". El regex de `restoreBackup` ya
|
|
54
|
+
* acepta este formato. */
|
|
44
55
|
function sanitizeTransactionTimestamp() {
|
|
45
|
-
|
|
56
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
57
|
+
return `${stamp}-${crypto_1.default.randomBytes(4).toString('hex')}`;
|
|
46
58
|
}
|
|
47
59
|
function ensureBackupDir(backupDir) {
|
|
48
60
|
fs_1.default.mkdirSync(backupDir, { recursive: true });
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WATCH_PROVIDERS = void 0;
|
|
4
|
+
exports.isWatchProvider = isWatchProvider;
|
|
3
5
|
exports.adapterFor = adapterFor;
|
|
4
6
|
const process_1 = require("./process");
|
|
5
7
|
const RESUME_PROMPT_PREFIX = 'Sos el orquestador SDD de este repo. Corre `awm job reconcile` y ejecuta ';
|
|
@@ -18,10 +20,17 @@ const claudeAdapter = {
|
|
|
18
20
|
activity: process_1.activitySnapshot,
|
|
19
21
|
safeToReplace: baseSafeToReplace,
|
|
20
22
|
};
|
|
23
|
+
/** Los providers que `awm watch` sabe supervisar. Exportado para que el CLI valide en
|
|
24
|
+
* el borde en vez de dejar que un `--provider` invalido llegue hasta el primer tick,
|
|
25
|
+
* ya con el journal tocado y el lock tomado. */
|
|
26
|
+
exports.WATCH_PROVIDERS = ['codex', 'claude-code'];
|
|
27
|
+
function isWatchProvider(value) {
|
|
28
|
+
return typeof value === 'string' && exports.WATCH_PROVIDERS.includes(value);
|
|
29
|
+
}
|
|
21
30
|
function adapterFor(provider) {
|
|
22
31
|
if (provider === 'codex')
|
|
23
32
|
return codexAdapter;
|
|
24
33
|
if (provider === 'claude-code')
|
|
25
34
|
return claudeAdapter;
|
|
26
|
-
throw new Error(`provider desconocido: ${provider} (validos:
|
|
35
|
+
throw new Error(`provider desconocido: ${provider} (validos: ${exports.WATCH_PROVIDERS.join(', ')})`);
|
|
27
36
|
}
|
package/dist/src/core/paths.js
CHANGED
|
@@ -17,8 +17,8 @@ exports.resolveOnPath = resolveOnPath;
|
|
|
17
17
|
// Functions are evaluated at CALL TIME (not require time) so env overrides are
|
|
18
18
|
// always honored and tests need no jest.resetModules().
|
|
19
19
|
const os_1 = __importDefault(require("os"));
|
|
20
|
+
const fs_1 = __importDefault(require("fs"));
|
|
20
21
|
const path_1 = __importDefault(require("path"));
|
|
21
|
-
const child_process_1 = require("child_process");
|
|
22
22
|
/** User home directory with a robust fallback. Never returns a raw, possibly-empty process.env.HOME. */
|
|
23
23
|
function homeDir() {
|
|
24
24
|
return process.env.HOME || os_1.default.homedir();
|
|
@@ -74,14 +74,67 @@ function noteWindowsCaveat(log) {
|
|
|
74
74
|
if (isWindowsNative())
|
|
75
75
|
log(exports.WINDOWS_KNOWN_GAP);
|
|
76
76
|
}
|
|
77
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* Resolve a binary on PATH, IN PROCESS — no shell, no subprocess.
|
|
79
|
+
*
|
|
80
|
+
* SECURITY (this is the whole reason for the implementation below): this used
|
|
81
|
+
* to build `command -v ${bin}` / `where ${bin}` and hand it to `execSync`,
|
|
82
|
+
* which runs it through a shell. `bin` is the first token of a sensor's `cmd`,
|
|
83
|
+
* and that string comes from `.awm/sensors.json` — a file committed in the
|
|
84
|
+
* repo, and also regenerated from a registry sensor-pack's `defaultCmd`. So a
|
|
85
|
+
* cloned untrusted repo, or a third-party registry, could put shell syntax
|
|
86
|
+
* there. The token can't contain whitespace (it's a split token), but `${IFS}`
|
|
87
|
+
* and `$(...)` make that irrelevant. Confirmed against the real binary: a
|
|
88
|
+
* crafted `cmd` executed an arbitrary command during `awm sensors status` —
|
|
89
|
+
* a read-only inspection command — which then reported the sensor HEALTHY,
|
|
90
|
+
* because the injected command exited 0.
|
|
91
|
+
*
|
|
92
|
+
* Resolving PATH ourselves removes the interpreter entirely, so there is
|
|
93
|
+
* nothing left to inject into on any platform. It's also faster (no process
|
|
94
|
+
* spawn) and more predictable than depending on `where`/`command -v` being
|
|
95
|
+
* present and well-behaved.
|
|
96
|
+
*/
|
|
78
97
|
function resolveOnPath(bin) {
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
(0, child_process_1.execSync)(cmd, { stdio: 'pipe' });
|
|
82
|
-
return true;
|
|
83
|
-
}
|
|
84
|
-
catch {
|
|
98
|
+
if (typeof bin !== 'string' || bin.trim() === '')
|
|
85
99
|
return false;
|
|
100
|
+
// win32 resolves `eslint` to `eslint.cmd`/`eslint.exe` via PATHEXT. Missing
|
|
101
|
+
// this broke every npm-shim-backed sensor on Windows (real bug, published in
|
|
102
|
+
// v3.9.0). POSIX has no such concept — the name is the name.
|
|
103
|
+
const extensions = isWindowsNative()
|
|
104
|
+
? (process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean)
|
|
105
|
+
: [];
|
|
106
|
+
const isExecutable = (candidate) => {
|
|
107
|
+
try {
|
|
108
|
+
if (!fs_1.default.statSync(candidate).isFile())
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
// On win32 the executable bit doesn't exist; being a file with a PATHEXT
|
|
115
|
+
// extension is the whole contract.
|
|
116
|
+
if (isWindowsNative())
|
|
117
|
+
return true;
|
|
118
|
+
try {
|
|
119
|
+
fs_1.default.accessSync(candidate, fs_1.default.constants.X_OK);
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
// PATHEXT is conventionally uppercase (`.COM;.EXE;.BAT;.CMD`) while the files
|
|
127
|
+
// on disk are usually lowercase (`eslint.cmd`). On real Windows that's a
|
|
128
|
+
// non-issue because the filesystem is case-insensitive — but it IS an issue
|
|
129
|
+
// on any case-sensitive volume, so try both spellings rather than depend on
|
|
130
|
+
// filesystem behavior we don't control.
|
|
131
|
+
const matches = (base) => isExecutable(base) ||
|
|
132
|
+
extensions.some((ext) => isExecutable(base + ext) || isExecutable(base + ext.toLowerCase()));
|
|
133
|
+
// A name carrying a separator is a path, not a PATH lookup — resolve it
|
|
134
|
+
// directly instead of joining it onto every PATH entry.
|
|
135
|
+
if (bin.includes('/') || (isWindowsNative() && bin.includes('\\'))) {
|
|
136
|
+
return matches(path_1.default.resolve(bin));
|
|
86
137
|
}
|
|
138
|
+
const entries = (process.env.PATH ?? '').split(path_1.default.delimiter).filter(Boolean);
|
|
139
|
+
return entries.some((dir) => matches(path_1.default.join(dir, bin)));
|
|
87
140
|
}
|
package/dist/src/core/profile.js
CHANGED
|
@@ -125,6 +125,18 @@ function ensureSkillsGitignored(root, agents) {
|
|
|
125
125
|
const config = provider[type];
|
|
126
126
|
if (!config)
|
|
127
127
|
continue;
|
|
128
|
+
// Solo se ignoran los artefactos que son SYMLINKS especificos de la
|
|
129
|
+
// maquina (renderer `link`), que es lo que `awm sync` reconstruye.
|
|
130
|
+
// Un renderer que produce CONTENIDO (`.mdc` de cursor,
|
|
131
|
+
// `.instructions.md` de copilot) genera archivos reales que el
|
|
132
|
+
// equipo necesita commitear — y para copilot ese es su UNICO canal
|
|
133
|
+
// de entrega, asi que ignorarlos hacia que los companeros no
|
|
134
|
+
// recibieran absolutamente nada. Tambien arrastraba a
|
|
135
|
+
// `.cursor/rules/awm.mdc`, el carrier que existe justamente para
|
|
136
|
+
// que el contexto sobreviva al Background Agent de Cursor, que lee
|
|
137
|
+
// del checkout de git.
|
|
138
|
+
if (config.renderer !== 'link')
|
|
139
|
+
continue;
|
|
128
140
|
const entry = config.local.endsWith('/') ? config.local : `${config.local}/`;
|
|
129
141
|
if (!toIgnore.includes(entry))
|
|
130
142
|
toIgnore.push(entry);
|