oficina-opencode 3.1.0 → 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 +1 -1
- 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 +24 -0
- package/dist/cli.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.map +1 -1
- package/dist/core/dispatch.js +11 -0
- package/dist/core/dispatch.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/systemPrompt.d.ts.map +1 -1
- package/dist/core/systemPrompt.js +3 -0
- package/dist/core/systemPrompt.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 +26 -22
- package/dist/tools/index.js.bak +972 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/v2.d.ts +5 -0
- package/dist/v2.d.ts.map +1 -1
- package/dist/v2.js +57 -5
- package/dist/v2.js.bak +517 -0
- package/dist/v2.js.map +1 -1
- package/docs/QUE-ES.md +1 -1
- package/package.json +4 -1
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Factory core — pure, DOM-free logic for the "Factory creando agentes" panel.
|
|
3
|
+
*
|
|
4
|
+
* Loaded as a CLASSIC script (no import/export) so dashboard/factory.html keeps
|
|
5
|
+
* working from file:// where ES module imports are blocked by CORS. The module
|
|
6
|
+
* publishes its API on globalThis.FactoryCore, which is also how Node tests
|
|
7
|
+
* consume it:
|
|
8
|
+
*
|
|
9
|
+
* import "../factory/factory-core.js" // side effect: globalThis.FactoryCore
|
|
10
|
+
* const { detectFactoryNeed } = globalThis.FactoryCore
|
|
11
|
+
*
|
|
12
|
+
* Contract (v1):
|
|
13
|
+
* parseCards(state) -> Card[]
|
|
14
|
+
* parseAgents(roster) -> Agent[]
|
|
15
|
+
* normalizeCapability(string) -> string
|
|
16
|
+
* computeCoverage(card, agents) -> Coverage
|
|
17
|
+
* detectFactoryNeed(state, roster, opts) -> FactoryNeed
|
|
18
|
+
* synthesizeAgent(card, agents, opts) -> FabricatedAgent
|
|
19
|
+
* renderFichaMarkdown(agent, card) -> string
|
|
20
|
+
* renderFichaLines(agent, card) -> string[]
|
|
21
|
+
* registerAgent(roster, agent) -> Roster (immutable)
|
|
22
|
+
* projectMatch(state, roster, opts) -> MatchProjection
|
|
23
|
+
*
|
|
24
|
+
* All functions are side-effect free. Nothing here writes .oficina/ — the
|
|
25
|
+
* dashboard is read-only; registration is a *projection* for the animation.
|
|
26
|
+
*/
|
|
27
|
+
;(function factoryCore(root) {
|
|
28
|
+
"use strict"
|
|
29
|
+
|
|
30
|
+
const COVERAGE_THRESHOLD = 0.6
|
|
31
|
+
|
|
32
|
+
/** Card states that still demand catalog coverage. */
|
|
33
|
+
const OPEN_CARD_STATES = [
|
|
34
|
+
"todo",
|
|
35
|
+
"in_progress",
|
|
36
|
+
"review",
|
|
37
|
+
"rework",
|
|
38
|
+
"blocked",
|
|
39
|
+
"escalated",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
const TERMINAL_CARD_STATES = ["done", "cancelled"]
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Normalize a capability token so "SVG-Canvas", "svg_canvas" and "svgcanvas"
|
|
46
|
+
* compare equal. Keeps a-z0-9 only.
|
|
47
|
+
*/
|
|
48
|
+
function normalizeCapability(value) {
|
|
49
|
+
return String(value ?? "")
|
|
50
|
+
.toLowerCase()
|
|
51
|
+
.normalize("NFD")
|
|
52
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
53
|
+
.replace(/[^a-z0-9]/g, "")
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function asArray(value) {
|
|
57
|
+
return Array.isArray(value) ? value : []
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function str(value, fallback = "") {
|
|
61
|
+
return typeof value === "string" ? value : fallback
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* state.json stores `cards` as an object keyed by id (v3), older snapshots as
|
|
66
|
+
* an array. Accept both, plus `order` for stable sequencing.
|
|
67
|
+
*/
|
|
68
|
+
function parseCards(state) {
|
|
69
|
+
const raw = state && typeof state === "object" ? state.cards : null
|
|
70
|
+
const order = asArray(state && state.order)
|
|
71
|
+
const entries = Array.isArray(raw)
|
|
72
|
+
? raw.map((card) => [str(card && card.id), card])
|
|
73
|
+
: raw && typeof raw === "object"
|
|
74
|
+
? Object.entries(raw)
|
|
75
|
+
: []
|
|
76
|
+
|
|
77
|
+
const byId = new Map(entries)
|
|
78
|
+
const ids = []
|
|
79
|
+
for (const id of order) {
|
|
80
|
+
if (byId.has(id) && !ids.includes(id)) ids.push(id)
|
|
81
|
+
}
|
|
82
|
+
for (const id of byId.keys()) {
|
|
83
|
+
if (!ids.includes(id)) ids.push(id)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return ids.map((id) => {
|
|
87
|
+
const card = byId.get(id) || {}
|
|
88
|
+
return {
|
|
89
|
+
id: str(card.id, id),
|
|
90
|
+
title: str(card.title, id),
|
|
91
|
+
type: str(card.type, "implementation"),
|
|
92
|
+
state: str(card.state || card.status, "todo").toLowerCase(),
|
|
93
|
+
owner: card.owner ? str(card.owner) : null,
|
|
94
|
+
reviewer: card.reviewer ? str(card.reviewer) : null,
|
|
95
|
+
requiredCapabilities: asArray(card.requiredCapabilities).map((c) => str(c)),
|
|
96
|
+
gate: str(card.gate),
|
|
97
|
+
dependsOn: asArray(card.dependsOn).map((d) => str(d)),
|
|
98
|
+
rejections: Number.isFinite(card.rejections) ? card.rejections : 0,
|
|
99
|
+
isOpen: !TERMINAL_CARD_STATES.includes(
|
|
100
|
+
str(card.state || card.status, "todo").toLowerCase(),
|
|
101
|
+
),
|
|
102
|
+
raw: card,
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* roster.json v2 stores `agents` as an object keyed by id. Tolerate arrays.
|
|
109
|
+
*/
|
|
110
|
+
function parseAgents(roster) {
|
|
111
|
+
const raw = roster && typeof roster === "object" ? roster.agents : null
|
|
112
|
+
const entries = Array.isArray(raw)
|
|
113
|
+
? raw.map((agent) => [str(agent && agent.id), agent])
|
|
114
|
+
: raw && typeof raw === "object"
|
|
115
|
+
? Object.entries(raw)
|
|
116
|
+
: []
|
|
117
|
+
|
|
118
|
+
return entries.map(([id, agent]) => ({
|
|
119
|
+
id: str(agent && agent.id, id),
|
|
120
|
+
role: str(agent && agent.role, id),
|
|
121
|
+
displayName: str(
|
|
122
|
+
agent && (agent.displayName || agent.name),
|
|
123
|
+
str(agent && agent.role, id),
|
|
124
|
+
),
|
|
125
|
+
state: str(agent && agent.state, "idle_available"),
|
|
126
|
+
capabilities: asArray(agent && agent.capabilities).map((c) => str(c)),
|
|
127
|
+
capabilityKeys: asArray(agent && agent.capabilities).map(normalizeCapability),
|
|
128
|
+
fichaPath: str(agent && agent.fichaPath),
|
|
129
|
+
currentCard: agent && agent.currentCard ? str(agent.currentCard) : null,
|
|
130
|
+
source: (agent && agent.source) || null,
|
|
131
|
+
raw: agent,
|
|
132
|
+
}))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Union of every capability the roster can currently provide. */
|
|
136
|
+
function catalogKeys(agents) {
|
|
137
|
+
const keys = new Set()
|
|
138
|
+
for (const agent of agents) {
|
|
139
|
+
for (const key of agent.capabilityKeys || []) keys.add(key)
|
|
140
|
+
}
|
|
141
|
+
return keys
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* @returns {{required:string[], matched:string[], missing:string[],
|
|
146
|
+
* ratio:number, pct:number, covered:boolean}}
|
|
147
|
+
*/
|
|
148
|
+
function computeCoverage(card, agents, threshold = COVERAGE_THRESHOLD) {
|
|
149
|
+
const required = asArray(card && card.requiredCapabilities).map((c) => str(c))
|
|
150
|
+
if (required.length === 0) {
|
|
151
|
+
return {
|
|
152
|
+
required: [],
|
|
153
|
+
matched: [],
|
|
154
|
+
missing: [],
|
|
155
|
+
ratio: 1,
|
|
156
|
+
pct: 100,
|
|
157
|
+
covered: true,
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const keys = catalogKeys(agents)
|
|
161
|
+
const matched = []
|
|
162
|
+
const missing = []
|
|
163
|
+
for (const capability of required) {
|
|
164
|
+
if (keys.has(normalizeCapability(capability))) matched.push(capability)
|
|
165
|
+
else missing.push(capability)
|
|
166
|
+
}
|
|
167
|
+
const ratio = matched.length / required.length
|
|
168
|
+
return {
|
|
169
|
+
required,
|
|
170
|
+
matched,
|
|
171
|
+
missing,
|
|
172
|
+
ratio,
|
|
173
|
+
pct: Math.round(ratio * 100),
|
|
174
|
+
covered: ratio >= threshold,
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Percent of roster capabilities that the card actually uses. */
|
|
179
|
+
function bestAgentFor(card, agents) {
|
|
180
|
+
const required = new Set(
|
|
181
|
+
asArray(card && card.requiredCapabilities).map(normalizeCapability),
|
|
182
|
+
)
|
|
183
|
+
let best = null
|
|
184
|
+
for (const agent of agents) {
|
|
185
|
+
let hits = 0
|
|
186
|
+
for (const key of agent.capabilityKeys || []) {
|
|
187
|
+
if (required.has(key)) hits += 1
|
|
188
|
+
}
|
|
189
|
+
if (!best || hits > best.hits) best = { agent, hits }
|
|
190
|
+
}
|
|
191
|
+
return best
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The detection criterion of T-08: a card whose required capabilities cannot
|
|
196
|
+
* be covered by >= threshold of the roster (and that nobody is actually
|
|
197
|
+
* building with a matched ficha) is a factory gap.
|
|
198
|
+
*/
|
|
199
|
+
function detectFactoryNeed(state, roster, options = {}) {
|
|
200
|
+
const threshold = Number.isFinite(options.threshold)
|
|
201
|
+
? options.threshold
|
|
202
|
+
: COVERAGE_THRESHOLD
|
|
203
|
+
const cards = parseCards(state)
|
|
204
|
+
const agents = parseAgents(roster)
|
|
205
|
+
|
|
206
|
+
const gaps = []
|
|
207
|
+
for (const card of cards) {
|
|
208
|
+
if (!card.isOpen) continue
|
|
209
|
+
const coverage = computeCoverage(card, agents, threshold)
|
|
210
|
+
if (coverage.covered) continue
|
|
211
|
+
const best = bestAgentFor(card, agents)
|
|
212
|
+
// An owner whose ficha still fails the threshold is the "sin match" case
|
|
213
|
+
// too: the assignment exists on paper but the catalog cannot back it.
|
|
214
|
+
const ownerAgent = card.owner
|
|
215
|
+
? agents.find((agent) => agent.id === card.owner)
|
|
216
|
+
: null
|
|
217
|
+
gaps.push({
|
|
218
|
+
cardId: card.id,
|
|
219
|
+
title: card.title,
|
|
220
|
+
type: card.type,
|
|
221
|
+
state: card.state,
|
|
222
|
+
owner: card.owner,
|
|
223
|
+
ownerKnown: Boolean(ownerAgent),
|
|
224
|
+
coverage: coverage.ratio,
|
|
225
|
+
pct: coverage.pct,
|
|
226
|
+
required: coverage.required,
|
|
227
|
+
matched: coverage.matched,
|
|
228
|
+
missing: coverage.missing,
|
|
229
|
+
bestAgentId: best && best.agent ? best.agent.id : null,
|
|
230
|
+
bestHits: best ? best.hits : 0,
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const overall = cards.length
|
|
235
|
+
? Math.round(
|
|
236
|
+
(cards.reduce(
|
|
237
|
+
(sum, card) =>
|
|
238
|
+
sum + computeCoverage(card, agents, threshold).ratio,
|
|
239
|
+
0,
|
|
240
|
+
) /
|
|
241
|
+
cards.length) *
|
|
242
|
+
100,
|
|
243
|
+
) /
|
|
244
|
+
100
|
|
245
|
+
: 1
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
needs: gaps.length > 0,
|
|
249
|
+
threshold,
|
|
250
|
+
thresholdPct: Math.round(threshold * 100),
|
|
251
|
+
cardCount: cards.length,
|
|
252
|
+
agentCount: agents.length,
|
|
253
|
+
overallCoverage: overall,
|
|
254
|
+
overallPct: Math.round(overall * 100),
|
|
255
|
+
gaps: gaps.sort((a, b) => a.coverage - b.coverage),
|
|
256
|
+
cards,
|
|
257
|
+
agents,
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const ROLE_BY_CAPABILITY = [
|
|
262
|
+
{
|
|
263
|
+
match: ["animation", "motion", "gsap", "transitions", "svgcanvas", "webgl"],
|
|
264
|
+
role: "frontend-motion-engineer",
|
|
265
|
+
displayName: "Frontend Motion Engineer",
|
|
266
|
+
defaults: ["animation", "css", "svg", "accessibility"],
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
match: ["kanban", "dragdrop", "dnd", "interaction"],
|
|
270
|
+
role: "interaction-engineer",
|
|
271
|
+
displayName: "Interaction Engineer",
|
|
272
|
+
defaults: ["interaction", "dom", "accessibility", "animation"],
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
match: ["realtime", "realtimeui", "polling", "websocket"],
|
|
276
|
+
role: "realtime-ui-engineer",
|
|
277
|
+
displayName: "Realtime UI Engineer",
|
|
278
|
+
defaults: ["realtime", "polling", "javascript", "performance"],
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
match: ["datavisualization", "dataviz", "chart", "charts", "visualization"],
|
|
282
|
+
role: "dataviz-engineer",
|
|
283
|
+
displayName: "Data Visualization Engineer",
|
|
284
|
+
defaults: ["dataviz", "svg", "accessibility", "typescript"],
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
match: ["chatui", "threading", "conversation"],
|
|
288
|
+
role: "conversation-ui-engineer",
|
|
289
|
+
displayName: "Conversation UI Engineer",
|
|
290
|
+
defaults: ["chat", "threading", "accessibility", "aria"],
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
match: ["decisiontracking", "decisions", "governance"],
|
|
294
|
+
role: "decision-systems-analyst",
|
|
295
|
+
displayName: "Decision Systems Analyst",
|
|
296
|
+
defaults: ["documentation", "analysis", "traceability"],
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
match: ["testing", "qa", "vitest", "playwright", "coverage"],
|
|
300
|
+
role: "qa-automation-engineer",
|
|
301
|
+
displayName: "QA Automation Engineer",
|
|
302
|
+
defaults: ["testing", "vitest", "coverage", "automation"],
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
match: ["frontend", "html", "css", "ui", "ux"],
|
|
306
|
+
role: "frontend-ui-engineer",
|
|
307
|
+
displayName: "Frontend UI Engineer",
|
|
308
|
+
defaults: ["frontend", "css", "accessibility", "javascript"],
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
match: ["typescript", "node", "filesystem", "backend"],
|
|
312
|
+
role: "node-systems-engineer",
|
|
313
|
+
displayName: "Node Systems Engineer",
|
|
314
|
+
defaults: ["typescript", "node", "filesystem", "testing"],
|
|
315
|
+
},
|
|
316
|
+
]
|
|
317
|
+
|
|
318
|
+
function slugify(value) {
|
|
319
|
+
return String(value ?? "")
|
|
320
|
+
.toLowerCase()
|
|
321
|
+
.normalize("NFD")
|
|
322
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
323
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
324
|
+
.replace(/^-+|-+$/g, "")
|
|
325
|
+
.slice(0, 48)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function titleCase(role) {
|
|
329
|
+
return role
|
|
330
|
+
.split("-")
|
|
331
|
+
.filter(Boolean)
|
|
332
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
333
|
+
.join(" ")
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Deterministic: same card + same roster => same fabricated agent. */
|
|
337
|
+
function synthesizeAgent(card, agents, options = {}) {
|
|
338
|
+
const required = asArray(card && card.requiredCapabilities).map((c) => str(c))
|
|
339
|
+
const keys = required.map(normalizeCapability)
|
|
340
|
+
const candidates = []
|
|
341
|
+
for (const group of ROLE_BY_CAPABILITY) {
|
|
342
|
+
let score = 0
|
|
343
|
+
for (const needle of group.match) {
|
|
344
|
+
if (keys.some((key) => key === needle || key.includes(needle))) score += 1
|
|
345
|
+
}
|
|
346
|
+
if (score > 0) candidates.push({ score, group })
|
|
347
|
+
}
|
|
348
|
+
candidates.sort((a, b) => b.score - a.score)
|
|
349
|
+
const picked = candidates[0]?.group
|
|
350
|
+
|
|
351
|
+
const cardType = slugify(card && card.type) || "implementation"
|
|
352
|
+
const role = picked ? picked.role : `${cardType}-specialist`
|
|
353
|
+
const displayName = picked
|
|
354
|
+
? picked.displayName
|
|
355
|
+
: `${titleCase(cardType)} Specialist`
|
|
356
|
+
|
|
357
|
+
const existing = new Set(asArray(agents).map((agent) => agent.id))
|
|
358
|
+
const base = `e-factory-${role}`
|
|
359
|
+
let id = base
|
|
360
|
+
let n = 2
|
|
361
|
+
while (existing.has(id)) {
|
|
362
|
+
id = `${base}-${n}`
|
|
363
|
+
n += 1
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const capabilities = []
|
|
367
|
+
for (const capability of required) {
|
|
368
|
+
if (!capabilities.includes(capability)) capabilities.push(capability)
|
|
369
|
+
}
|
|
370
|
+
if (picked) {
|
|
371
|
+
for (const capability of picked.defaults) {
|
|
372
|
+
if (!capabilities.includes(capability)) capabilities.push(capability)
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const gapCardId = card && card.id ? str(card.id) : "unknown-card"
|
|
377
|
+
return {
|
|
378
|
+
id,
|
|
379
|
+
role,
|
|
380
|
+
slug: role,
|
|
381
|
+
displayName,
|
|
382
|
+
capabilities,
|
|
383
|
+
fichaPath: `.opencode/agents/oficina-${role}.md`,
|
|
384
|
+
status: "fabricated",
|
|
385
|
+
fabricatedFor: gapCardId,
|
|
386
|
+
fabricatedAt: options.now ? str(options.now) : new Date().toISOString(),
|
|
387
|
+
rationale: picked
|
|
388
|
+
? `Cobertura insuficiente para ${gapCardId}: el roster no cubre ${asArray(card && card.requiredCapabilities).join(", ") || "los requisitos"}. Se sintetiza ${displayName} con las capabilities faltantes.`
|
|
389
|
+
: `Cobertura insuficiente para ${gapCardId}: no hay rol de catálogo que matchee; se crea ${displayName}.`,
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Markdown of the ficha that the animation "types" into .opencode/agents/. */
|
|
394
|
+
function renderFichaMarkdown(agent, card) {
|
|
395
|
+
const capabilities = asArray(agent && agent.capabilities).join(", ")
|
|
396
|
+
const target = card && card.id ? `${card.id} — ${card.title || ""}`.trim() : "card pendiente"
|
|
397
|
+
return [
|
|
398
|
+
"---",
|
|
399
|
+
`description: ${agent.description || `Rol fabricado para ${target}`}`,
|
|
400
|
+
"mode: subagent",
|
|
401
|
+
`capabilities: ${asArray(agent && agent.capabilities).slice(0, 6).join(", ")}`,
|
|
402
|
+
"---",
|
|
403
|
+
"",
|
|
404
|
+
`# ${agent.displayName}`,
|
|
405
|
+
"",
|
|
406
|
+
`Rol: \`${agent.role}\``,
|
|
407
|
+
`Origen: factory · ${agent.fabricatedFor || "sin card"}`,
|
|
408
|
+
"",
|
|
409
|
+
"## Quién sos",
|
|
410
|
+
`Sos ${agent.displayName}, sintetizado por la Factory para cerrar un hueco de cobertura del catálogo.`,
|
|
411
|
+
"",
|
|
412
|
+
"## Tu misión",
|
|
413
|
+
`Cubrir los criterios de aceptación de ${target} sin tocar fuera de su scope.`,
|
|
414
|
+
"",
|
|
415
|
+
"## Capabilities",
|
|
416
|
+
capabilities || "ninguna",
|
|
417
|
+
"",
|
|
418
|
+
"## Cómo trabajás",
|
|
419
|
+
"1. Leé el briefing de la card.",
|
|
420
|
+
"2. Verificá los archivos que podés tocar.",
|
|
421
|
+
"3. Implementá, medí y dejá evidencia.",
|
|
422
|
+
"",
|
|
423
|
+
"## Qué no podés hacer",
|
|
424
|
+
"- Declarar terminado sin evidence.md.",
|
|
425
|
+
"- Aprobar tu propio trabajo.",
|
|
426
|
+
"",
|
|
427
|
+
].join("\n")
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function renderFichaLines(agent, card) {
|
|
431
|
+
return renderFichaMarkdown(agent, card).split("\n")
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Immutable roster registration (animation projection, not a disk write). */
|
|
435
|
+
function registerAgent(roster, agent, options = {}) {
|
|
436
|
+
const copy =
|
|
437
|
+
roster && typeof roster === "object"
|
|
438
|
+
? { ...roster, agents: { ...(roster.agents || {}) } }
|
|
439
|
+
: { version: 2, agents: {} }
|
|
440
|
+
copy.agents[agent.id] = {
|
|
441
|
+
id: agent.id,
|
|
442
|
+
role: agent.role,
|
|
443
|
+
displayName: agent.displayName,
|
|
444
|
+
state: "idle_available",
|
|
445
|
+
capabilities: [...asArray(agent.capabilities)],
|
|
446
|
+
fichaPath: agent.fichaPath,
|
|
447
|
+
currentCard: null,
|
|
448
|
+
lease: null,
|
|
449
|
+
idleSince: options.now || agent.fabricatedAt,
|
|
450
|
+
canAccept: true,
|
|
451
|
+
helpGiven: 0,
|
|
452
|
+
helpReceived: 0,
|
|
453
|
+
helpedPeers: [],
|
|
454
|
+
lastHeartbeat: options.now || agent.fabricatedAt,
|
|
455
|
+
source: {
|
|
456
|
+
kind: "factory",
|
|
457
|
+
fabricatedFor: agent.fabricatedFor,
|
|
458
|
+
at: options.now || agent.fabricatedAt,
|
|
459
|
+
cardTitle: options.cardTitle || null,
|
|
460
|
+
},
|
|
461
|
+
}
|
|
462
|
+
return copy
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* What changes after registration:
|
|
467
|
+
* - the triggering card's coverage goes up,
|
|
468
|
+
* - the *next* open card flips to covered (that's the "match en próxima card"),
|
|
469
|
+
* - previously impossible cards become routable.
|
|
470
|
+
*/
|
|
471
|
+
function projectMatch(state, roster, options = {}) {
|
|
472
|
+
const threshold = Number.isFinite(options.threshold)
|
|
473
|
+
? options.threshold
|
|
474
|
+
: COVERAGE_THRESHOLD
|
|
475
|
+
const cards = parseCards(state)
|
|
476
|
+
const agents = parseAgents(roster)
|
|
477
|
+
const before = new Map()
|
|
478
|
+
for (const card of cards) {
|
|
479
|
+
before.set(card.id, computeCoverage(card, agents, threshold))
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const fabricated = options.fabricated || null
|
|
483
|
+
const afterAgents = fabricated
|
|
484
|
+
? parseAgents(registerAgent(roster, fabricated))
|
|
485
|
+
: agents
|
|
486
|
+
|
|
487
|
+
const rows = cards.map((card) => {
|
|
488
|
+
const prev = before.get(card.id)
|
|
489
|
+
const next = computeCoverage(card, afterAgents, threshold)
|
|
490
|
+
return {
|
|
491
|
+
cardId: card.id,
|
|
492
|
+
title: card.title,
|
|
493
|
+
state: card.state,
|
|
494
|
+
before: prev,
|
|
495
|
+
after: next,
|
|
496
|
+
flipped: !prev.covered && next.covered,
|
|
497
|
+
improved: next.ratio > prev.ratio,
|
|
498
|
+
owner: card.owner,
|
|
499
|
+
}
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
const flipped = rows.filter((row) => row.flipped)
|
|
503
|
+
const ordered = []
|
|
504
|
+
for (const id of asArray(state && state.order)) {
|
|
505
|
+
const row = rows.find((r) => r.cardId === id)
|
|
506
|
+
if (row) ordered.push(row)
|
|
507
|
+
}
|
|
508
|
+
for (const row of rows) {
|
|
509
|
+
if (!ordered.includes(row)) ordered.push(row)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return {
|
|
513
|
+
threshold,
|
|
514
|
+
rows: ordered,
|
|
515
|
+
flipped,
|
|
516
|
+
nextMatch:
|
|
517
|
+
flipped.find((row) => row.cardId !== (fabricated && fabricated.fabricatedFor)) ||
|
|
518
|
+
flipped[0] ||
|
|
519
|
+
null,
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const api = {
|
|
524
|
+
COVERAGE_THRESHOLD,
|
|
525
|
+
OPEN_CARD_STATES,
|
|
526
|
+
normalizeCapability,
|
|
527
|
+
parseCards,
|
|
528
|
+
parseAgents,
|
|
529
|
+
catalogKeys,
|
|
530
|
+
computeCoverage,
|
|
531
|
+
bestAgentFor,
|
|
532
|
+
detectFactoryNeed,
|
|
533
|
+
synthesizeAgent,
|
|
534
|
+
renderFichaMarkdown,
|
|
535
|
+
renderFichaLines,
|
|
536
|
+
registerAgent,
|
|
537
|
+
projectMatch,
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
root.FactoryCore = api
|
|
541
|
+
})(typeof globalThis !== "undefined" ? globalThis : window)
|