agentic-workflow-manager 8.2.1 → 8.4.0
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/doctor.js +40 -1
- package/dist/src/commands/hooks/claude.js +47 -31
- package/dist/src/commands/hooks/index.js +1 -1
- package/dist/src/commands/hooks/shared.js +1 -1
- package/dist/src/commands/registry/add.js +6 -1
- package/dist/src/commands/registry/index.js +3 -0
- package/dist/src/core/context/orchestrator.js +15 -2
- package/dist/src/core/context/provider.js +29 -1
- package/dist/src/core/context/regenerate.js +29 -0
- package/dist/src/core/dashboard/collect.js +170 -0
- package/dist/src/core/dashboard/plan-state.js +44 -0
- package/dist/src/core/dashboard/render-html.js +82 -0
- package/dist/src/core/dashboard/render-terminal.js +43 -0
- package/dist/src/core/dashboard/sanitize.js +62 -0
- package/dist/src/core/dashboard/styles.js +51 -0
- package/dist/src/core/dashboard/types.js +21 -0
- package/dist/src/core/dashboard/validate.js +106 -0
- package/dist/src/core/dashboard/write-html.js +88 -0
- package/dist/src/core/diagnostics/context.js +1 -1
- package/dist/src/core/orchestrators.js +142 -0
- package/dist/tests/commands/doctor-is-read-only.test.js +54 -1
- package/dist/tests/commands/doctor.test.js +160 -0
- package/dist/tests/commands/hooks/install-symlink-fallback.test.js +13 -4
- package/dist/tests/commands/hooks/install.test.js +121 -3
- package/dist/tests/commands/hooks/resync.test.js +40 -2
- package/dist/tests/commands/registry/add.test.js +104 -0
- package/dist/tests/core/context/orchestrator.test.js +86 -0
- package/dist/tests/core/context/provider.test.js +137 -0
- package/dist/tests/core/context/regenerate.test.js +56 -0
- package/dist/tests/core/dashboard/collect.test.js +173 -0
- package/dist/tests/core/dashboard/contracts.test.js +92 -0
- package/dist/tests/core/dashboard/plan-state.test.js +32 -0
- package/dist/tests/core/dashboard/production-adapters.test.js +70 -0
- package/dist/tests/core/dashboard/render-html.test.js +175 -0
- package/dist/tests/core/dashboard/render-terminal.test.js +65 -0
- package/dist/tests/core/dashboard/write-html.test.js +112 -0
- package/dist/tests/core/orchestrators.test.js +236 -0
- package/dist/tests/helpers/dashboard-fixtures.js +66 -0
- package/package.json +1 -1
|
@@ -15,6 +15,10 @@ const paths_1 = require("../core/paths");
|
|
|
15
15
|
// read-only y el segundo auto-vivifica el archivo. Ver la nota en config.ts.
|
|
16
16
|
const config_1 = require("../utils/config");
|
|
17
17
|
const agent_targets_1 = require("../core/agent-targets");
|
|
18
|
+
const collect_1 = require("../core/dashboard/collect");
|
|
19
|
+
const render_html_1 = require("../core/dashboard/render-html");
|
|
20
|
+
const render_terminal_1 = require("../core/dashboard/render-terminal");
|
|
21
|
+
const write_html_1 = require("../core/dashboard/write-html");
|
|
18
22
|
function glyph(status) {
|
|
19
23
|
if (status === 'ok')
|
|
20
24
|
return picocolors_1.default.green('✔');
|
|
@@ -123,6 +127,38 @@ function renderProviderReport(report) {
|
|
|
123
127
|
}
|
|
124
128
|
function runDoctor(opts = {}) {
|
|
125
129
|
const resolveTargets = opts.resolveTargets ?? agent_targets_1.resolveAgentTargets;
|
|
130
|
+
const invalid = (message) => { process.stderr.write(`awm doctor: ${message}\n`); return 2; };
|
|
131
|
+
const htmlRequested = opts.html !== undefined;
|
|
132
|
+
if (opts.json && opts.full)
|
|
133
|
+
return invalid('--json cannot be combined with --full');
|
|
134
|
+
if (opts.json && htmlRequested)
|
|
135
|
+
return invalid('--json cannot be combined with --html');
|
|
136
|
+
if (opts.full && htmlRequested)
|
|
137
|
+
return invalid('--full cannot be combined with --html');
|
|
138
|
+
if (opts.force && !htmlRequested)
|
|
139
|
+
return invalid('--force requires --html');
|
|
140
|
+
if (opts.full || htmlRequested) {
|
|
141
|
+
try {
|
|
142
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
143
|
+
const target = htmlRequested ? (0, write_html_1.resolveHtmlTarget)({ cwd, target: opts.html, force: opts.force }) : undefined;
|
|
144
|
+
const targets = resolveTargets({ prefs: (0, config_1.readPreferences)(), explicit: opts.agent });
|
|
145
|
+
const collectSnapshot = opts.collectSnapshot ?? collect_1.collectDashboardSnapshot;
|
|
146
|
+
const context = (0, context_1.gatherContext)({ cwd, agents: targets });
|
|
147
|
+
const snapshot = collectSnapshot({ cwd, now: new Date().toISOString(), adapters: {
|
|
148
|
+
...(0, collect_1.productionDashboardAdapters)(context),
|
|
149
|
+
} });
|
|
150
|
+
if (opts.full)
|
|
151
|
+
process.stdout.write((0, render_terminal_1.renderFullTerminal)(snapshot) + '\n');
|
|
152
|
+
if (htmlRequested) {
|
|
153
|
+
(0, write_html_1.writeHtmlAtomically)({ cwd, target: target, html: (0, render_html_1.renderDashboardHtml)(snapshot), force: opts.force });
|
|
154
|
+
process.stdout.write(`${target}\n`);
|
|
155
|
+
}
|
|
156
|
+
return snapshot.overall === 'healthy' ? 0 : 1;
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
return invalid(error.message);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
126
162
|
// Validates --agent separately from the general diagnostic gathering below:
|
|
127
163
|
// an unknown or disabled agent is a normal input-validation failure (the
|
|
128
164
|
// user typo'd or forgot `awm init --agent <x>`), not an "internal error" —
|
|
@@ -159,8 +195,11 @@ function registerDoctorCommand(program) {
|
|
|
159
195
|
program.command('doctor')
|
|
160
196
|
.description('Read-only dashboard of the AWM harness state, per provider')
|
|
161
197
|
.option('--json', 'Emit the diagnostic report as JSON')
|
|
198
|
+
.option('--full', 'Emit the full dashboard snapshot')
|
|
199
|
+
.option('--html [file]', 'Write the dashboard snapshot as HTML')
|
|
200
|
+
.option('--force', 'Allow replacing an existing HTML file')
|
|
162
201
|
.option('-a, --agent <agent>', 'Target agent subset (comma-separated); defaults to every enabled agent')
|
|
163
202
|
.action((options) => {
|
|
164
|
-
process.exitCode = runDoctor({ json: options.json, agent: options.agent });
|
|
203
|
+
process.exitCode = runDoctor({ json: options.json, full: options.full, html: typeof options.html === 'string' ? options.html : options.html === true ? '' : undefined, force: options.force, agent: options.agent });
|
|
165
204
|
});
|
|
166
205
|
}
|
|
@@ -19,6 +19,43 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
19
19
|
const path_1 = __importDefault(require("path"));
|
|
20
20
|
const providers_1 = require("../../providers");
|
|
21
21
|
const shared_1 = require("./shared");
|
|
22
|
+
const provider_1 = require("../../core/context/provider");
|
|
23
|
+
const orchestrators_1 = require("../../core/orchestrators");
|
|
24
|
+
/**
|
|
25
|
+
* Materializa el payload compuesto (using-awm + orquestadores declarados) en `skillDest`.
|
|
26
|
+
*
|
|
27
|
+
* Antes `using-awm.md` era un symlink al SKILL.md crudo del registry: todo lo que
|
|
28
|
+
* `buildContext` compone (declared orchestrators — Tasks 1-4) nunca llegaba a Claude
|
|
29
|
+
* Code, el proveedor PRINCIPAL que este framework existe para servir. Se escribe el
|
|
30
|
+
* archivo en vez de enlazarlo porque el contenido ya no es un archivo del registry sino
|
|
31
|
+
* un derivado suyo — igual que `materialize()` hace para los demas proveedores
|
|
32
|
+
* (core/context/materializer.ts), solo que aca el destino es el propio scriptsDir del
|
|
33
|
+
* hook en vez del awm-context.md generico.
|
|
34
|
+
*
|
|
35
|
+
* Usada por `installClaudeHook` Y por `resyncClaudeHookFiles` — con proposito: si solo
|
|
36
|
+
* una de las dos escribiera el payload materializado, la otra seguiria symlinkeando al
|
|
37
|
+
* SKILL.md crudo y el siguiente `awm update` reabriria el mismo bypass que esto cierra.
|
|
38
|
+
*
|
|
39
|
+
* Escritura atomica via write-then-rename (post-implementation-qa Finding 1, TOCTOU):
|
|
40
|
+
* el patron previo (`unlinkSync` seguido de `writeFileSync`) dejaba una ventana entre
|
|
41
|
+
* ambas llamadas donde un symlink recreado en `skillDest` seria seguido por
|
|
42
|
+
* `writeFileSync` (que no usa `O_EXCL`), escribiendo a traves de el sobre lo que sea
|
|
43
|
+
* que apunte. Escribir a un temporal unico en el MISMO directorio (para que el rename
|
|
44
|
+
* quede en el mismo filesystem y sea atomico) y luego `renameSync` al destino evita eso
|
|
45
|
+
* por completo: el rename reemplaza la entrada de directorio de forma atomica sin
|
|
46
|
+
* dereferenciar un symlink preexistente en el destino, y sin ventana entre borrar y
|
|
47
|
+
* escribir — no hace falta unlink previo, el rename ya sobrescribe en un solo paso.
|
|
48
|
+
*/
|
|
49
|
+
function writeMaterializedSkill(skillDest, registryRoot) {
|
|
50
|
+
const ctx = (0, provider_1.buildContext)({
|
|
51
|
+
registryRoot,
|
|
52
|
+
profileExtensions: [],
|
|
53
|
+
declaredOrchestrators: (0, orchestrators_1.collectAndWarn)(),
|
|
54
|
+
});
|
|
55
|
+
const tmpPath = path_1.default.join(path_1.default.dirname(skillDest), `.using-awm.md.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
56
|
+
fs_1.default.writeFileSync(tmpPath, ctx.markdown, 'utf-8');
|
|
57
|
+
fs_1.default.renameSync(tmpPath, skillDest);
|
|
58
|
+
}
|
|
22
59
|
function isAwmEntry(entry, scriptsDir, matcher) {
|
|
23
60
|
return (entry?.matcher === matcher &&
|
|
24
61
|
Array.isArray(entry?.hooks) &&
|
|
@@ -39,19 +76,10 @@ function installClaudeHook(options) {
|
|
|
39
76
|
fs_1.default.mkdirSync(config.scriptsDir, { recursive: true });
|
|
40
77
|
(0, shared_1.syncExecutable)(path_1.default.join(sourceHooks, 'session-start'), path_1.default.join(config.scriptsDir, 'session-start'), options.installMethod);
|
|
41
78
|
(0, shared_1.syncExecutable)(path_1.default.join(sourceHooks, 'run-hook.cmd'), path_1.default.join(config.scriptsDir, 'run-hook.cmd'), options.installMethod);
|
|
42
|
-
// 3.
|
|
79
|
+
// 3. Materializar el payload compuesto (using-awm + orquestadores declarados) — ver
|
|
80
|
+
// writeMaterializedSkill arriba para por que esto ya no es un symlink al SKILL.md crudo.
|
|
43
81
|
const skillDest = path_1.default.join(config.scriptsDir, 'using-awm.md');
|
|
44
|
-
|
|
45
|
-
fs_1.default.unlinkSync(skillDest);
|
|
46
|
-
}
|
|
47
|
-
catch { /* not exists */ }
|
|
48
|
-
try {
|
|
49
|
-
fs_1.default.symlinkSync(sourceSkill, skillDest, 'file'); // ver shared.ts: el tipo no se infiere
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
// best-effort: copy the single skill file; 'awm update' will not auto-propagate
|
|
53
|
-
fs_1.default.copyFileSync(sourceSkill, skillDest);
|
|
54
|
-
}
|
|
82
|
+
writeMaterializedSkill(skillDest, options.registryRoot);
|
|
55
83
|
// 4. Backup settings if it exists
|
|
56
84
|
const backupPath = (0, shared_1.backupManagedFile)(config.settingsPath);
|
|
57
85
|
// 5. Read or initialize settings
|
|
@@ -178,29 +206,17 @@ function uninstallClaudeHook(agent) {
|
|
|
178
206
|
/** Refresh the Claude hook's script/skill files in place (used by resync). Assumes the caller has verified the settings entry is already present. */
|
|
179
207
|
function resyncClaudeHookFiles(config, registryRoot, method) {
|
|
180
208
|
const sourceHooks = path_1.default.join(registryRoot, 'hooks');
|
|
181
|
-
const sourceSkill = path_1.default.join(registryRoot, 'skills/using-awm/SKILL.md');
|
|
182
209
|
fs_1.default.mkdirSync(config.scriptsDir, { recursive: true });
|
|
183
210
|
(0, shared_1.syncExecutable)(path_1.default.join(sourceHooks, 'session-start'), path_1.default.join(config.scriptsDir, 'session-start'), method);
|
|
184
211
|
(0, shared_1.syncExecutable)(path_1.default.join(sourceHooks, 'run-hook.cmd'), path_1.default.join(config.scriptsDir, 'run-hook.cmd'), method);
|
|
212
|
+
// Re-materializar el payload compuesto igual que `installClaudeHook` (arriba) — NO
|
|
213
|
+
// volver a symlinkear al SKILL.md crudo. Antes este era el segundo escritor del mismo
|
|
214
|
+
// archivo con el symlink-con-fallback-a-copia; si solo `installClaudeHook` se hubiera
|
|
215
|
+
// arreglado y este no, el primer `awm update` posterior a un install correcto habria
|
|
216
|
+
// sobreescrito el payload materializado con un symlink crudo de nuevo — reabriendo el
|
|
217
|
+
// bypass que esto existe para cerrar.
|
|
185
218
|
const skillDest = path_1.default.join(config.scriptsDir, 'using-awm.md');
|
|
186
|
-
|
|
187
|
-
fs_1.default.unlinkSync(skillDest);
|
|
188
|
-
}
|
|
189
|
-
catch { /* not exists */ }
|
|
190
|
-
// Mismo fallback a copia que `installClaudeHook` (arriba). Sin el, en
|
|
191
|
-
// Windows sin Developer Mode este symlink tira EPERM, `resyncInstalledHooks`
|
|
192
|
-
// propaga el throw y `awm update` devuelve 1 — PARA SIEMPRE: el install
|
|
193
|
-
// funcionaba (tenia el fallback) y el update no, en una plataforma que la
|
|
194
|
-
// matriz de soporte declara verificada en CI. Dos escritores del mismo
|
|
195
|
-
// archivo, solo uno endurecido.
|
|
196
|
-
try {
|
|
197
|
-
fs_1.default.symlinkSync(sourceSkill, skillDest, 'file'); // ver shared.ts: el tipo no se infiere
|
|
198
|
-
}
|
|
199
|
-
catch {
|
|
200
|
-
// best-effort: `awm update` no auto-propagara cambios de esta skill,
|
|
201
|
-
// pero el hook queda funcional en vez de dejar el comando inservible.
|
|
202
|
-
fs_1.default.copyFileSync(sourceSkill, skillDest);
|
|
203
|
-
}
|
|
219
|
+
writeMaterializedSkill(skillDest, registryRoot);
|
|
204
220
|
}
|
|
205
221
|
/** True when the registry has everything needed to resync the Claude hook files. */
|
|
206
222
|
function claudeResyncSourcesExist(registryRoot) {
|
|
@@ -58,7 +58,7 @@ function registerHooksCommand(program) {
|
|
|
58
58
|
console.log(` Scripts: ${result.scriptsDir}/session-start`);
|
|
59
59
|
if (agent === 'claude-code') {
|
|
60
60
|
console.log(` ${result.scriptsDir}/run-hook.cmd`);
|
|
61
|
-
console.log(` ${result.scriptsDir}/using-awm.md
|
|
61
|
+
console.log(` ${result.scriptsDir}/using-awm.md (composed context, written)`);
|
|
62
62
|
}
|
|
63
63
|
console.log('');
|
|
64
64
|
console.log(` Settings file: ${result.settingsPath}`);
|
|
@@ -12,6 +12,7 @@ const simple_git_1 = __importDefault(require("simple-git"));
|
|
|
12
12
|
const registries_1 = require("../../core/registries");
|
|
13
13
|
const discovery_1 = require("../../core/discovery");
|
|
14
14
|
const bundles_1 = require("../../core/bundles");
|
|
15
|
+
const orchestrators_1 = require("../../core/orchestrators");
|
|
15
16
|
function deriveRegistryName(remote) {
|
|
16
17
|
// Split on '/', '\' and ':' — not just '/' and ':'. A git remote URL
|
|
17
18
|
// (https://…/repo.git, git@host:org/repo.git) only ever uses the first
|
|
@@ -66,6 +67,10 @@ async function addRegistry(remote, nameOverride) {
|
|
|
66
67
|
fs_1.default.rmSync(dest, { recursive: true, force: true });
|
|
67
68
|
return { ok: false, name, error: e instanceof Error ? e.message : String(e) };
|
|
68
69
|
}
|
|
70
|
+
// Una declaracion de orquestador malformada se REPORTA, no aborta: el
|
|
71
|
+
// registry puede aportar skills utiles aunque su declaracion este rota,
|
|
72
|
+
// y abortar por eso invalidaria contenido sano (R1.2).
|
|
73
|
+
const { diagnostics: orchestratorDiagnostics } = (0, orchestrators_1.readDeclaredOrchestrators)(dest);
|
|
69
74
|
(0, registries_1.writeRegistriesConfig)([...existing, { name, remote }]);
|
|
70
|
-
return { ok: true, name, contentRoot: dest };
|
|
75
|
+
return { ok: true, name, contentRoot: dest, orchestratorDiagnostics };
|
|
71
76
|
}
|
|
@@ -38,6 +38,9 @@ function registerRegistryCommand(program) {
|
|
|
38
38
|
process.exit(1);
|
|
39
39
|
}
|
|
40
40
|
s.stop(`Registry ${picocolors_1.default.cyan(result.name)} added at ${result.contentRoot}`);
|
|
41
|
+
for (const d of result.orchestratorDiagnostics) {
|
|
42
|
+
console.warn(picocolors_1.default.yellow(` ⚠ ${d}`));
|
|
43
|
+
}
|
|
41
44
|
try {
|
|
42
45
|
(0, regenerate_1.regenerateGlobalContext)();
|
|
43
46
|
}
|
|
@@ -8,6 +8,7 @@ const config_instructions_1 = require("./strategies/config-instructions");
|
|
|
8
8
|
const codex_agents_1 = require("./strategies/codex-agents");
|
|
9
9
|
const provider_1 = require("./provider");
|
|
10
10
|
const materializer_1 = require("./materializer");
|
|
11
|
+
const orchestrators_1 = require("../orchestrators");
|
|
11
12
|
class InjectionOrchestrator {
|
|
12
13
|
overrides;
|
|
13
14
|
constructor(overrides = {}) {
|
|
@@ -41,7 +42,11 @@ class InjectionOrchestrator {
|
|
|
41
42
|
}
|
|
42
43
|
/** Full input: builds context from registry and materializes to disk. Used by installContext only. */
|
|
43
44
|
inputFor(op) {
|
|
44
|
-
const ctx = (0, provider_1.buildContext)({
|
|
45
|
+
const ctx = (0, provider_1.buildContext)({
|
|
46
|
+
registryRoot: op.registryRoot,
|
|
47
|
+
profileExtensions: op.profileExtensions,
|
|
48
|
+
declaredOrchestrators: (0, orchestrators_1.collectAndWarn)(),
|
|
49
|
+
});
|
|
45
50
|
const absPath = this.contextPathFor(op);
|
|
46
51
|
const ref = (0, materializer_1.materialize)(ctx, absPath, op.scope);
|
|
47
52
|
return {
|
|
@@ -66,7 +71,15 @@ class InjectionOrchestrator {
|
|
|
66
71
|
const absPath = this.contextPathFor(op);
|
|
67
72
|
let contentHash = '';
|
|
68
73
|
try {
|
|
69
|
-
|
|
74
|
+
// Debe recolectar declarados igual que inputFor: si no, el hash "esperado" aqui
|
|
75
|
+
// diverge del hash realmente materializado por installContext en cuanto algun
|
|
76
|
+
// registry instalado declare un orquestador, y contextStatus reportaria 'stale'
|
|
77
|
+
// de forma permanente incluso justo despues de un install correcto.
|
|
78
|
+
const ctx = (0, provider_1.buildContext)({
|
|
79
|
+
registryRoot: op.registryRoot,
|
|
80
|
+
profileExtensions: op.profileExtensions,
|
|
81
|
+
declaredOrchestrators: (0, orchestrators_1.collectAndWarn)(),
|
|
82
|
+
});
|
|
70
83
|
contentHash = ctx.contentHash;
|
|
71
84
|
}
|
|
72
85
|
catch (err) {
|
|
@@ -16,6 +16,33 @@ function parseVersion(skill) {
|
|
|
16
16
|
const m = skill.match(/^version:\s*["']?([^"'\n]+)["']?\s*$/m);
|
|
17
17
|
return m ? m[1].trim() : '0.0.0';
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Neutraliza contenido no confiable proveniente de registries declarados
|
|
21
|
+
* (name/appliesWhen/terminatesTo) antes de interpolarlo en markdown.
|
|
22
|
+
* Sin esto, un registry malicioso/comprometido podria inyectar saltos de
|
|
23
|
+
* linea, marcadores markdown (##, `, *, _) o pseudo-tags XML/HTML (<, >)
|
|
24
|
+
* para forjar una seccion nueva o un bloque instruccional dentro del
|
|
25
|
+
* payload de contexto que consume el proveedor de IA — un vector de
|
|
26
|
+
* prompt-injection. `readDeclaredOrchestrators` solo valida que los
|
|
27
|
+
* campos sean strings no vacios; el saneo pertenece a esta frontera de
|
|
28
|
+
* render, no a la validacion de lectura.
|
|
29
|
+
*/
|
|
30
|
+
function sanitizeForMarkdown(s) {
|
|
31
|
+
return s.replace(/\r?\n/g, ' ').replace(/[`*_#<>]/g, '');
|
|
32
|
+
}
|
|
33
|
+
function renderDeclared(list) {
|
|
34
|
+
if (list.length === 0)
|
|
35
|
+
return '';
|
|
36
|
+
const rows = list
|
|
37
|
+
.map(o => {
|
|
38
|
+
const name = sanitizeForMarkdown(o.name);
|
|
39
|
+
const appliesWhen = sanitizeForMarkdown(o.appliesWhen);
|
|
40
|
+
const terminatesTo = sanitizeForMarkdown(o.terminatesTo);
|
|
41
|
+
return `- **${name}** — applies when: ${appliesWhen}. Terminates to: \`${terminatesTo}\`.`;
|
|
42
|
+
})
|
|
43
|
+
.join('\n');
|
|
44
|
+
return `## Declared orchestrators\n\nConsider these before the built-in pair:\n\n${rows}\n\n`;
|
|
45
|
+
}
|
|
19
46
|
function buildContext(input) {
|
|
20
47
|
const skillPath = path_1.default.join(input.registryRoot, 'skills/using-awm/SKILL.md');
|
|
21
48
|
if (!fs_1.default.existsSync(skillPath)) {
|
|
@@ -24,6 +51,7 @@ function buildContext(input) {
|
|
|
24
51
|
const skill = fs_1.default.readFileSync(skillPath, 'utf-8');
|
|
25
52
|
const exts = input.profileExtensions.length ? input.profileExtensions.join(', ') : 'none';
|
|
26
53
|
const header = `<!-- AWM context (generated) -->\n# AWM\n\nActive extensions: ${exts}\n\n`;
|
|
27
|
-
const
|
|
54
|
+
const declared = renderDeclared(input.declaredOrchestrators ?? []);
|
|
55
|
+
const markdown = header + declared + skill;
|
|
28
56
|
return { markdown, sourceVersion: parseVersion(skill), contentHash: sha256(markdown) };
|
|
29
57
|
}
|
|
@@ -13,11 +13,28 @@ exports.regenerateGlobalContext = regenerateGlobalContext;
|
|
|
13
13
|
const fs_1 = __importDefault(require("fs"));
|
|
14
14
|
const providers_1 = require("../../providers");
|
|
15
15
|
const registries_1 = require("../registries");
|
|
16
|
+
const orchestrators_1 = require("../orchestrators");
|
|
16
17
|
const orchestrator_1 = require("./orchestrator");
|
|
17
18
|
function regenerateGlobalContext(targets = [...providers_1.AGENT_TARGETS], orch = new orchestrator_1.InjectionOrchestrator()) {
|
|
18
19
|
const skillsRoot = (0, registries_1.capabilityRoot)('skills');
|
|
19
20
|
if (!skillsRoot)
|
|
20
21
|
return [];
|
|
22
|
+
// Print each declared-orchestrator diagnostic ONCE up front, rather than letting
|
|
23
|
+
// every agent's contextStatus/installContext call independently re-collect and
|
|
24
|
+
// re-warn via InjectionOrchestrator.inputFor/statusInputFor (both call
|
|
25
|
+
// orchestrators.ts's collectAndWarn() internally, unaware of this outer loop). A
|
|
26
|
+
// single `awm update` touching N agents (e.g. opencode + codex) would otherwise
|
|
27
|
+
// print the same diagnostic up to 2x per agent (once from statusInputFor, once
|
|
28
|
+
// from inputFor when stale) — noisy but not incorrect, since no data is lost or
|
|
29
|
+
// wrong. This only reduces the printing done BY THIS FUNCTION to one line per
|
|
30
|
+
// diagnostic; inputFor/statusInputFor still call collectAndWarn() internally per
|
|
31
|
+
// op (they need the declared list to build/hash context), so a per-agent
|
|
32
|
+
// duplicate can still print alongside this upfront one. Deeper dedup would mean
|
|
33
|
+
// threading a pre-collected list through ContextOp/InjectionOrchestrator, which
|
|
34
|
+
// touches shared collection-scope logic other callers (hooks/claude.ts) rely on —
|
|
35
|
+
// out of proportion for this minor finding.
|
|
36
|
+
for (const d of (0, orchestrators_1.collectDeclaredOrchestrators)().diagnostics)
|
|
37
|
+
console.warn(`warning: ${d}`);
|
|
21
38
|
const out = [];
|
|
22
39
|
for (const agent of targets) {
|
|
23
40
|
const inj = (0, providers_1.providerFor)(agent).injection;
|
|
@@ -42,6 +59,18 @@ function regenerateGlobalContext(targets = [...providers_1.AGENT_TARGETS], orch
|
|
|
42
59
|
if (inj.globalPath === null)
|
|
43
60
|
continue;
|
|
44
61
|
}
|
|
62
|
+
else if (inj.type === 'cc-settings-merge') {
|
|
63
|
+
// Claude Code no pasa por este dispatcher generico: su contexto se
|
|
64
|
+
// materializa via el hook dedicado (hooks/claude.ts's installClaudeHook/
|
|
65
|
+
// resyncClaudeHookFiles), invocado por `awm update` a traves de un path
|
|
66
|
+
// separado (hooks/resync.ts's resyncInstalledHooks). Mismo salteo que
|
|
67
|
+
// stepContextInjection (init/steps.ts) y contextGlobalCheck
|
|
68
|
+
// (diagnostics/provider-checks.ts) — "covered by hook". Sin este
|
|
69
|
+
// continue, orch.contextStatus/installContext corrian igual para
|
|
70
|
+
// claude-code y escribian un ~/.awm/context/awm-context.md huerfano que
|
|
71
|
+
// nadie lee (el archivo real vive en el scriptsDir del hook).
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
45
74
|
const op = {
|
|
46
75
|
agent,
|
|
47
76
|
scope: 'global',
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.REMEDIATION_BY_FINDING_ID = void 0;
|
|
4
|
+
exports.productionDashboardAdapters = productionDashboardAdapters;
|
|
5
|
+
exports.collectDashboardSnapshot = collectDashboardSnapshot;
|
|
6
|
+
const profile_1 = require("../profile");
|
|
7
|
+
const sanitize_1 = require("./sanitize");
|
|
8
|
+
const validate_1 = require("./validate");
|
|
9
|
+
const plan_state_1 = require("./plan-state");
|
|
10
|
+
exports.REMEDIATION_BY_FINDING_ID = {
|
|
11
|
+
'machine.preferences.missing': 'awm init',
|
|
12
|
+
'machine.registries.stale': 'awm update',
|
|
13
|
+
'project.profile.missing': 'awm init',
|
|
14
|
+
'project.sensors.unavailable': 'awm sensors status',
|
|
15
|
+
'project.preflight.degraded': 'awm preflight',
|
|
16
|
+
'planning.source.unavailable': 'awm preflight',
|
|
17
|
+
'execution.source.unavailable': 'awm sensors status',
|
|
18
|
+
};
|
|
19
|
+
const EMPTY_ADAPTERS = {
|
|
20
|
+
machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined,
|
|
21
|
+
};
|
|
22
|
+
const SAFE_REMEDIATIONS = new Set(['awm init', 'awm update', 'awm sync', 'awm sensors status', 'awm preflight']);
|
|
23
|
+
const HEALTHY_PROVIDER_STATES = new Set(['supported', 'healthy', 'shared', 'delivered']);
|
|
24
|
+
const INAPPLICABLE_PROVIDER_STATES = new Set(['unsupported']);
|
|
25
|
+
function providerState(state) {
|
|
26
|
+
if (HEALTHY_PROVIDER_STATES.has(state))
|
|
27
|
+
return 'ok';
|
|
28
|
+
if (INAPPLICABLE_PROVIDER_STATES.has(state))
|
|
29
|
+
return 'not_applicable';
|
|
30
|
+
return 'attention';
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Maps the already-gathered diagnostics matrix into the snapshot's read-only
|
|
34
|
+
* source seam. It deliberately does not re-read providers, execute sensors, or
|
|
35
|
+
* relay provider detail/remediation prose: those values can contain local paths,
|
|
36
|
+
* command output, or credentials. The fixed IDs are derived solely from the
|
|
37
|
+
* provider/check enums and therefore remain stable across runs.
|
|
38
|
+
*/
|
|
39
|
+
function productionDashboardAdapters(context) {
|
|
40
|
+
if (!context || typeof context !== 'object' || !Array.isArray(context.providers))
|
|
41
|
+
throw new Error('productionDashboardAdapters requires gathered provider diagnostics');
|
|
42
|
+
const machineFindings = context.providers.flatMap((provider) => provider.checks.flatMap((check) => {
|
|
43
|
+
const finding = {
|
|
44
|
+
id: `machine.provider.${provider.id}.${check.id}`,
|
|
45
|
+
// Provider labels are configuration prose. The provider id and check id
|
|
46
|
+
// are enum-controlled and sufficient for a stable public observation.
|
|
47
|
+
label: `Provider ${provider.id}: ${check.id}`,
|
|
48
|
+
state: providerState(check.state),
|
|
49
|
+
// Provider remediation is intentionally not forwarded. It is free-form
|
|
50
|
+
// diagnostics text, not a dashboard-approved canonical command.
|
|
51
|
+
remediationVerified: false,
|
|
52
|
+
};
|
|
53
|
+
// These two legacy diagnosis states are the only provider observations
|
|
54
|
+
// with a pre-existing, exact dashboard command mapping.
|
|
55
|
+
if (check.id === 'skills.global' && check.state === 'absent') {
|
|
56
|
+
return [finding, { id: 'machine.preferences.missing', label: 'Preferences', state: 'missing', remediationVerified: true }];
|
|
57
|
+
}
|
|
58
|
+
if (check.id === 'skills.global' && check.state === 'stale') {
|
|
59
|
+
return [finding, { id: 'machine.registries.stale', label: 'Registries', state: 'attention', remediationVerified: true }];
|
|
60
|
+
}
|
|
61
|
+
return [finding];
|
|
62
|
+
}));
|
|
63
|
+
const project = context.project;
|
|
64
|
+
const projectFindings = !project ? [] : [
|
|
65
|
+
{ id: project.profile.present ? 'project.profile.present' : 'project.profile.missing', label: 'Profile', state: project.profile.present ? 'ok' : 'missing', remediation: 'awm init', remediationVerified: true },
|
|
66
|
+
{ id: 'project.extensions.configured', label: 'Extensions', state: project.profile.extensions.length > 0 ? 'ok' : 'not_applicable' },
|
|
67
|
+
{ id: 'project.registry-pins.present', label: 'Registry pins', state: project.profile.registries && Object.keys(project.profile.registries).length > 0 ? 'ok' : 'not_applicable' },
|
|
68
|
+
{ id: 'project.bundles.coherent', label: 'Active bundles', state: project.activeBundles.broken.length === 0 ? 'ok' : 'attention', remediation: 'awm sync', remediationVerified: true },
|
|
69
|
+
{ id: 'project.context.present', label: 'Project context', state: project.context.present ? 'ok' : 'missing', remediation: 'awm init', remediationVerified: true },
|
|
70
|
+
{ id: 'project.constitution.present', label: 'Constitution', state: project.constitution.present ? 'ok' : 'missing' },
|
|
71
|
+
{ id: project.sensors.present ? 'project.sensors.present' : 'project.sensors.unavailable', label: 'Sensors', state: project.sensors.present ? 'ok' : 'unavailable', remediation: 'awm sensors status', remediationVerified: true },
|
|
72
|
+
// `preflight()` is async because static tool inspection is async. Doctor's
|
|
73
|
+
// synchronous legacy API must not dispatch it here; make the absence of that
|
|
74
|
+
// observation explicit rather than inventing a readiness verdict.
|
|
75
|
+
{ id: 'project.preflight.not_collected', label: 'Static preflight', state: 'not_applicable' },
|
|
76
|
+
];
|
|
77
|
+
return {
|
|
78
|
+
machine: () => ({ findings: machineFindings }),
|
|
79
|
+
project: () => ({ label: 'Project detected', findings: projectFindings }),
|
|
80
|
+
plans: () => [],
|
|
81
|
+
execution: () => undefined,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function findings(items, optional = false) {
|
|
85
|
+
if (optional && items === undefined)
|
|
86
|
+
return [];
|
|
87
|
+
if (!Array.isArray(items))
|
|
88
|
+
throw new Error('Dashboard findings must be an array');
|
|
89
|
+
return items.flatMap((item) => {
|
|
90
|
+
if (!item || typeof item !== 'object' || typeof item.id !== 'string' || item.id.trim() === '' || typeof item.label !== 'string' || item.label.trim() === '')
|
|
91
|
+
throw new Error('Dashboard finding is invalid');
|
|
92
|
+
if (!['ok', 'attention', 'missing', 'unavailable', 'not_applicable'].includes(item.state))
|
|
93
|
+
throw new Error('Dashboard finding state is invalid');
|
|
94
|
+
const remediation = exports.REMEDIATION_BY_FINDING_ID[item.id]
|
|
95
|
+
?? (item.remediationVerified === true && typeof item.remediation === 'string' && SAFE_REMEDIATIONS.has(item.remediation) ? item.remediation : undefined);
|
|
96
|
+
if (item.state !== 'ok' && item.state !== 'not_applicable' && !remediation)
|
|
97
|
+
return [];
|
|
98
|
+
return [{ id: item.id, label: item.label, state: item.state, ...(item.detail ? { detail: item.detail } : {}), ...(remediation ? { remediation } : {}) }];
|
|
99
|
+
}).sort((left, right) => left.id.localeCompare(right.id));
|
|
100
|
+
}
|
|
101
|
+
function section(id, availability, items = []) {
|
|
102
|
+
return { id, availability, items };
|
|
103
|
+
}
|
|
104
|
+
function optional(source) {
|
|
105
|
+
try {
|
|
106
|
+
return { value: source(), failed: false, failure: {} };
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
const findingId = error && typeof error === 'object' && typeof error.findingId === 'string'
|
|
110
|
+
? error.findingId : undefined;
|
|
111
|
+
const remediationVerified = error !== null && typeof error === 'object'
|
|
112
|
+
? error.remediationVerified === true : false;
|
|
113
|
+
return { failed: true, failure: { findingId, remediationVerified } };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function canonicalOptionalFailure(failure) {
|
|
117
|
+
if (!failure.remediationVerified || !failure.findingId || !Object.hasOwn(exports.REMEDIATION_BY_FINDING_ID, failure.findingId))
|
|
118
|
+
return [];
|
|
119
|
+
return [{ id: failure.findingId, label: 'Optional source unavailable', state: 'unavailable', remediation: exports.REMEDIATION_BY_FINDING_ID[failure.findingId] }];
|
|
120
|
+
}
|
|
121
|
+
function isolatedFindings(items) {
|
|
122
|
+
return optional(() => findings(items, true));
|
|
123
|
+
}
|
|
124
|
+
/** Pure read-only aggregation over injected source adapters. */
|
|
125
|
+
function collectDashboardSnapshot(options) {
|
|
126
|
+
if (!options || typeof options.cwd !== 'string' || options.cwd.length === 0 || typeof options.now !== 'string' || Number.isNaN(Date.parse(options.now)))
|
|
127
|
+
throw new Error('collectDashboardSnapshot requires cwd and valid now');
|
|
128
|
+
const adapters = { ...EMPTY_ADAPTERS, ...(options.adapters ?? {}) };
|
|
129
|
+
const machine = adapters.machine({ cwd: options.cwd });
|
|
130
|
+
if (!Array.isArray(machine?.findings))
|
|
131
|
+
throw new Error('Dashboard findings must be an array');
|
|
132
|
+
const root = (0, profile_1.findProjectRoot)(options.cwd);
|
|
133
|
+
const machineItems = findings((0, sanitize_1.sanitizeDashboardSource)(machine).findings);
|
|
134
|
+
const machineSection = section('machine', 'available', machineItems);
|
|
135
|
+
if (!root) {
|
|
136
|
+
const degraded = machineSection.items.some((item) => item.state !== 'ok' && item.state !== 'not_applicable');
|
|
137
|
+
return (0, validate_1.validateDashboardSnapshotV1)({ schema: 1, generatedAt: options.now, overall: degraded ? 'degraded' : 'healthy', project: { detected: false, label: 'No project detected' }, confidence: 'none', sections: [machineSection] });
|
|
138
|
+
}
|
|
139
|
+
const projectResult = optional(() => (0, sanitize_1.sanitizeDashboardSource)(adapters.project({ root })));
|
|
140
|
+
const plansResult = optional(() => (0, sanitize_1.sanitizeDashboardSource)(adapters.plans({ root })));
|
|
141
|
+
const executionResult = optional(() => {
|
|
142
|
+
const source = adapters.execution({ root });
|
|
143
|
+
return source === undefined ? undefined : (0, sanitize_1.sanitizeDashboardSource)(source);
|
|
144
|
+
});
|
|
145
|
+
const projectSource = projectResult.value;
|
|
146
|
+
const execution = executionResult.value;
|
|
147
|
+
const projectItemsResult = projectSource ? isolatedFindings(projectSource.findings) : { value: [], failed: false, failure: {} };
|
|
148
|
+
const planItemsResult = plansResult.value ? optional(() => findings(plansResult.value.map((plan) => plan.lifecycle
|
|
149
|
+
? { ...plan, detail: (0, plan_state_1.classifyPlanState)(plan.lifecycle) } : plan))) : { value: [], failed: false, failure: {} };
|
|
150
|
+
const executionItems = isolatedFindings(execution?.execution);
|
|
151
|
+
const qaItems = isolatedFindings(execution?.qa);
|
|
152
|
+
const retroItems = isolatedFindings(execution?.retro);
|
|
153
|
+
const historyItems = isolatedFindings(execution?.history);
|
|
154
|
+
const executionUnavailable = !executionResult.failed && execution === undefined;
|
|
155
|
+
const sections = [
|
|
156
|
+
machineSection,
|
|
157
|
+
section('project', projectResult.failed || projectItemsResult.failed ? 'unavailable' : 'available', projectResult.failed
|
|
158
|
+
? canonicalOptionalFailure(projectResult.failure) : projectItemsResult.failed ? [] : projectItemsResult.value),
|
|
159
|
+
section('planning', plansResult.failed || planItemsResult.failed ? 'unavailable' : 'available', plansResult.failed
|
|
160
|
+
? canonicalOptionalFailure(plansResult.failure) : planItemsResult.failed ? [] : planItemsResult.value),
|
|
161
|
+
section('execution', executionResult.failed || executionUnavailable || executionItems.failed ? 'unavailable' : 'available', executionResult.failed ? canonicalOptionalFailure(executionResult.failure) : executionUnavailable ? canonicalOptionalFailure({ findingId: 'execution.source.unavailable', remediationVerified: true }) : executionItems.value),
|
|
162
|
+
// There is no read-only QA, retro, or history adapter in Release A. An
|
|
163
|
+
// absent execution source is not evidence of a successful empty cycle.
|
|
164
|
+
section('qa', executionResult.failed || executionUnavailable || qaItems.failed ? 'unavailable' : 'available', qaItems.value),
|
|
165
|
+
section('retro', executionResult.failed || executionUnavailable || retroItems.failed ? 'unavailable' : 'available', retroItems.value),
|
|
166
|
+
section('history', executionResult.failed || executionUnavailable || historyItems.failed ? 'unavailable' : 'available', historyItems.value),
|
|
167
|
+
];
|
|
168
|
+
const degraded = sections.some((entry) => entry.availability === 'unavailable' || entry.items.some((item) => item.state !== 'ok' && item.state !== 'not_applicable'));
|
|
169
|
+
return (0, validate_1.validateDashboardSnapshotV1)({ schema: 1, generatedAt: options.now, overall: degraded ? 'degraded' : 'healthy', project: { detected: true, label: projectSource?.label || 'Project detected' }, confidence: 'provisional', sections });
|
|
170
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.classifyPlanState = classifyPlanState;
|
|
4
|
+
function assertRecord(value, label) {
|
|
5
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
6
|
+
throw new Error(`Plan ${label} must be an object`);
|
|
7
|
+
}
|
|
8
|
+
function assertKeys(value, label, keys) {
|
|
9
|
+
if (Object.keys(value).some((key) => !keys.includes(key)))
|
|
10
|
+
throw new Error(`Plan ${label} has unsupported fields`);
|
|
11
|
+
}
|
|
12
|
+
function classifyPlanState(input) {
|
|
13
|
+
assertRecord(input, 'state input');
|
|
14
|
+
assertKeys(input, 'state input', ['journal', 'markers', 'tasks']);
|
|
15
|
+
assertRecord(input.markers, 'markers');
|
|
16
|
+
assertKeys(input.markers, 'markers', ['qaComplete', 'retroComplete']);
|
|
17
|
+
assertRecord(input.tasks, 'tasks');
|
|
18
|
+
assertKeys(input.tasks, 'tasks', ['total', 'completed']);
|
|
19
|
+
const markers = input.markers;
|
|
20
|
+
const tasks = input.tasks;
|
|
21
|
+
if (typeof markers.qaComplete !== 'boolean' || typeof markers.retroComplete !== 'boolean')
|
|
22
|
+
throw new Error('Plan markers must be boolean');
|
|
23
|
+
if (typeof tasks.total !== 'number' || typeof tasks.completed !== 'number' || !Number.isInteger(tasks.total) || !Number.isInteger(tasks.completed) || tasks.total < 0 || tasks.completed < 0 || tasks.completed > tasks.total)
|
|
24
|
+
throw new Error('Plan task counts are invalid');
|
|
25
|
+
let journalState;
|
|
26
|
+
if (input.journal !== undefined) {
|
|
27
|
+
assertRecord(input.journal, 'journal');
|
|
28
|
+
assertKeys(input.journal, 'journal', ['state']);
|
|
29
|
+
if (input.journal.state !== 'active' && input.journal.state !== 'blocked')
|
|
30
|
+
throw new Error('Plan journal state is invalid');
|
|
31
|
+
journalState = input.journal.state;
|
|
32
|
+
}
|
|
33
|
+
if (journalState === 'blocked')
|
|
34
|
+
return 'blocked';
|
|
35
|
+
if (journalState === 'active')
|
|
36
|
+
return 'active';
|
|
37
|
+
if (markers.retroComplete)
|
|
38
|
+
return 'executed';
|
|
39
|
+
if (markers.qaComplete)
|
|
40
|
+
return 'retro_pending';
|
|
41
|
+
if (tasks.total > 0 && tasks.completed === tasks.total)
|
|
42
|
+
return 'qa_pending';
|
|
43
|
+
return 'legacy_unverifiable';
|
|
44
|
+
}
|