oficina-opencode 3.0.5 → 3.1.1
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/README.md +240 -208
- package/commands/oficina.md +7 -6
- package/dashboard/.dom-check.html +1257 -0
- package/dashboard/README-DASHBOARD.md +64 -0
- package/dashboard/assets/factory.css +1543 -0
- package/dashboard/assets/tokens.css +208 -0
- package/dashboard/build-kanban-data.mjs +279 -0
- package/dashboard/dashboard.css +753 -0
- package/dashboard/dashboard.html +121 -0
- package/dashboard/dashboard.js +610 -0
- package/dashboard/data/snapshot.json +883 -0
- package/dashboard/debate.html +134 -0
- package/dashboard/factory/build-snapshot.mjs +89 -0
- package/dashboard/factory/factory-app.js +1185 -0
- package/dashboard/factory/factory-core.js +541 -0
- package/dashboard/factory/snapshot.js +850 -0
- package/dashboard/factory.html +350 -0
- package/dashboard/kanban-data.json +671 -0
- package/dashboard/kanban-source.js +643 -0
- package/dashboard/kanban.check.mjs +454 -0
- package/dashboard/kanban.html +1257 -0
- package/dashboard/leadership.html +1008 -0
- package/dashboard/lib/debate-app.js +1149 -0
- package/dashboard/lib/debate-core.js +698 -0
- package/dashboard/lib/debate-data.js +163 -0
- package/dashboard/lib/debate-demo.js +110 -0
- package/dashboard/lib/debate.css +1155 -0
- package/dashboard/lib/leadership-model.js +946 -0
- package/dashboard/probe.mjs +35 -0
- package/dashboard/scripts/serve.mjs +122 -0
- package/dashboard/scripts/snapshot-data.mjs +93 -0
- package/dashboard/scripts/snapshot.mjs +28 -0
- package/dashboard/server.mjs +242 -0
- package/dashboard/shot.mjs +54 -0
- package/dashboard/shots/desktop-gates.png +0 -0
- package/dashboard/shots/desktop-light.png +0 -0
- package/dashboard/shots/desktop-real.png +0 -0
- package/dashboard/shots/mobile.png +0 -0
- package/dashboard/test/factory-core.test.mjs +244 -0
- package/dashboard/tests/debate-core.test.mjs +336 -0
- package/dashboard/tests/leadership-model.test.mjs +404 -0
- package/dist/cli.js +148 -26
- package/dist/cli.js.map +1 -1
- package/dist/core/bus.js +3 -3
- package/dist/core/bus.js.map +1 -1
- package/dist/core/catalog.d.ts +9 -1
- package/dist/core/catalog.d.ts.map +1 -1
- package/dist/core/catalog.js +77 -2
- package/dist/core/catalog.js.map +1 -1
- package/dist/core/dashboard.d.ts +33 -0
- package/dist/core/dashboard.d.ts.map +1 -0
- package/dist/core/dashboard.js +115 -0
- package/dist/core/dashboard.js.map +1 -0
- package/dist/core/dispatch.d.ts +1 -1
- package/dist/core/dispatch.d.ts.map +1 -1
- package/dist/core/dispatch.js +84 -12
- package/dist/core/dispatch.js.map +1 -1
- package/dist/core/docsync.d.ts +0 -9
- package/dist/core/docsync.d.ts.map +1 -1
- package/dist/core/docsync.js +12 -15
- package/dist/core/docsync.js.map +1 -1
- package/dist/core/environment.d.ts +28 -0
- package/dist/core/environment.d.ts.map +1 -0
- package/dist/core/environment.js +80 -0
- package/dist/core/environment.js.map +1 -0
- package/dist/core/factory.d.ts +16 -0
- package/dist/core/factory.d.ts.map +1 -1
- package/dist/core/factory.js +143 -0
- package/dist/core/factory.js.map +1 -1
- package/dist/core/fsx.d.ts +20 -2
- package/dist/core/fsx.d.ts.map +1 -1
- package/dist/core/fsx.js +10 -6
- package/dist/core/fsx.js.map +1 -1
- package/dist/core/gates.d.ts +23 -1
- package/dist/core/gates.d.ts.map +1 -1
- package/dist/core/gates.js +83 -8
- package/dist/core/gates.js.map +1 -1
- package/dist/core/leases.d.ts.map +1 -1
- package/dist/core/leases.js +4 -1
- package/dist/core/leases.js.map +1 -1
- package/dist/core/mesa.d.ts +1 -6
- package/dist/core/mesa.d.ts.map +1 -1
- package/dist/core/mesa.js +1 -6
- package/dist/core/mesa.js.map +1 -1
- package/dist/core/office.d.ts +3 -29
- package/dist/core/office.d.ts.map +1 -1
- package/dist/core/office.js +77 -90
- package/dist/core/office.js.map +1 -1
- package/dist/core/paths.d.ts.map +1 -1
- package/dist/core/paths.js +6 -1
- package/dist/core/paths.js.map +1 -1
- package/dist/core/retry.d.ts +2 -8
- package/dist/core/retry.d.ts.map +1 -1
- package/dist/core/retry.js +3 -34
- package/dist/core/retry.js.map +1 -1
- package/dist/core/roster.js +3 -3
- package/dist/core/roster.js.map +1 -1
- package/dist/core/sandbox.d.ts +38 -1
- package/dist/core/sandbox.d.ts.map +1 -1
- package/dist/core/sandbox.js +119 -15
- package/dist/core/sandbox.js.map +1 -1
- package/dist/core/state.d.ts +2 -2
- package/dist/core/state.d.ts.map +1 -1
- package/dist/core/state.js +5 -33
- package/dist/core/state.js.map +1 -1
- package/dist/core/stores.d.ts +3 -13
- package/dist/core/stores.d.ts.map +1 -1
- package/dist/core/stores.js +8 -32
- package/dist/core/stores.js.map +1 -1
- package/dist/core/systemPrompt.d.ts.map +1 -1
- package/dist/core/systemPrompt.js +4 -1
- package/dist/core/systemPrompt.js.map +1 -1
- package/dist/core/tripwire.d.ts +22 -8
- package/dist/core/tripwire.d.ts.map +1 -1
- package/dist/core/tripwire.js +71 -26
- package/dist/core/tripwire.js.map +1 -1
- package/dist/core/types.d.ts +6 -14
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +11 -3
- package/dist/core/types.js.map +1 -1
- package/dist/hooks/scopeGuard.d.ts +15 -1
- package/dist/hooks/scopeGuard.d.ts.map +1 -1
- package/dist/hooks/scopeGuard.js +88 -73
- package/dist/hooks/scopeGuard.js.map +1 -1
- package/dist/tools/index.d.ts +9 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +29 -81
- package/dist/tools/index.js.bak +972 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/v1.d.ts.map +1 -1
- package/dist/v1.js +30 -36
- package/dist/v1.js.map +1 -1
- package/dist/v2.d.ts +5 -0
- package/dist/v2.d.ts.map +1 -1
- package/dist/v2.js +79 -46
- package/dist/v2.js.bak +517 -0
- package/dist/v2.js.map +1 -1
- package/docs/GARANTIAS.md +2 -2
- package/docs/PRUEBA-E2E.md +4 -4
- package/docs/QUE-ES.md +4 -5
- package/docs/SEGURIDAD-REPO.md +83 -0
- package/docs/assets/banner.svg +42 -0
- package/docs/catalog-facts.json +3 -3
- package/docs/superpowers/specs/2026-10-29-factory-skill-creator-and-remove-budget.md +182 -0
- package/package.json +14 -6
|
@@ -0,0 +1,972 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tools de la OFICINA.
|
|
3
|
+
*
|
|
4
|
+
* La superficie que ve el modelo. Diez tools, no treinta: cada tool que se
|
|
5
|
+
* suma es ruido en el contexto de todos los agentes.
|
|
6
|
+
*
|
|
7
|
+
* Lo que el modelo decide: el plan, el contenido, los veredictos, el triage.
|
|
8
|
+
* Lo que el código decide: los gates, los invariantes, el routing, los
|
|
9
|
+
* contadores, los leases. Esa división es la que hace que el sistema sea
|
|
10
|
+
* determinista donde tiene que serlo y creativo donde tiene que serlo.
|
|
11
|
+
*/
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import { CHANNELS } from "../core/bus.js";
|
|
14
|
+
import { Office, renderFicha } from "../core/office.js";
|
|
15
|
+
import { renderBriefing } from "../core/briefing.js";
|
|
16
|
+
import { dispatchOffice } from "../core/dispatch.js";
|
|
17
|
+
import { snapshotMesa, renderMesa } from "../core/mesa.js";
|
|
18
|
+
/**
|
|
19
|
+
* Helper local: construye una tool en la forma neutra.
|
|
20
|
+
*
|
|
21
|
+
* Es la identidad a proposito. Antes usaba el `tool()` del plugin v1, que
|
|
22
|
+
* ataba el motor a esa API.
|
|
23
|
+
*
|
|
24
|
+
* ## Por que construye el `z.object`
|
|
25
|
+
*
|
|
26
|
+
* El helper de v1 recibia los argumentos como **forma plana**
|
|
27
|
+
* (`{ objective: z.string() }`) y armaba el `z.object` por dentro. El motor
|
|
28
|
+
* esta escrito asi, y es la forma mas comoda de declarar una tool. Este
|
|
29
|
+
* helper mantiene esa ergonomia y guarda el objeto construido, que es lo que
|
|
30
|
+
* despues traduce cada entry (v1 lo pasa tal cual, v2 lo convierte a JSON
|
|
31
|
+
* Schema).
|
|
32
|
+
*/
|
|
33
|
+
function tool(input) {
|
|
34
|
+
return {
|
|
35
|
+
description: input.description,
|
|
36
|
+
args: z.object(input.args),
|
|
37
|
+
execute: input.execute,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const cardTypeSchema = z.enum([
|
|
41
|
+
"exploration",
|
|
42
|
+
"design",
|
|
43
|
+
"implementation",
|
|
44
|
+
"bugfix",
|
|
45
|
+
"refactor",
|
|
46
|
+
"testing",
|
|
47
|
+
"docs",
|
|
48
|
+
"security",
|
|
49
|
+
"infra",
|
|
50
|
+
]);
|
|
51
|
+
export function buildTools(office, hooks = {}) {
|
|
52
|
+
const officeOf = (ctx) => {
|
|
53
|
+
// El directorio del contexto tiene prioridad sobre el cwd del plugin:
|
|
54
|
+
// es el proyecto real en el que se está trabajando.
|
|
55
|
+
if (ctx.directory && ctx.directory !== office.paths.projectRoot) {
|
|
56
|
+
const alt = new Office({ cwd: ctx.directory, limits: office.limits });
|
|
57
|
+
return alt;
|
|
58
|
+
}
|
|
59
|
+
return office;
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
/**
|
|
63
|
+
* Guarda el plan de cards. El modelo propone; el código valida y rutea.
|
|
64
|
+
*/
|
|
65
|
+
oficina_plan: tool({
|
|
66
|
+
description: "Guarda el plan de trabajo de la oficina: descompone el objetivo en cards con acceptance criteria verificables, scope y gate. " +
|
|
67
|
+
"El motor valida el plan, rutea owner y reviewer automáticamente, y persiste el estado. " +
|
|
68
|
+
"Usá esto UNA vez, al inicio de la corrida. Un card = un entregable coherente; si tiene más de 5 criterios de aceptación, partila.",
|
|
69
|
+
args: {
|
|
70
|
+
objective: z.string().describe("El objetivo de la corrida, textual."),
|
|
71
|
+
cards: z
|
|
72
|
+
.array(z.object({
|
|
73
|
+
title: z.string().describe("Verbo + objeto. Específico."),
|
|
74
|
+
type: cardTypeSchema.describe("Tipo de trabajo."),
|
|
75
|
+
requiredCapabilities: z
|
|
76
|
+
.array(z.string())
|
|
77
|
+
.describe("Capabilities que el agente necesita. Se usan para resolver el rol desde el catálogo."),
|
|
78
|
+
scope: z
|
|
79
|
+
.object({
|
|
80
|
+
mayTouch: z.array(z.string()).describe("Globs que el agente puede tocar."),
|
|
81
|
+
forbidden: z.array(z.string()).describe("Globs prohibidos."),
|
|
82
|
+
})
|
|
83
|
+
.optional(),
|
|
84
|
+
acceptance: z
|
|
85
|
+
.array(z.string())
|
|
86
|
+
.describe("Criterios verificables. Nada de 'hecho' genérico."),
|
|
87
|
+
gate: z.string().describe("Condición concreta para pasar a DONE."),
|
|
88
|
+
dependsOn: z.array(z.string()).describe("Ids de cards que deben estar DONE."),
|
|
89
|
+
}))
|
|
90
|
+
.describe("Las cards del plan, en orden de ejecución."),
|
|
91
|
+
},
|
|
92
|
+
async execute(args, ctx) {
|
|
93
|
+
const off = officeOf(ctx);
|
|
94
|
+
await off.init();
|
|
95
|
+
const state = await off.loadState(args.objective);
|
|
96
|
+
const updated = await off.savePlan(state, args.cards);
|
|
97
|
+
// El plan marca el arranque de la corrida: acá se abre el dashboard
|
|
98
|
+
// visual (la URL viaja en la salida para que el modelo la reporte).
|
|
99
|
+
let dashUrl;
|
|
100
|
+
try {
|
|
101
|
+
dashUrl = await hooks.onRunStart?.(updated.runId);
|
|
102
|
+
}
|
|
103
|
+
catch { /* el dashboard es best-effort: no bloquea la corrida */ }
|
|
104
|
+
const lines = [
|
|
105
|
+
`Plan guardado. ${updated.order.length} cards en la corrida.`,
|
|
106
|
+
"",
|
|
107
|
+
...updated.order.map((id) => {
|
|
108
|
+
const c = updated.cards[id];
|
|
109
|
+
return `- **${id}** ${c.title} [${c.type}] owner=${c.owner ?? "(sin owner)"} reviewer=${c.reviewer ?? "(sin reviewer)"} — gate: ${c.gate}`;
|
|
110
|
+
}),
|
|
111
|
+
"",
|
|
112
|
+
`Siguiente paso: llamá \`oficina_assign\` para tomar la primera card.`,
|
|
113
|
+
];
|
|
114
|
+
if (dashUrl)
|
|
115
|
+
lines.push("", `Dashboard en vivo: ${dashUrl} (ya se abrió en tu navegador)`);
|
|
116
|
+
return {
|
|
117
|
+
title: `Plan guardado: ${updated.order.length} cards`,
|
|
118
|
+
output: lines.join("\n"),
|
|
119
|
+
metadata: { runId: updated.runId, cards: updated.order.length, dashboard: dashUrl },
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
}),
|
|
123
|
+
/**
|
|
124
|
+
* Lee el estado de la corrida.
|
|
125
|
+
*/
|
|
126
|
+
oficina_board: tool({
|
|
127
|
+
description: "Muestra el estado de la oficina: cards, sus owners, revisiones pendientes, agentes libres, agentes muertos y presupuesto. " +
|
|
128
|
+
"Usá esto para saber en qué punto estás antes de actuar.",
|
|
129
|
+
args: {
|
|
130
|
+
filter: z.enum(["all", "todo", "in_progress", "review", "done", "blocked", "escalated"]).default("all"),
|
|
131
|
+
},
|
|
132
|
+
async execute(args, ctx) {
|
|
133
|
+
const off = officeOf(ctx);
|
|
134
|
+
const snap = await off.snapshot();
|
|
135
|
+
const { state, roster, idle, working, dead } = snap;
|
|
136
|
+
const cards = Object.values(state.cards).filter((c) => args.filter === "all" || c.state === args.filter);
|
|
137
|
+
const lines = [
|
|
138
|
+
`# OFICINA — ${state.objective}`,
|
|
139
|
+
"",
|
|
140
|
+
`**Estado:** ${state.status} · **iteración:** ${state.iteration} · **cards:** ${Object.keys(state.cards).length}`,
|
|
141
|
+
"",
|
|
142
|
+
"## Cards",
|
|
143
|
+
"",
|
|
144
|
+
cards.length === 0 ? "(ninguna)" : "",
|
|
145
|
+
...cards.map((c) => `- **${c.id}** [${c.state}] ${c.title} · owner=${c.owner ?? "—"} reviewer=${c.reviewer ?? "—"} · rechazos=${c.rejections} · ${c.result ?? ""}`),
|
|
146
|
+
"",
|
|
147
|
+
`## Agentes (${Object.keys(roster.agents).length})`,
|
|
148
|
+
"",
|
|
149
|
+
`**Trabajando:** ${working.map((a) => `${a.id}(${a.role})`).join(", ") || "—"}`,
|
|
150
|
+
`**Libres:** ${idle.map((a) => `${a.id}(${a.role})`).join(", ") || "—"}`,
|
|
151
|
+
`**Sin heartbeat:** ${dead.map((a) => `${a.id}(${a.role})`).join(", ") || "—"}`,
|
|
152
|
+
];
|
|
153
|
+
return {
|
|
154
|
+
title: `Oficina: ${state.status} · ${Object.keys(state.cards).length} cards`,
|
|
155
|
+
output: lines.join("\n"),
|
|
156
|
+
metadata: {
|
|
157
|
+
status: state.status,
|
|
158
|
+
cards: Object.keys(state.cards).length,
|
|
159
|
+
idle: idle.length,
|
|
160
|
+
dead: dead.length,
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
},
|
|
164
|
+
}),
|
|
165
|
+
/**
|
|
166
|
+
* Toma trabajo: claim de la próxima card o de una card puntual.
|
|
167
|
+
*/
|
|
168
|
+
oficina_assign: tool({
|
|
169
|
+
description: "Tomá trabajo en la oficina. Sin cardId, toma la próxima card lista (dependencias satisfechas, o retomando una en curso). " +
|
|
170
|
+
"Con cardId, reclama una card puntual — es el claim proactivo del pool libre. Si la card ya está tomada por otro agente, el motor lo rechaza.",
|
|
171
|
+
args: {
|
|
172
|
+
agentId: z.string().describe("Tu id de agente (el de tu ficha)."),
|
|
173
|
+
cardId: z.string().optional().describe("Card puntual a reclamar. Opcional."),
|
|
174
|
+
},
|
|
175
|
+
async execute(args, ctx) {
|
|
176
|
+
const off = officeOf(ctx);
|
|
177
|
+
const state = await off.loadState();
|
|
178
|
+
let card = null;
|
|
179
|
+
let reason = "";
|
|
180
|
+
if (args.cardId) {
|
|
181
|
+
const claimed = await off.claimCard(state, args.agentId, args.cardId, ctx.sessionID);
|
|
182
|
+
if (!claimed.ok) {
|
|
183
|
+
return { title: "No se pudo reclamar", output: claimed.reason };
|
|
184
|
+
}
|
|
185
|
+
card = state.cards[args.cardId] ?? null;
|
|
186
|
+
reason = claimed.reason;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
const next = await off.claimNext(state, args.agentId, ctx.sessionID);
|
|
190
|
+
card = next.card;
|
|
191
|
+
reason = next.reason;
|
|
192
|
+
}
|
|
193
|
+
if (!card) {
|
|
194
|
+
return { title: "Sin trabajo disponible", output: reason };
|
|
195
|
+
}
|
|
196
|
+
const roster = await off.loadRoster(state.runId);
|
|
197
|
+
const entry = roster.agents[args.agentId];
|
|
198
|
+
return {
|
|
199
|
+
title: `${card.id}: ${card.title}`,
|
|
200
|
+
output: [
|
|
201
|
+
`**Card ${card.id}: ${card.title}** (${card.type})`,
|
|
202
|
+
"",
|
|
203
|
+
`**Por qué esta:** ${reason}`,
|
|
204
|
+
"",
|
|
205
|
+
"**Acceptance criteria:**",
|
|
206
|
+
...card.acceptance.map((a) => `- ${a}`),
|
|
207
|
+
"",
|
|
208
|
+
`**Gate:** ${card.gate}`,
|
|
209
|
+
"",
|
|
210
|
+
"**Scope (may_touch):**",
|
|
211
|
+
...card.scope.mayTouch.map((s) => `- ${s}`),
|
|
212
|
+
"",
|
|
213
|
+
"**Scope (forbidden):**",
|
|
214
|
+
...card.scope.forbidden.map((s) => `- ${s}`),
|
|
215
|
+
"",
|
|
216
|
+
`**Tus peers:** ${entry?.capabilities.join(", ") ?? "—"}`,
|
|
217
|
+
`**Revisor:** ${card.reviewer ?? "—"} (no sos vos)`,
|
|
218
|
+
"",
|
|
219
|
+
"Escribí tu trabajo, y cuando termines llamá `oficina_submit` con el handoff y la evidencia real.",
|
|
220
|
+
].join("\n"),
|
|
221
|
+
metadata: { cardId: card.id, type: card.type },
|
|
222
|
+
};
|
|
223
|
+
},
|
|
224
|
+
}),
|
|
225
|
+
/**
|
|
226
|
+
* Entrega el trabajo de una card.
|
|
227
|
+
*/
|
|
228
|
+
oficina_submit: tool({
|
|
229
|
+
description: "Entrega el trabajo de una card: escribís el handoff (qué hiciste, qué archivos tocaste, qué decidiste, qué quedó afuera) y la evidencia real (salida literal de los comandos que corriste). " +
|
|
230
|
+
"Sin evidencia pegada, la card no se puede aprobar. Después de esto, la card pasa a REVIEW.",
|
|
231
|
+
args: {
|
|
232
|
+
agentId: z.string().describe("Tu id de agente."),
|
|
233
|
+
cardId: z.string().describe("La card que entregás."),
|
|
234
|
+
handoff: z.string().describe("Qué hiciste. Con paths de archivo concretos."),
|
|
235
|
+
evidence: z.string().optional().describe("Salida real de los comandos, pegada literal."),
|
|
236
|
+
},
|
|
237
|
+
async execute(args, ctx) {
|
|
238
|
+
const off = officeOf(ctx);
|
|
239
|
+
const state = await off.loadState();
|
|
240
|
+
const result = await off.submitWork(state, args.cardId, args.agentId, {
|
|
241
|
+
handoff: args.handoff,
|
|
242
|
+
evidence: args.evidence,
|
|
243
|
+
});
|
|
244
|
+
if (!result.ok)
|
|
245
|
+
return { title: "No se pudo entregar", output: result.reason };
|
|
246
|
+
const card = state.cards[args.cardId];
|
|
247
|
+
return {
|
|
248
|
+
title: `${args.cardId} entregada`,
|
|
249
|
+
output: [
|
|
250
|
+
result.reason,
|
|
251
|
+
"",
|
|
252
|
+
`**Revisor asignado:** ${card.reviewer ?? "(sin asignar)"}`,
|
|
253
|
+
"",
|
|
254
|
+
"El revisor corre ahora. Si te rechaza, vas a recibir el motivo y una instrucción concreta de qué corregir.",
|
|
255
|
+
].join("\n"),
|
|
256
|
+
metadata: { cardId: args.cardId },
|
|
257
|
+
};
|
|
258
|
+
},
|
|
259
|
+
}),
|
|
260
|
+
/**
|
|
261
|
+
* Registra el veredicto de revisión.
|
|
262
|
+
*/
|
|
263
|
+
oficina_review: tool({
|
|
264
|
+
description: "Registrá el veredicto de una revisión. NUNCA revises una card que vos implementaste: el motor te lo rechaza. " +
|
|
265
|
+
"El review necesita un ledger de lectura (qué leíste completo, qué callers leíste) y findings con confianza 0-100. " +
|
|
266
|
+
"Solo se reportan findings con confianza ≥ 80.",
|
|
267
|
+
args: {
|
|
268
|
+
agentId: z.string().describe("Tu id de agente (el revisor)."),
|
|
269
|
+
cardId: z.string().describe("La card revisada."),
|
|
270
|
+
verdict: z.enum(["approved", "rejected"]).describe("Veredicto."),
|
|
271
|
+
reason: z.string().describe("Por qué. Si aprobaste, qué te quedó claro; si rechazaste, qué falla exactamente."),
|
|
272
|
+
review: z.string().describe("El review completo en markdown: ledger de lectura, findings con path:línea, escenario de reproducción, fix y test de regresión."),
|
|
273
|
+
},
|
|
274
|
+
async execute(args, ctx) {
|
|
275
|
+
const off = officeOf(ctx);
|
|
276
|
+
const state = await off.loadState();
|
|
277
|
+
const result = await off.submitReview(state, args.cardId, args.agentId, args.verdict, {
|
|
278
|
+
review: args.review,
|
|
279
|
+
reason: args.reason,
|
|
280
|
+
});
|
|
281
|
+
if (!result.ok)
|
|
282
|
+
return { title: "Review inválido", output: result.reason };
|
|
283
|
+
return {
|
|
284
|
+
title: `${args.cardId}: ${args.verdict}`,
|
|
285
|
+
output: [
|
|
286
|
+
`**Veredicto: ${args.verdict.toUpperCase()}**`,
|
|
287
|
+
"",
|
|
288
|
+
result.reason,
|
|
289
|
+
"",
|
|
290
|
+
result.action === "run_gates"
|
|
291
|
+
? "Ahora corré los gates: `oficina_gate`."
|
|
292
|
+
: result.action === "escalate"
|
|
293
|
+
? "La card agotó los reintentos. Se escala al usuario con opciones concretas."
|
|
294
|
+
: "La card vuelve a trabajo. Corregí exactamente lo que el review marcó.",
|
|
295
|
+
].join("\n"),
|
|
296
|
+
metadata: { cardId: args.cardId, verdict: args.verdict, action: result.action },
|
|
297
|
+
};
|
|
298
|
+
},
|
|
299
|
+
}),
|
|
300
|
+
/**
|
|
301
|
+
* Corre los gates de calidad.
|
|
302
|
+
*/
|
|
303
|
+
oficina_gate: tool({
|
|
304
|
+
description: "Corre los gates de calidad de una card: build, type check, lint, tests, security grep y diff review. " +
|
|
305
|
+
"Son comandos reales — el motor interpretan exit codes, no tu opinión. Si pasan, la card se cierra. " +
|
|
306
|
+
"Si fallan, la salida real queda en el artefacto y la card vuelve a trabajo con el motivo.",
|
|
307
|
+
args: {
|
|
308
|
+
cardId: z.string().describe("La card a verificar."),
|
|
309
|
+
},
|
|
310
|
+
async execute(args, ctx) {
|
|
311
|
+
const off = officeOf(ctx);
|
|
312
|
+
const state = await off.loadState();
|
|
313
|
+
const gates = await off.runGates(state, args.cardId);
|
|
314
|
+
const table = (rows) => rows.map((r) => `| ${r.phase} | \`${r.command ?? "—"}\` | ${r.outcome.toUpperCase()} | ${r.detail} |`).join("\n");
|
|
315
|
+
const closed = await off.closeCard(state, args.cardId, gates);
|
|
316
|
+
return {
|
|
317
|
+
title: `${args.cardId}: gates ${gates.passed ? "PASS" : "FAIL"}`,
|
|
318
|
+
output: [
|
|
319
|
+
`## Gate técnico: ${gates.technicalOutcome.toUpperCase()}`,
|
|
320
|
+
"",
|
|
321
|
+
"| Fase | Comando | Resultado | Detalle |",
|
|
322
|
+
"|------|---------|-----------|---------|",
|
|
323
|
+
table(gates.technical),
|
|
324
|
+
"",
|
|
325
|
+
`## Gate seguridad: ${gates.securityOutcome.toUpperCase()}`,
|
|
326
|
+
"",
|
|
327
|
+
"| Fase | Comando | Resultado | Detalle |",
|
|
328
|
+
"|------|---------|-----------|---------|",
|
|
329
|
+
table(gates.security),
|
|
330
|
+
"",
|
|
331
|
+
closed.ok
|
|
332
|
+
? `✅ **${closed.reason}**`
|
|
333
|
+
: `❌ **${closed.reason}**`,
|
|
334
|
+
"",
|
|
335
|
+
"La salida completa de cada comando quedó en `.oficina/artifacts/<card>/gates.md`.",
|
|
336
|
+
].join("\n"),
|
|
337
|
+
metadata: {
|
|
338
|
+
cardId: args.cardId,
|
|
339
|
+
passed: gates.passed,
|
|
340
|
+
technical: gates.technicalOutcome,
|
|
341
|
+
security: gates.securityOutcome,
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
},
|
|
345
|
+
}),
|
|
346
|
+
/**
|
|
347
|
+
* Bus de mensajes: pedir ayuda, recibir, acusar recibo.
|
|
348
|
+
*/
|
|
349
|
+
oficina_bus: tool({
|
|
350
|
+
description: "Bus de mensajes de la oficina. Acciones: 'send' (mandar un mensaje), 'receive' (leer lo que te llegó), 'ack' (acusar recibo), 'help' (pedir ayuda a un peer o al pool). " +
|
|
351
|
+
"Pedir ayuda es un verbo de primera clase: si la tarea es de otro dominio, pedila en vez de Stretcharla.",
|
|
352
|
+
args: {
|
|
353
|
+
action: z.enum(["send", "receive", "ack", "help"]),
|
|
354
|
+
agentId: z.string().describe("Tu id de agente."),
|
|
355
|
+
to: z.array(z.string()).describe("Destinatarios. ['*'] es broadcast al pool."),
|
|
356
|
+
kind: z
|
|
357
|
+
.enum([
|
|
358
|
+
"HELP_REQUEST",
|
|
359
|
+
"HELP_ACCEPT",
|
|
360
|
+
"HELP_DECLINE",
|
|
361
|
+
"HELP_PARTIAL",
|
|
362
|
+
"HELP_QUESTION",
|
|
363
|
+
"QUESTION",
|
|
364
|
+
"ANSWER",
|
|
365
|
+
"SPAWN_REQUEST",
|
|
366
|
+
"SPAWN_APPROVED",
|
|
367
|
+
"SPAWN_REJECTED",
|
|
368
|
+
"CLAIM",
|
|
369
|
+
"RELEASE",
|
|
370
|
+
"STATUS",
|
|
371
|
+
"NOTICE",
|
|
372
|
+
])
|
|
373
|
+
.optional()
|
|
374
|
+
.describe("Tipo de mensaje. Requerido para send/help."),
|
|
375
|
+
subject: z.string().optional().describe("Card o tema del mensaje."),
|
|
376
|
+
body: z.string().optional().describe("Cuerpo del mensaje en texto o JSON."),
|
|
377
|
+
messageId: z.string().optional().describe("Id del mensaje (para ack)."),
|
|
378
|
+
need: z.string().optional().describe("Para help: qué necesitás."),
|
|
379
|
+
whyMe: z.string().optional().describe("Para help: por qué no lo resolvés vos."),
|
|
380
|
+
priority: z.enum(["low", "normal", "high", "critical"]).default("normal"),
|
|
381
|
+
},
|
|
382
|
+
async execute(args, ctx) {
|
|
383
|
+
const off = officeOf(ctx);
|
|
384
|
+
await off.init();
|
|
385
|
+
if (args.action === "receive") {
|
|
386
|
+
const messages = await off.bus.receive(args.agentId, [CHANNELS.peer, CHANNELS.flow, CHANNELS.command, CHANNELS.arch]);
|
|
387
|
+
if (messages.length === 0) {
|
|
388
|
+
return { title: "Sin mensajes", output: "No hay mensajes nuevos en tu inbox." };
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
title: `${messages.length} mensajes`,
|
|
392
|
+
output: messages
|
|
393
|
+
.map((m) => `### [${m.kind}] de ${m.from} — ${m.subject}\n${JSON.stringify(m.body, null, 2)}\n_(id: ${m.id})_`)
|
|
394
|
+
.join("\n\n"),
|
|
395
|
+
metadata: { count: messages.length },
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
if (args.action === "ack") {
|
|
399
|
+
if (!args.messageId)
|
|
400
|
+
return { title: "Falta messageId", output: "Indicá el messageId a acusar." };
|
|
401
|
+
await off.bus.ack(args.agentId, args.messageId);
|
|
402
|
+
return { title: "Acusado", output: `Mensaje ${args.messageId} acusado.` };
|
|
403
|
+
}
|
|
404
|
+
if (args.action === "help") {
|
|
405
|
+
if (!args.need)
|
|
406
|
+
return { title: "Falta need", output: "Indicá qué necesitás." };
|
|
407
|
+
const state = await off.loadState();
|
|
408
|
+
const cardId = args.subject ?? "";
|
|
409
|
+
const route = await off.routeHelpRequest(state, args.agentId, cardId);
|
|
410
|
+
const targets = route.targets.length > 0 ? route.targets : args.to;
|
|
411
|
+
if (targets.length === 0) {
|
|
412
|
+
return { title: "Sin destinatario", output: `No hay a quién pedirle: ${route.reason}` };
|
|
413
|
+
}
|
|
414
|
+
const msg = await off.bus.broadcastHelp({
|
|
415
|
+
from: args.agentId,
|
|
416
|
+
cardId,
|
|
417
|
+
need: args.need,
|
|
418
|
+
whyMe: args.whyMe ?? "",
|
|
419
|
+
});
|
|
420
|
+
return {
|
|
421
|
+
title: `Ayuda pedida a ${targets.join(", ")}`,
|
|
422
|
+
output: `Pedido de ayuda publicado (${msg.id}). Destinatarios: ${targets.join(", ")}. Razón del routing: ${route.reason}.`,
|
|
423
|
+
metadata: { messageId: msg.id, targets },
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
// send
|
|
427
|
+
if (!args.kind)
|
|
428
|
+
return { title: "Falta kind", output: "Indicá el tipo de mensaje." };
|
|
429
|
+
const msg = await off.bus.send(CHANNELS.peer, {
|
|
430
|
+
from: args.agentId,
|
|
431
|
+
to: args.to,
|
|
432
|
+
kind: args.kind,
|
|
433
|
+
subject: args.subject ?? "",
|
|
434
|
+
body: args.body ? safeJson(args.body) : {},
|
|
435
|
+
priority: args.priority,
|
|
436
|
+
});
|
|
437
|
+
return { title: `Mensaje ${msg.id}`, output: `Publicado en el canal peer (seq ${msg.seq}).` };
|
|
438
|
+
},
|
|
439
|
+
}),
|
|
440
|
+
/**
|
|
441
|
+
* Solicita desplegar un agente nuevo.
|
|
442
|
+
*/
|
|
443
|
+
oficina_spawn: tool({
|
|
444
|
+
description: "Pedí desplegar un agente nuevo cuando el rol que te falta NO está en el roster. " +
|
|
445
|
+
"El Integrador aprueba o rechaza: si la capacidad ya la cubre un peer desplegado, te dice a quién preguntar. " +
|
|
446
|
+
"No clones agentes por tu cuenta: el motor mantiene el registro y el presupuesto de spawns.",
|
|
447
|
+
args: {
|
|
448
|
+
agentId: z.string().describe("Tu id de agente."),
|
|
449
|
+
requestedRole: z.string().describe("El rol que necesitás."),
|
|
450
|
+
capabilities: z.array(z.string()).describe("Capabilities del rol nuevo."),
|
|
451
|
+
reason: z.string().describe("Por qué lo necesitás y por qué ningún peeractual lo cubre."),
|
|
452
|
+
cardId: z.string().optional().describe("Card donde lo vas a usar."),
|
|
453
|
+
},
|
|
454
|
+
async execute(args, ctx) {
|
|
455
|
+
const off = officeOf(ctx);
|
|
456
|
+
const state = await off.loadState();
|
|
457
|
+
const roster = await off.loadRoster(state.runId);
|
|
458
|
+
// ¿Ya existe un agente con ese rol?
|
|
459
|
+
const existing = Object.values(roster.agents).find((a) => a.role === args.requestedRole);
|
|
460
|
+
if (existing) {
|
|
461
|
+
return {
|
|
462
|
+
title: "El rol ya existe",
|
|
463
|
+
output: `El rol \`${args.requestedRole}\` ya está desplegado como \`${existing.id}\` (${existing.state}). Pedile ayuda a él en vez de spawnear.`,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
// ¿Algún peer ya cubre la capacidad?
|
|
467
|
+
const covered = Object.values(roster.agents).filter((a) => args.capabilities.some((c) => a.capabilities.includes(c)));
|
|
468
|
+
if (covered.length > 0) {
|
|
469
|
+
return {
|
|
470
|
+
title: "La capacidad ya está cubierta",
|
|
471
|
+
output: [
|
|
472
|
+
`Estos agentes ya cubren alguna de las capabilities que pediste:`,
|
|
473
|
+
...covered.map((a) => `- ${a.id} (${a.role}) — ${a.capabilities.join(", ")}`),
|
|
474
|
+
"",
|
|
475
|
+
"Pediles ayuda por el bus antes de spawnear. Spawnear duplicaría capacidad.",
|
|
476
|
+
].join("\n"),
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
const msg = await off.bus.send(CHANNELS.flow, {
|
|
480
|
+
from: args.agentId,
|
|
481
|
+
to: ["integrator"],
|
|
482
|
+
kind: "SPAWN_REQUEST",
|
|
483
|
+
subject: args.cardId ?? "",
|
|
484
|
+
body: {
|
|
485
|
+
requested_role: args.requestedRole,
|
|
486
|
+
capabilities: args.capabilities,
|
|
487
|
+
reason: args.reason,
|
|
488
|
+
},
|
|
489
|
+
idempotencyKey: `spawn:${args.requestedRole}:${args.agentId}`,
|
|
490
|
+
});
|
|
491
|
+
return {
|
|
492
|
+
title: `Spawn solicitado: ${args.requestedRole}`,
|
|
493
|
+
output: `Spawn request publicado (${msg.id}). El Integrador lo aprueba o lo rechaza. Te llega la respuesta por el bus.`,
|
|
494
|
+
metadata: { messageId: msg.id, role: args.requestedRole },
|
|
495
|
+
};
|
|
496
|
+
},
|
|
497
|
+
}),
|
|
498
|
+
/**
|
|
499
|
+
* Escala al usuario.
|
|
500
|
+
*/
|
|
501
|
+
oficina_escalate: tool({
|
|
502
|
+
description: "Escalá al usuario cuando hay un bloqueo real: dependencia externa, conflicto que ningún coordinador resuelve, " +
|
|
503
|
+
"o presupuesto agotado. NUNCA con un monólogo: siempre 2-3 opciones concretas con trade-off y tu recomendación. " +
|
|
504
|
+
"Lo que un coordinador puede resolver NO se escala.",
|
|
505
|
+
args: {
|
|
506
|
+
agentId: z.string().describe("Tu id de agente."),
|
|
507
|
+
cardId: z.string().describe("La card bloqueada."),
|
|
508
|
+
cause: z.string().describe("Causa raíz. Qué impide avanzar."),
|
|
509
|
+
options: z
|
|
510
|
+
.array(z.object({
|
|
511
|
+
label: z.string().describe("Qué hacer."),
|
|
512
|
+
tradeoff: z.string().describe("Qué cuesta o qué se pierde."),
|
|
513
|
+
}))
|
|
514
|
+
.min(2)
|
|
515
|
+
.describe("Entre 2 y 3 opciones concretas."),
|
|
516
|
+
recommendation: z.string().describe("Cuál recomendás y por qué."),
|
|
517
|
+
},
|
|
518
|
+
async execute(args, ctx) {
|
|
519
|
+
const off = officeOf(ctx);
|
|
520
|
+
const state = await off.loadState();
|
|
521
|
+
const card = state.cards[args.cardId];
|
|
522
|
+
state.escalations.push({
|
|
523
|
+
id: `esc-${state.escalations.length + 1}`,
|
|
524
|
+
cardId: args.cardId,
|
|
525
|
+
at: new Date().toISOString(),
|
|
526
|
+
cause: args.cause,
|
|
527
|
+
attempts: card ? [{ iteration: card.attempts, summary: card.result ?? "sin detalle" }] : [],
|
|
528
|
+
options: args.options.map((o, i) => ({ id: `o${i + 1}`, label: o.label, tradeoff: o.tradeoff })),
|
|
529
|
+
recommendation: args.recommendation,
|
|
530
|
+
status: "open",
|
|
531
|
+
});
|
|
532
|
+
state.decisions.push({
|
|
533
|
+
at: new Date().toISOString(),
|
|
534
|
+
cardId: args.cardId,
|
|
535
|
+
decision: "escalated",
|
|
536
|
+
reason: args.cause,
|
|
537
|
+
actor: args.agentId,
|
|
538
|
+
});
|
|
539
|
+
if (card)
|
|
540
|
+
card.state = "escalated";
|
|
541
|
+
await off.saveState(state);
|
|
542
|
+
return {
|
|
543
|
+
title: `Escalado: ${args.cardId}`,
|
|
544
|
+
output: [
|
|
545
|
+
`**Causa raíz:** ${args.cause}`,
|
|
546
|
+
"",
|
|
547
|
+
"**Opciones:**",
|
|
548
|
+
...args.options.map((o, i) => `${i + 1}. ${o.label} — ${o.tradeoff}`),
|
|
549
|
+
"",
|
|
550
|
+
`**Recomendación:** ${args.recommendation}`,
|
|
551
|
+
].join("\n"),
|
|
552
|
+
metadata: { cardId: args.cardId, options: args.options.length },
|
|
553
|
+
};
|
|
554
|
+
},
|
|
555
|
+
}),
|
|
556
|
+
/**
|
|
557
|
+
* Genera el briefing de un agente para una card.
|
|
558
|
+
*/
|
|
559
|
+
oficina_brief: tool({
|
|
560
|
+
description: "Generá el briefing de un agente: el contrato de información completo con 10 secciones en orden fijo " +
|
|
561
|
+
"(quién sos · tu misión · lo que ya sabés · lo que NO sabés · tu territorio · cómo trabajás · tu canal · " +
|
|
562
|
+
"qué se espera de vos · lo que no podés hacer · cómo se mide que terminaste). " +
|
|
563
|
+
"Las secciones requeridas son el mínimo: si falta alguna, el briefing se marca INCOMPLETO y el agente no arranca. " +
|
|
564
|
+
"Devuelve también las alternativas de rol que servían, para que elCommander pueda repartir carga.",
|
|
565
|
+
args: {
|
|
566
|
+
role: z.string().describe("El rol que querés. Se resuelve contra el catálogo de agency-agents."),
|
|
567
|
+
capabilities: z.array(z.string()).describe("Capabilities que el rol necesita."),
|
|
568
|
+
cardId: z.string().optional().describe("Card a la que se asignará. Si se omite, se usa una card de ejemplo."),
|
|
569
|
+
objective: z.string().optional().describe("Objetivo de la corrida, para que el briefing no sea genérico."),
|
|
570
|
+
},
|
|
571
|
+
async execute(args, ctx) {
|
|
572
|
+
const off = officeOf(ctx);
|
|
573
|
+
await off.init();
|
|
574
|
+
const resolved = await off.resolveFicha({
|
|
575
|
+
role: args.role,
|
|
576
|
+
capabilities: args.capabilities,
|
|
577
|
+
objective: args.objective,
|
|
578
|
+
});
|
|
579
|
+
const state = await off.loadState();
|
|
580
|
+
const card = (args.cardId ? state.cards[args.cardId] : undefined) ??
|
|
581
|
+
state.order.map((id) => state.cards[id]).find((c) => c && c.state === "todo");
|
|
582
|
+
if (!card) {
|
|
583
|
+
return {
|
|
584
|
+
title: "Sin card para el briefing",
|
|
585
|
+
output: "No hay ninguna card en la corrida. Pasá `cardId` de una card existente, o armá el plan primero con `oficina_plan`.",
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
const roster = await off.loadRoster(state.runId);
|
|
589
|
+
const briefing = off.buildBriefing(resolved.ficha, card, roster);
|
|
590
|
+
const alternatives = resolved.alternatives
|
|
591
|
+
.slice(0, 4)
|
|
592
|
+
.map((alt) => `- \`${alt.agent.slug}\` (${alt.agent.division}) — cobertura ${(alt.coverage * 100).toFixed(0)}%`);
|
|
593
|
+
// El panel: primario + complementos + huecos. Los complementos son
|
|
594
|
+
// los agentes que cubren lo que el primario NO cubre.
|
|
595
|
+
const panel = resolved.panel;
|
|
596
|
+
const panelLines = [];
|
|
597
|
+
if (panel?.primary) {
|
|
598
|
+
panelLines.push(`**Primario:** \`${panel.primary.agent.slug}\` (${panel.primary.agent.division}) — ${(panel.primary.coverage * 100).toFixed(0)}% · cubre ${panel.primary.matched.join(", ") || "—"}`);
|
|
599
|
+
for (const c of panel.complements) {
|
|
600
|
+
const aporta = c.matched.filter((m) => !panel.primary.matched.includes(m));
|
|
601
|
+
panelLines.push(`**Complemento:** \`${c.agent.slug}\` (${c.agent.division}) — ${(c.coverage * 100).toFixed(0)}% · aporta ${aporta.join(", ") || "—"}`);
|
|
602
|
+
}
|
|
603
|
+
if (panel.uncovered.length > 0) {
|
|
604
|
+
panelLines.push(`**Sin cubrir por nadie del panel:** ${panel.uncovered.join(", ")} — considerá partir la card o fabricar un rol.`);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
title: `Briefing de ${resolved.ficha.displayName} — ${card.id}`,
|
|
609
|
+
output: [
|
|
610
|
+
`**Rol:** \`${resolved.ficha.role}\` · **origen:** ${resolved.source} · **cobertura:** ${(resolved.coverage * 100).toFixed(0)}%`,
|
|
611
|
+
`**Card:** ${card.id} — ${card.title}`,
|
|
612
|
+
`**Briefing:** ${briefing.complete ? "COMPLETO" : `INCOMPLETO — faltan: ${briefing.missing.join(", ")}`}`,
|
|
613
|
+
"",
|
|
614
|
+
...(panelLines.length > 0 ? ["### Panel de roles", "", ...panelLines, ""] : []),
|
|
615
|
+
"---",
|
|
616
|
+
"",
|
|
617
|
+
renderBriefing(briefing),
|
|
618
|
+
"---",
|
|
619
|
+
"",
|
|
620
|
+
"**Alternativas de rol** (por si querés repartir carga):",
|
|
621
|
+
...alternatives,
|
|
622
|
+
"",
|
|
623
|
+
`Los 4 artefactos (\`ficha.md\`, \`misiones.md\`, \`protocolo.md\`, \`bus.md\`) se escriben en \`.oficina/fichas/${resolved.ficha.id}/\` al desplegar.`,
|
|
624
|
+
].join("\n"),
|
|
625
|
+
metadata: {
|
|
626
|
+
agentId: resolved.ficha.id,
|
|
627
|
+
source: resolved.source,
|
|
628
|
+
coverage: resolved.coverage,
|
|
629
|
+
complete: briefing.complete,
|
|
630
|
+
missing: briefing.missing,
|
|
631
|
+
complements: resolved.panel?.complements.length ?? 0,
|
|
632
|
+
uncovered: resolved.panel?.uncovered ?? [],
|
|
633
|
+
},
|
|
634
|
+
};
|
|
635
|
+
},
|
|
636
|
+
}),
|
|
637
|
+
/**
|
|
638
|
+
* Resuelve un rol a una ficha.
|
|
639
|
+
*/
|
|
640
|
+
oficina_ficha: tool({
|
|
641
|
+
description: "Resolvé un rol a una ficha de agente. El motor busca en el catálogo de agency-agents el agente que mejor matchea las capabilities; " +
|
|
642
|
+
"si la cobertura es menor al 60%, lo fabrica desde un template con el contexto de la corrida. " +
|
|
643
|
+
"Devuelve la ficha completa: scope, peers, límites y prompt de origen.",
|
|
644
|
+
args: {
|
|
645
|
+
role: z.string().describe("El rol que querés."),
|
|
646
|
+
capabilities: z.array(z.string()).describe("Capabilities necesarias."),
|
|
647
|
+
scope: z
|
|
648
|
+
.object({
|
|
649
|
+
mayTouch: z.array(z.string()).optional(),
|
|
650
|
+
forbidden: z.array(z.string()).optional(),
|
|
651
|
+
})
|
|
652
|
+
.optional(),
|
|
653
|
+
canDelegate: z.boolean().default(false),
|
|
654
|
+
delegateBudget: z.number().int().min(0).max(5).default(0),
|
|
655
|
+
peers: z.array(z.string()).default([]),
|
|
656
|
+
objective: z.string().optional().describe("Objetivo de la corrida, para que el prompt no sea genérico."),
|
|
657
|
+
},
|
|
658
|
+
async execute(args, ctx) {
|
|
659
|
+
const off = officeOf(ctx);
|
|
660
|
+
await off.init();
|
|
661
|
+
const result = await off.resolveFicha({
|
|
662
|
+
role: args.role,
|
|
663
|
+
capabilities: args.capabilities,
|
|
664
|
+
scope: args.scope,
|
|
665
|
+
canDelegate: args.canDelegate,
|
|
666
|
+
delegateBudget: args.delegateBudget,
|
|
667
|
+
peers: args.peers,
|
|
668
|
+
objective: args.objective,
|
|
669
|
+
});
|
|
670
|
+
const entry = await off.deploy(result.ficha);
|
|
671
|
+
return {
|
|
672
|
+
title: `${result.ficha.displayName} (${result.source})`,
|
|
673
|
+
output: [
|
|
674
|
+
renderFicha(result.ficha),
|
|
675
|
+
`**Agente desplegado:** \`${entry.id}\` — usá este id en todas las llamadas.`,
|
|
676
|
+
`**Origen:** ${result.source} · **cobertura:** ${(result.coverage * 100).toFixed(0)}%`,
|
|
677
|
+
].join("\n"),
|
|
678
|
+
metadata: { agentId: entry.id, source: result.source, coverage: result.coverage },
|
|
679
|
+
};
|
|
680
|
+
},
|
|
681
|
+
}),
|
|
682
|
+
/**
|
|
683
|
+
* Despacha el equipo: crea los subagentes y les da el briefing.
|
|
684
|
+
*/
|
|
685
|
+
oficina_dispatch: tool({
|
|
686
|
+
description: "Despacha el equipo de la corrida. Para cada card sin owner: resuelve el rol desde el catálogo de agency-agents " +
|
|
687
|
+
"(o lo fabrica si no hay match), materializa el agente con su briefing de 10 secciones, y devuelve el prompt " +
|
|
688
|
+
"de arranque de cada uno. Los complementos del panel quedan disponibles como pares para pedir ayuda. " +
|
|
689
|
+
"Usá esto UNA vez, después de `oficina_plan`: es el momento en que el equipo se forma.",
|
|
690
|
+
args: {
|
|
691
|
+
maxAgents: z.number().int().min(1).max(14).default(10).describe("Máximo de agentes a materializar."),
|
|
692
|
+
maxComplements: z.number().int().min(0).max(3).default(2).describe("Complementos por card."),
|
|
693
|
+
spawn: z
|
|
694
|
+
.boolean()
|
|
695
|
+
.default(true)
|
|
696
|
+
.describe("Si es true, arranca los subagentes en paralelo automáticamente."),
|
|
697
|
+
},
|
|
698
|
+
async execute(args, ctx) {
|
|
699
|
+
const off = officeOf(ctx);
|
|
700
|
+
await off.init();
|
|
701
|
+
const state = await off.loadState();
|
|
702
|
+
const catalog = await off.catalog();
|
|
703
|
+
const roster = await off.loadRoster(state.runId);
|
|
704
|
+
const result = await dispatchOffice(off, state, {
|
|
705
|
+
projectRoot: off.paths.projectRoot,
|
|
706
|
+
catalog,
|
|
707
|
+
objective: state.objective,
|
|
708
|
+
maxAgents: Math.min(args.maxAgents, off.limits.maxConcurrentAgents),
|
|
709
|
+
maxComplements: args.maxComplements,
|
|
710
|
+
minCoverage: off.limits.minCatalogCoverage,
|
|
711
|
+
existing: Object.values(roster.agents),
|
|
712
|
+
});
|
|
713
|
+
// Arranque en paralelo de los agentes con card propia.
|
|
714
|
+
const spawned = [];
|
|
715
|
+
if (args.spawn) {
|
|
716
|
+
for (const agent of result.dispatched) {
|
|
717
|
+
if (agent.isPeer)
|
|
718
|
+
continue;
|
|
719
|
+
const ok = await off.spawnAgent(ctx.sessionID, agent.agentName, agent.prompt, agent.cardId ?? "");
|
|
720
|
+
if (ok)
|
|
721
|
+
spawned.push(`${agent.agentName} (${agent.role})`);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
const lines = [
|
|
725
|
+
`**Equipo despachado:** ${result.dispatched.length} agentes (${result.dispatched.filter((a) => !a.isPeer).length} con card, ${result.dispatched.filter((a) => a.isPeer).length} como pares libres)`,
|
|
726
|
+
"",
|
|
727
|
+
"| Agente | Rol | Card | Origen | Aporta |",
|
|
728
|
+
"|--------|-----|------|--------|---------|",
|
|
729
|
+
...result.dispatched.map((a) => `| \`${a.agentName}\` | ${a.displayName} | ${a.cardId ?? "— (par libre)"} | ${a.source} | ${a.contributes.join(", ") || "—"} |`),
|
|
730
|
+
];
|
|
731
|
+
if (spawned.length > 0) {
|
|
732
|
+
lines.push("", `**Arrancados en paralelo:** ${spawned.join(", ")}`);
|
|
733
|
+
}
|
|
734
|
+
if (result.uncovered.length > 0) {
|
|
735
|
+
lines.push("", `**Capabilities que nadie cubre:** ${result.uncovered.join(", ")}`, "", "Opciones: partir la card en dos (una por dominio), fabricar el rol con `oficina_spawn`, o aceptar la deuda.");
|
|
736
|
+
}
|
|
737
|
+
if (result.unassigned.length > 0) {
|
|
738
|
+
lines.push("", `**Cards sin owner:** ${result.unassigned.join(", ")}`);
|
|
739
|
+
}
|
|
740
|
+
for (const note of result.notes)
|
|
741
|
+
lines.push(`- ${note}`);
|
|
742
|
+
lines.push("", "**Agentes materializados en** `.opencode/agents/` — versionados con el repo y disponibles la próxima sesión.");
|
|
743
|
+
return {
|
|
744
|
+
title: `Equipo despachado: ${result.dispatched.length} agentes`,
|
|
745
|
+
output: lines.join("\n"),
|
|
746
|
+
metadata: {
|
|
747
|
+
dispatched: result.dispatched.length,
|
|
748
|
+
spawned: spawned.length,
|
|
749
|
+
uncovered: result.uncovered,
|
|
750
|
+
unassigned: result.unassigned,
|
|
751
|
+
},
|
|
752
|
+
};
|
|
753
|
+
},
|
|
754
|
+
}),
|
|
755
|
+
/**
|
|
756
|
+
* La mesa: la vista compartida del equipo.
|
|
757
|
+
*/
|
|
758
|
+
oficina_mesa: tool({
|
|
759
|
+
description: "La mesa de la oficina: quién está trabajando en qué, quién está libre para ayudar, qué cards no tienen owner, " +
|
|
760
|
+
"qué pedidos de ayuda siguen sin responder y qué preguntas están abiertas. " +
|
|
761
|
+
"Usala antes de reclamar trabajo, antes de pedir ayuda (para saber a quién) y cuando te digan que te asignaron una card.",
|
|
762
|
+
args: {
|
|
763
|
+
action: z
|
|
764
|
+
.enum(["view", "read", "respond", "offer", "claim"])
|
|
765
|
+
.default("view")
|
|
766
|
+
.describe("view = foto de la mesa · read = mis mensajes · respond = contestar un pedido · offer = ofrecer ayuda · claim = reclamar una card"),
|
|
767
|
+
agentId: z.string().describe("Tu id de agente."),
|
|
768
|
+
messageId: z.string().optional().describe("Mensaje al que respondés (para respond)."),
|
|
769
|
+
kind: z
|
|
770
|
+
.enum([
|
|
771
|
+
"HELP_ACCEPT",
|
|
772
|
+
"HELP_DECLINE",
|
|
773
|
+
"HELP_PARTIAL",
|
|
774
|
+
"HELP_QUESTION",
|
|
775
|
+
"CLAIM",
|
|
776
|
+
"NOTICE",
|
|
777
|
+
])
|
|
778
|
+
.optional()
|
|
779
|
+
.describe("Tipo de respuesta (para respond/offer)."),
|
|
780
|
+
cardId: z.string().optional().describe("Card (para respond/claim)."),
|
|
781
|
+
body: z.string().optional().describe("Tu respuesta o anuncio."),
|
|
782
|
+
to: z.array(z.string()).optional().describe("Destinatarios (para offer)."),
|
|
783
|
+
},
|
|
784
|
+
async execute(args, ctx) {
|
|
785
|
+
const off = officeOf(ctx);
|
|
786
|
+
await off.init();
|
|
787
|
+
if (args.action === "view") {
|
|
788
|
+
const snapshot = await snapshotMesa(off);
|
|
789
|
+
return {
|
|
790
|
+
title: `Mesa: ${snapshot.trabajando.length} trabajando · ${snapshot.libres.length} libres`,
|
|
791
|
+
output: renderMesa(snapshot),
|
|
792
|
+
metadata: {
|
|
793
|
+
working: snapshot.trabajando.length,
|
|
794
|
+
free: snapshot.libres.length,
|
|
795
|
+
unowned: snapshot.sinOwner.length,
|
|
796
|
+
helpOpen: snapshot.ayudaAbierta.length,
|
|
797
|
+
},
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
if (args.action === "read") {
|
|
801
|
+
const messages = await off.bus.receive(args.agentId, [CHANNELS.peer, CHANNELS.flow, CHANNELS.command, CHANNELS.arch]);
|
|
802
|
+
if (messages.length === 0) {
|
|
803
|
+
return { title: "Sin mensajes", output: "No hay mensajes nuevos en tu inbox." };
|
|
804
|
+
}
|
|
805
|
+
return {
|
|
806
|
+
title: `${messages.length} mensajes`,
|
|
807
|
+
output: messages
|
|
808
|
+
.map((m) => `### [${m.kind}] de ${m.from} — ${m.subject}\n${JSON.stringify(m.body, null, 2)}\n_(id: ${m.id})_`)
|
|
809
|
+
.join("\n\n"),
|
|
810
|
+
metadata: { count: messages.length },
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
if (args.action === "respond" || args.action === "offer") {
|
|
814
|
+
if (!args.messageId && args.action === "respond") {
|
|
815
|
+
return { title: "Falta messageId", output: "Indicá el messageId del pedido al que respondés." };
|
|
816
|
+
}
|
|
817
|
+
const msg = await off.bus.send(CHANNELS.peer, {
|
|
818
|
+
from: args.agentId,
|
|
819
|
+
to: args.to ?? [],
|
|
820
|
+
kind: (args.kind ?? "HELP_ACCEPT"),
|
|
821
|
+
subject: args.cardId ?? "",
|
|
822
|
+
body: args.body ? safeJson(args.body) : {},
|
|
823
|
+
inReplyTo: args.messageId ?? null,
|
|
824
|
+
});
|
|
825
|
+
await off.bus.ack(args.agentId, args.messageId ?? msg.id);
|
|
826
|
+
return {
|
|
827
|
+
title: `Mensaje publicado: ${msg.kind}`,
|
|
828
|
+
output: `Publicado en el canal peer (seq ${msg.seq}).`,
|
|
829
|
+
metadata: { messageId: msg.id },
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
// claim
|
|
833
|
+
if (!args.cardId) {
|
|
834
|
+
return { title: "Falta cardId", output: "Indicá la card que reclamás." };
|
|
835
|
+
}
|
|
836
|
+
const state = await off.loadState();
|
|
837
|
+
const claimed = await off.claimCard(state, args.agentId, args.cardId);
|
|
838
|
+
if (!claimed.ok) {
|
|
839
|
+
return { title: "No se pudo reclamar", output: claimed.reason };
|
|
840
|
+
}
|
|
841
|
+
await off.bus.send(CHANNELS.peer, {
|
|
842
|
+
from: args.agentId,
|
|
843
|
+
to: ["*"],
|
|
844
|
+
kind: "CLAIM",
|
|
845
|
+
subject: args.cardId,
|
|
846
|
+
body: { claimed: args.cardId },
|
|
847
|
+
});
|
|
848
|
+
return {
|
|
849
|
+
title: `Card ${args.cardId} reclamada`,
|
|
850
|
+
output: `Reclamaste \`${args.cardId}\`. Avisado en el canal peer.`,
|
|
851
|
+
metadata: { cardId: args.cardId },
|
|
852
|
+
};
|
|
853
|
+
},
|
|
854
|
+
}),
|
|
855
|
+
/**
|
|
856
|
+
* El grafo de requisitos a evidencia.
|
|
857
|
+
*/
|
|
858
|
+
oficina_evidence: tool({
|
|
859
|
+
description: "La evidencia de una card: cada criterio de aceptación enlazado a lo que lo respalda. " +
|
|
860
|
+
"Usá `view` para ver qué falta y `add` para registrar evidencia (un artefacto, un test, " +
|
|
861
|
+
"un gate o una inspección). Una card NO se puede cerrar sin evidencia de todos sus " +
|
|
862
|
+
"requisitos obligatorios: los gates dicen que el código está sano, la evidencia dice que " +
|
|
863
|
+
"el objetivo está cumplido.",
|
|
864
|
+
args: {
|
|
865
|
+
action: z.enum(["view", "add"]).default("view").describe("view = grafo · add = registrar evidencia"),
|
|
866
|
+
cardId: z.string().describe("Card."),
|
|
867
|
+
agentId: z.string().describe("Tu id de agente."),
|
|
868
|
+
requirement: z.string().optional().describe("Criterio de aceptación (texto exacto). Si falta, se usa el índice."),
|
|
869
|
+
requirementIndex: z.number().int().min(0).optional().describe("Índice del criterio de aceptación."),
|
|
870
|
+
kind: z
|
|
871
|
+
.enum(["artifact", "test", "gate", "inspection", "external"])
|
|
872
|
+
.default("artifact")
|
|
873
|
+
.describe("Tipo de evidencia."),
|
|
874
|
+
ref: z.string().optional().describe("Ruta del artefacto, nombre del test, id del gate o referencia."),
|
|
875
|
+
note: z.string().optional().describe("Qué demuestra esta evidencia."),
|
|
876
|
+
},
|
|
877
|
+
async execute(args, ctx) {
|
|
878
|
+
const off = officeOf(ctx);
|
|
879
|
+
await off.init();
|
|
880
|
+
const state = await off.loadState();
|
|
881
|
+
const card = state.cards[args.cardId];
|
|
882
|
+
if (!card)
|
|
883
|
+
return { title: "Card inexistente", output: `No existe la card ${args.cardId}.` };
|
|
884
|
+
if (args.action === "view") {
|
|
885
|
+
const graph = await off.evidence.graph(card);
|
|
886
|
+
const { renderEvidenceGraph } = await import("../core/evidence.js");
|
|
887
|
+
return {
|
|
888
|
+
title: `Evidencia: ${graph.requirements.length} requisitos, ${graph.missingMandatory.length} sin cubrir`,
|
|
889
|
+
output: renderEvidenceGraph(graph),
|
|
890
|
+
metadata: {
|
|
891
|
+
complete: graph.complete,
|
|
892
|
+
missing: graph.missingMandatory.length,
|
|
893
|
+
minLevel: graph.minLevel,
|
|
894
|
+
},
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
if (!args.ref) {
|
|
898
|
+
return { title: "Falta ref", output: "Indicá el `ref`: la ruta del artefacto, el test o el gate." };
|
|
899
|
+
}
|
|
900
|
+
const { evidenceFromArtifact, evidenceFromInspection, evidenceFromExternal, evidenceFromTest } = await import("../core/evidence.js");
|
|
901
|
+
const requirementText = args.requirement ?? card.acceptance[args.requirementIndex ?? 0] ?? "";
|
|
902
|
+
const options = {
|
|
903
|
+
recordedBy: args.agentId,
|
|
904
|
+
requirementIndex: args.requirementIndex ?? card.acceptance.indexOf(requirementText),
|
|
905
|
+
note: args.note,
|
|
906
|
+
owner: card.owner ?? undefined,
|
|
907
|
+
reviewer: card.reviewer ?? undefined,
|
|
908
|
+
};
|
|
909
|
+
const node = args.kind === "test"
|
|
910
|
+
? evidenceFromTest(args.ref, card.id, requirementText, options)
|
|
911
|
+
: args.kind === "inspection"
|
|
912
|
+
? evidenceFromInspection(args.ref, card.id, requirementText, options)
|
|
913
|
+
: args.kind === "external"
|
|
914
|
+
? evidenceFromExternal(args.ref, card.id, requirementText, options)
|
|
915
|
+
: evidenceFromArtifact(args.ref, card.id, requirementText, options);
|
|
916
|
+
await off.evidence.add(card.id, node);
|
|
917
|
+
const graph = await off.evidence.graph(card);
|
|
918
|
+
const { renderEvidenceGraph } = await import("../core/evidence.js");
|
|
919
|
+
return {
|
|
920
|
+
title: `Evidencia registrada (${graph.complete ? "card completa" : "faltan requisitos"})`,
|
|
921
|
+
output: renderEvidenceGraph(graph),
|
|
922
|
+
metadata: { complete: graph.complete, missing: graph.missingMandatory.length },
|
|
923
|
+
};
|
|
924
|
+
},
|
|
925
|
+
}),
|
|
926
|
+
/**
|
|
927
|
+
* El freno de emergencia.
|
|
928
|
+
*/
|
|
929
|
+
oficina_stop: tool({
|
|
930
|
+
description: "Frena la corrida. Con `halt` la oficina deja de avanzar: no se reanuda sola, no se " +
|
|
931
|
+
"despacha y no se gastan mas tokens. Con `resume` vuelve. Usalo si la corrida se esta " +
|
|
932
|
+
"descontrolando, si el costo se dispara o si querés revisar antes de seguir.",
|
|
933
|
+
args: {
|
|
934
|
+
action: z.enum(["halt", "resume"]).default("halt"),
|
|
935
|
+
reason: z.string().optional().describe("Por que frenas (para halt)."),
|
|
936
|
+
agentId: z.string().optional().describe("Quien frena."),
|
|
937
|
+
},
|
|
938
|
+
async execute(args, ctx) {
|
|
939
|
+
const off = officeOf(ctx);
|
|
940
|
+
await off.init();
|
|
941
|
+
const state = await off.loadState();
|
|
942
|
+
if (args.action === "resume") {
|
|
943
|
+
await off.resume(state);
|
|
944
|
+
return {
|
|
945
|
+
title: "Corrida reanudada",
|
|
946
|
+
output: "La corrida vuelve a avanzar. El freno de emergencia quedó desactivado.",
|
|
947
|
+
metadata: { halted: false },
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
const reason = args.reason ?? "freno manual";
|
|
951
|
+
await off.halt(state, reason, args.agentId ?? "usuario");
|
|
952
|
+
return {
|
|
953
|
+
title: "Corrida detenida",
|
|
954
|
+
output: `**La oficina está detenida.**\n\nMotivo: ${reason}\n\n` +
|
|
955
|
+
`No se van a despachar agentes ni a reanudar sesiones. ` +
|
|
956
|
+
`Cuando quieras seguir: \`oficina_stop\` con action="resume".`,
|
|
957
|
+
metadata: { halted: true, reason },
|
|
958
|
+
};
|
|
959
|
+
},
|
|
960
|
+
}),
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
function safeJson(text) {
|
|
964
|
+
try {
|
|
965
|
+
const parsed = JSON.parse(text);
|
|
966
|
+
return typeof parsed === "object" && parsed !== null ? parsed : { text };
|
|
967
|
+
}
|
|
968
|
+
catch {
|
|
969
|
+
return { text };
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
//# sourceMappingURL=index.js.map
|