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.
Files changed (145) hide show
  1. package/README.md +240 -208
  2. package/commands/oficina.md +7 -6
  3. package/dashboard/.dom-check.html +1257 -0
  4. package/dashboard/README-DASHBOARD.md +64 -0
  5. package/dashboard/assets/factory.css +1543 -0
  6. package/dashboard/assets/tokens.css +208 -0
  7. package/dashboard/build-kanban-data.mjs +279 -0
  8. package/dashboard/dashboard.css +753 -0
  9. package/dashboard/dashboard.html +121 -0
  10. package/dashboard/dashboard.js +610 -0
  11. package/dashboard/data/snapshot.json +883 -0
  12. package/dashboard/debate.html +134 -0
  13. package/dashboard/factory/build-snapshot.mjs +89 -0
  14. package/dashboard/factory/factory-app.js +1185 -0
  15. package/dashboard/factory/factory-core.js +541 -0
  16. package/dashboard/factory/snapshot.js +850 -0
  17. package/dashboard/factory.html +350 -0
  18. package/dashboard/kanban-data.json +671 -0
  19. package/dashboard/kanban-source.js +643 -0
  20. package/dashboard/kanban.check.mjs +454 -0
  21. package/dashboard/kanban.html +1257 -0
  22. package/dashboard/leadership.html +1008 -0
  23. package/dashboard/lib/debate-app.js +1149 -0
  24. package/dashboard/lib/debate-core.js +698 -0
  25. package/dashboard/lib/debate-data.js +163 -0
  26. package/dashboard/lib/debate-demo.js +110 -0
  27. package/dashboard/lib/debate.css +1155 -0
  28. package/dashboard/lib/leadership-model.js +946 -0
  29. package/dashboard/probe.mjs +35 -0
  30. package/dashboard/scripts/serve.mjs +122 -0
  31. package/dashboard/scripts/snapshot-data.mjs +93 -0
  32. package/dashboard/scripts/snapshot.mjs +28 -0
  33. package/dashboard/server.mjs +242 -0
  34. package/dashboard/shot.mjs +54 -0
  35. package/dashboard/shots/desktop-gates.png +0 -0
  36. package/dashboard/shots/desktop-light.png +0 -0
  37. package/dashboard/shots/desktop-real.png +0 -0
  38. package/dashboard/shots/mobile.png +0 -0
  39. package/dashboard/test/factory-core.test.mjs +244 -0
  40. package/dashboard/tests/debate-core.test.mjs +336 -0
  41. package/dashboard/tests/leadership-model.test.mjs +404 -0
  42. package/dist/cli.js +148 -26
  43. package/dist/cli.js.map +1 -1
  44. package/dist/core/bus.js +3 -3
  45. package/dist/core/bus.js.map +1 -1
  46. package/dist/core/catalog.d.ts +9 -1
  47. package/dist/core/catalog.d.ts.map +1 -1
  48. package/dist/core/catalog.js +77 -2
  49. package/dist/core/catalog.js.map +1 -1
  50. package/dist/core/dashboard.d.ts +33 -0
  51. package/dist/core/dashboard.d.ts.map +1 -0
  52. package/dist/core/dashboard.js +115 -0
  53. package/dist/core/dashboard.js.map +1 -0
  54. package/dist/core/dispatch.d.ts +1 -1
  55. package/dist/core/dispatch.d.ts.map +1 -1
  56. package/dist/core/dispatch.js +84 -12
  57. package/dist/core/dispatch.js.map +1 -1
  58. package/dist/core/docsync.d.ts +0 -9
  59. package/dist/core/docsync.d.ts.map +1 -1
  60. package/dist/core/docsync.js +12 -15
  61. package/dist/core/docsync.js.map +1 -1
  62. package/dist/core/environment.d.ts +28 -0
  63. package/dist/core/environment.d.ts.map +1 -0
  64. package/dist/core/environment.js +80 -0
  65. package/dist/core/environment.js.map +1 -0
  66. package/dist/core/factory.d.ts +16 -0
  67. package/dist/core/factory.d.ts.map +1 -1
  68. package/dist/core/factory.js +143 -0
  69. package/dist/core/factory.js.map +1 -1
  70. package/dist/core/fsx.d.ts +20 -2
  71. package/dist/core/fsx.d.ts.map +1 -1
  72. package/dist/core/fsx.js +10 -6
  73. package/dist/core/fsx.js.map +1 -1
  74. package/dist/core/gates.d.ts +23 -1
  75. package/dist/core/gates.d.ts.map +1 -1
  76. package/dist/core/gates.js +83 -8
  77. package/dist/core/gates.js.map +1 -1
  78. package/dist/core/leases.d.ts.map +1 -1
  79. package/dist/core/leases.js +4 -1
  80. package/dist/core/leases.js.map +1 -1
  81. package/dist/core/mesa.d.ts +1 -6
  82. package/dist/core/mesa.d.ts.map +1 -1
  83. package/dist/core/mesa.js +1 -6
  84. package/dist/core/mesa.js.map +1 -1
  85. package/dist/core/office.d.ts +3 -29
  86. package/dist/core/office.d.ts.map +1 -1
  87. package/dist/core/office.js +77 -90
  88. package/dist/core/office.js.map +1 -1
  89. package/dist/core/paths.d.ts.map +1 -1
  90. package/dist/core/paths.js +6 -1
  91. package/dist/core/paths.js.map +1 -1
  92. package/dist/core/retry.d.ts +2 -8
  93. package/dist/core/retry.d.ts.map +1 -1
  94. package/dist/core/retry.js +3 -34
  95. package/dist/core/retry.js.map +1 -1
  96. package/dist/core/roster.js +3 -3
  97. package/dist/core/roster.js.map +1 -1
  98. package/dist/core/sandbox.d.ts +38 -1
  99. package/dist/core/sandbox.d.ts.map +1 -1
  100. package/dist/core/sandbox.js +119 -15
  101. package/dist/core/sandbox.js.map +1 -1
  102. package/dist/core/state.d.ts +2 -2
  103. package/dist/core/state.d.ts.map +1 -1
  104. package/dist/core/state.js +5 -33
  105. package/dist/core/state.js.map +1 -1
  106. package/dist/core/stores.d.ts +3 -13
  107. package/dist/core/stores.d.ts.map +1 -1
  108. package/dist/core/stores.js +8 -32
  109. package/dist/core/stores.js.map +1 -1
  110. package/dist/core/systemPrompt.d.ts.map +1 -1
  111. package/dist/core/systemPrompt.js +4 -1
  112. package/dist/core/systemPrompt.js.map +1 -1
  113. package/dist/core/tripwire.d.ts +22 -8
  114. package/dist/core/tripwire.d.ts.map +1 -1
  115. package/dist/core/tripwire.js +71 -26
  116. package/dist/core/tripwire.js.map +1 -1
  117. package/dist/core/types.d.ts +6 -14
  118. package/dist/core/types.d.ts.map +1 -1
  119. package/dist/core/types.js +11 -3
  120. package/dist/core/types.js.map +1 -1
  121. package/dist/hooks/scopeGuard.d.ts +15 -1
  122. package/dist/hooks/scopeGuard.d.ts.map +1 -1
  123. package/dist/hooks/scopeGuard.js +88 -73
  124. package/dist/hooks/scopeGuard.js.map +1 -1
  125. package/dist/tools/index.d.ts +9 -1
  126. package/dist/tools/index.d.ts.map +1 -1
  127. package/dist/tools/index.js +29 -81
  128. package/dist/tools/index.js.bak +972 -0
  129. package/dist/tools/index.js.map +1 -1
  130. package/dist/v1.d.ts.map +1 -1
  131. package/dist/v1.js +30 -36
  132. package/dist/v1.js.map +1 -1
  133. package/dist/v2.d.ts +5 -0
  134. package/dist/v2.d.ts.map +1 -1
  135. package/dist/v2.js +79 -46
  136. package/dist/v2.js.bak +517 -0
  137. package/dist/v2.js.map +1 -1
  138. package/docs/GARANTIAS.md +2 -2
  139. package/docs/PRUEBA-E2E.md +4 -4
  140. package/docs/QUE-ES.md +4 -5
  141. package/docs/SEGURIDAD-REPO.md +83 -0
  142. package/docs/assets/banner.svg +42 -0
  143. package/docs/catalog-facts.json +3 -3
  144. package/docs/superpowers/specs/2026-10-29-factory-skill-creator-and-remove-budget.md +182 -0
  145. package/package.json +14 -6
@@ -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)