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.
Files changed (69) hide show
  1. package/README.md +1 -1
  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 +24 -0
  43. package/dist/cli.js.map +1 -1
  44. package/dist/core/dashboard.d.ts +33 -0
  45. package/dist/core/dashboard.d.ts.map +1 -0
  46. package/dist/core/dashboard.js +115 -0
  47. package/dist/core/dashboard.js.map +1 -0
  48. package/dist/core/dispatch.d.ts.map +1 -1
  49. package/dist/core/dispatch.js +11 -0
  50. package/dist/core/dispatch.js.map +1 -1
  51. package/dist/core/environment.d.ts +28 -0
  52. package/dist/core/environment.d.ts.map +1 -0
  53. package/dist/core/environment.js +80 -0
  54. package/dist/core/environment.js.map +1 -0
  55. package/dist/core/systemPrompt.d.ts.map +1 -1
  56. package/dist/core/systemPrompt.js +3 -0
  57. package/dist/core/systemPrompt.js.map +1 -1
  58. package/dist/tools/index.d.ts +9 -1
  59. package/dist/tools/index.d.ts.map +1 -1
  60. package/dist/tools/index.js +26 -22
  61. package/dist/tools/index.js.bak +972 -0
  62. package/dist/tools/index.js.map +1 -1
  63. package/dist/v2.d.ts +5 -0
  64. package/dist/v2.d.ts.map +1 -1
  65. package/dist/v2.js +57 -5
  66. package/dist/v2.js.bak +517 -0
  67. package/dist/v2.js.map +1 -1
  68. package/docs/QUE-ES.md +1 -1
  69. package/package.json +4 -1
@@ -0,0 +1,208 @@
1
+ /* ==========================================================================
2
+ Oficina dashboard — design tokens (shared, framework-free)
3
+ Namespaced --of-* so later panels (T-03..T-10) can reuse them without
4
+ inheriting this page's component styles.
5
+ Dark = "foundry" (default). Light = "linen". Toggle with [data-theme].
6
+ ========================================================================== */
7
+
8
+ :root {
9
+ color-scheme: dark;
10
+
11
+ --of-font-ui: "Inter", "Segoe UI Variable Text", "Segoe UI", system-ui,
12
+ -apple-system, sans-serif;
13
+ --of-font-mono: ui-monospace, "Cascadia Mono", "JetBrains Mono", "SFMono-Regular",
14
+ Consolas, monospace;
15
+
16
+ /* surfaces */
17
+ --of-bg: oklch(0.165 0.012 62);
18
+ --of-panel: oklch(0.205 0.014 62);
19
+ --of-panel-2: oklch(0.238 0.016 62);
20
+ --of-panel-3: oklch(0.275 0.018 62);
21
+ --of-line: oklch(0.305 0.014 62);
22
+ --of-line-strong: oklch(0.395 0.016 62);
23
+
24
+ /* text */
25
+ --of-ink: oklch(0.955 0.008 85);
26
+ --of-ink-dim: oklch(0.755 0.012 80);
27
+ --of-ink-faint: oklch(0.6 0.014 75);
28
+
29
+ /* accents */
30
+ --of-ember: oklch(0.79 0.16 62);
31
+ --of-ember-deep: oklch(0.63 0.19 42);
32
+ --of-ember-soft: oklch(0.79 0.16 62 / 0.14);
33
+ --of-steel: oklch(0.74 0.09 235);
34
+ --of-steel-soft: oklch(0.74 0.09 235 / 0.14);
35
+ --of-ok: oklch(0.78 0.13 158);
36
+ --of-ok-soft: oklch(0.78 0.13 158 / 0.13);
37
+ --of-warn: oklch(0.82 0.14 85);
38
+ --of-bad: oklch(0.67 0.19 25);
39
+ --of-bad-soft: oklch(0.67 0.19 25 / 0.13);
40
+
41
+ /* type scale */
42
+ --of-text-3xs: 0.625rem;
43
+ --of-text-2xs: 0.6875rem;
44
+ --of-text-xs: 0.75rem;
45
+ --of-text-sm: 0.8125rem;
46
+ --of-text-md: 0.9375rem;
47
+ --of-text-lg: 1.125rem;
48
+ --of-text-xl: 1.5rem;
49
+ --of-text-2xl: clamp(1.6rem, 1.2rem + 1.4vw, 2.3rem);
50
+
51
+ /* spacing */
52
+ --of-1: 0.25rem;
53
+ --of-2: 0.5rem;
54
+ --of-3: 0.75rem;
55
+ --of-4: 1rem;
56
+ --of-5: 1.5rem;
57
+ --of-6: 2rem;
58
+ --of-7: 3rem;
59
+
60
+ /* geometry */
61
+ --of-radius-sm: 4px;
62
+ --of-radius: 8px;
63
+ --of-radius-lg: 14px;
64
+ --of-radius-pill: 999px;
65
+ --of-shadow: 0 1px 0 oklch(1 0 0 / 0.04) inset,
66
+ 0 18px 40px -24px oklch(0.1 0.01 60 / 0.85);
67
+ --of-ring: 0 0 0 2px var(--of-bg), 0 0 0 4px var(--of-ember);
68
+
69
+ --of-ease: cubic-bezier(0.22, 0.61, 0.36, 1);
70
+ --of-ease-out: cubic-bezier(0.16, 1, 0.3, 1);
71
+ }
72
+
73
+ :root[data-theme="light"] {
74
+ color-scheme: light;
75
+
76
+ --of-bg: oklch(0.968 0.007 85);
77
+ --of-panel: oklch(0.995 0.003 85);
78
+ --of-panel-2: oklch(0.955 0.009 82);
79
+ --of-panel-3: oklch(0.925 0.012 80);
80
+ --of-line: oklch(0.885 0.01 80);
81
+ --of-line-strong: oklch(0.775 0.014 78);
82
+
83
+ --of-ink: oklch(0.235 0.018 65);
84
+ --of-ink-dim: oklch(0.42 0.016 68);
85
+ --of-ink-faint: oklch(0.55 0.015 70);
86
+
87
+ --of-ember: oklch(0.63 0.18 55);
88
+ --of-ember-deep: oklch(0.52 0.19 42);
89
+ --of-ember-soft: oklch(0.63 0.18 55 / 0.12);
90
+ --of-steel: oklch(0.5 0.11 240);
91
+ --of-steel-soft: oklch(0.5 0.11 240 / 0.12);
92
+ --of-ok: oklch(0.55 0.14 155);
93
+ --of-ok-soft: oklch(0.55 0.14 155 / 0.12);
94
+ --of-warn: oklch(0.62 0.15 70);
95
+ --of-bad: oklch(0.53 0.19 25);
96
+ --of-bad-soft: oklch(0.53 0.19 25 / 0.1);
97
+
98
+ --of-shadow: 0 1px 0 oklch(1 0 0 / 0.7) inset,
99
+ 0 14px 30px -22px oklch(0.4 0.02 70 / 0.45);
100
+ }
101
+
102
+ /* --- base ---------------------------------------------------------------- */
103
+
104
+ *,
105
+ *::before,
106
+ *::after {
107
+ box-sizing: border-box;
108
+ }
109
+
110
+ html,
111
+ body {
112
+ margin: 0;
113
+ min-height: 100%;
114
+ }
115
+
116
+ body {
117
+ background: var(--of-bg);
118
+ color: var(--of-ink);
119
+ font-family: var(--of-font-ui);
120
+ font-size: var(--of-text-md);
121
+ line-height: 1.5;
122
+ -webkit-font-smoothing: antialiased;
123
+ text-rendering: optimizeLegibility;
124
+ }
125
+
126
+ button {
127
+ font: inherit;
128
+ color: inherit;
129
+ background: none;
130
+ border: 0;
131
+ padding: 0;
132
+ cursor: pointer;
133
+ }
134
+
135
+ code,
136
+ kbd,
137
+ pre,
138
+ samp {
139
+ font-family: var(--of-font-mono);
140
+ font-size: 0.92em;
141
+ }
142
+
143
+ :focus-visible {
144
+ outline: none;
145
+ box-shadow: var(--of-ring);
146
+ border-radius: var(--of-radius-sm);
147
+ }
148
+
149
+ ::selection {
150
+ background: oklch(0.79 0.16 62 / 0.3);
151
+ }
152
+
153
+ ::-webkit-scrollbar {
154
+ width: 10px;
155
+ height: 10px;
156
+ }
157
+ ::-webkit-scrollbar-track {
158
+ background: transparent;
159
+ }
160
+ ::-webkit-scrollbar-thumb {
161
+ background: var(--of-line);
162
+ border: 3px solid transparent;
163
+ background-clip: padding-box;
164
+ border-radius: 999px;
165
+ }
166
+ ::-webkit-scrollbar-thumb:hover {
167
+ background: var(--of-line-strong);
168
+ background-clip: padding-box;
169
+ }
170
+
171
+ .visually-hidden {
172
+ position: absolute !important;
173
+ width: 1px;
174
+ height: 1px;
175
+ padding: 0;
176
+ margin: -1px;
177
+ overflow: hidden;
178
+ clip: rect(0 0 0 0);
179
+ white-space: nowrap;
180
+ border: 0;
181
+ }
182
+
183
+ .skip-link {
184
+ position: absolute;
185
+ left: var(--of-4);
186
+ top: -100px;
187
+ z-index: 50;
188
+ padding: var(--of-2) var(--of-3);
189
+ background: var(--of-ember);
190
+ color: oklch(0.18 0.02 60);
191
+ border-radius: var(--of-radius);
192
+ font-weight: 600;
193
+ transition: top 120ms var(--of-ease);
194
+ }
195
+ .skip-link:focus-visible {
196
+ top: var(--of-3);
197
+ }
198
+
199
+ @media (prefers-reduced-motion: reduce) {
200
+ *,
201
+ *::before,
202
+ *::after {
203
+ animation-duration: 0.001ms !important;
204
+ animation-iteration-count: 1 !important;
205
+ transition-duration: 0.001ms !important;
206
+ scroll-behavior: auto !important;
207
+ }
208
+ }
@@ -0,0 +1,279 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * build-kanban-data.mjs — exporta el estado real de `.oficina/` al dataset que
4
+ * consume `dashboard/kanban.html`.
5
+ *
6
+ * Lee (read-only):
7
+ * .oficina/state.json cards, runId, status, decisions
8
+ * .oficina/roster.json agentes y displayName
9
+ * .oficina/artifacts/<id>/gates.md resultados de gate por fase
10
+ * .oficina/evidence/<id>.json grafo de evidencia por criterio
11
+ *
12
+ * Escribe (solo dentro de dashboard/):
13
+ * dashboard/kanban-data.json snapshot completo (para `?data=`)
14
+ * dashboard/kanban.html inyecta el snapshot entre los markers
15
+ * BEGIN_DATASET / END_DATASET
16
+ *
17
+ * Uso:
18
+ * node dashboard/build-kanban-data.mjs
19
+ * node dashboard/build-kanban-data.mjs --check # valida el embed, no escribe
20
+ */
21
+ import fs from "node:fs/promises"
22
+ import path from "node:path"
23
+ import { fileURLToPath } from "node:url"
24
+ import "./kanban-source.js"
25
+
26
+ const here = path.dirname(fileURLToPath(import.meta.url))
27
+ const repoRoot = path.resolve(here, "..")
28
+ const officeDir = path.join(repoRoot, ".oficina")
29
+ const htmlPath = path.join(here, "kanban.html")
30
+ const jsonPath = path.join(here, "kanban-data.json")
31
+
32
+ const BEGIN = "<!--BEGIN_DATASET-->"
33
+ const END = "<!--END_DATASET-->"
34
+
35
+ const CHECK = process.argv.includes("--check")
36
+
37
+ async function readJson(file, fallback) {
38
+ try {
39
+ return JSON.parse(await fs.readFile(file, "utf8"))
40
+ } catch (err) {
41
+ if (err.code === "ENOENT") return fallback
42
+ throw new Error("No pude leer " + file + ": " + err.message)
43
+ }
44
+ }
45
+
46
+ async function listDir(dir) {
47
+ try {
48
+ return await fs.readdir(dir, { withFileTypes: true })
49
+ } catch (err) {
50
+ if (err.code === "ENOENT") return []
51
+ throw err
52
+ }
53
+ }
54
+
55
+ function splitRow(line) {
56
+ // `escapeCell` escapa el pipe como `\|`; no partimos ahí.
57
+ const cells = []
58
+ let buffer = ""
59
+ for (let i = 0; i < line.length; i++) {
60
+ const ch = line[i]
61
+ if (ch === "\\" && line[i + 1] === "|") {
62
+ buffer += "|"
63
+ i++
64
+ continue
65
+ }
66
+ if (ch === "|") {
67
+ cells.push(buffer.trim())
68
+ buffer = ""
69
+ continue
70
+ }
71
+ buffer += ch
72
+ }
73
+ if (buffer.trim()) cells.push(buffer.trim())
74
+ // La fila empieza y termina con `|`: el primer hueco es el borde, no una celda.
75
+ if (cells.length && cells[0] === "") cells.shift()
76
+ return cells
77
+ }
78
+
79
+ function uncode(text) {
80
+ return String(text || "").replace(/^`|`$/g, "").trim()
81
+ }
82
+
83
+ function outcomeOf(text) {
84
+ const value = String(text || "").trim().toUpperCase()
85
+ if (value === "PASS") return "pass"
86
+ if (value === "FAIL") return "fail"
87
+ if (value === "NA" || value === "N/A") return "na"
88
+ return "not_run"
89
+ }
90
+
91
+ /**
92
+ * Parsea el `gates.md` que escribe `renderGate` en src/core/gates.ts.
93
+ * Tablas: `| # | Fase | Comando | Resultado | Detalle | Motivo |`
94
+ * Footer: `**Gate técnico:** PASS` / `**Gate seguridad:** PASS` / `**Veredicto:** PASS`
95
+ */
96
+ export function parseGates(markdown) {
97
+ const result = {
98
+ ranAt: null,
99
+ technical: [],
100
+ security: [],
101
+ requirements: [],
102
+ technicalOutcome: "not_run",
103
+ securityOutcome: "not_run",
104
+ requirementsOutcome: "not_run",
105
+ verdict: "not_run"
106
+ }
107
+ if (!markdown) return result
108
+
109
+ const lines = markdown.split(/\r?\n/)
110
+ let section = null
111
+
112
+ for (const line of lines) {
113
+ const ranAt = /^\*\*Corredor:\*\*\s*(.+)$/.exec(line)
114
+ if (ranAt) result.ranAt = ranAt[1].trim()
115
+
116
+ const heading = /^##\s+(.+)$/.exec(line)
117
+ if (heading) {
118
+ const title = heading[1].trim().toLowerCase()
119
+ if (title.startsWith("t") && title.includes("cnico")) section = "technical"
120
+ else if (title.startsWith("seguridad")) section = "security"
121
+ else if (title.startsWith("requisitos")) section = "requirements"
122
+ else section = null
123
+ continue
124
+ }
125
+
126
+ if (section && line.trim().startsWith("|")) {
127
+ const cells = splitRow(line.trim())
128
+ if (cells.length < 4) continue
129
+ if (/^#?$/.test(cells[0]) || /^:?-{2,}/.test(cells[0])) continue
130
+ result[section].push({
131
+ phase: cells[1] || "—",
132
+ command: uncode(cells[2]) === "—" ? null : uncode(cells[2]),
133
+ outcome: outcomeOf(cells[3]),
134
+ detail: cells[4] || "",
135
+ reason: cells[5] && cells[5] !== "—" ? cells[5] : ""
136
+ })
137
+ }
138
+
139
+ const technical = /^\*\*Gate t[eé]cnico:\*\*\s*(\w+)/i.exec(line)
140
+ if (technical) result.technicalOutcome = outcomeOf(technical[1])
141
+ const security = /^\*\*Gate seguridad:\*\*\s*(\w+)/i.exec(line)
142
+ if (security) result.securityOutcome = outcomeOf(security[1])
143
+ const verdict = /^\*\*Veredicto:\*\*\s*(\w+)/i.exec(line)
144
+ if (verdict) result.verdict = outcomeOf(verdict[1])
145
+ }
146
+
147
+ return result
148
+ }
149
+
150
+ async function loadGates(cardIds) {
151
+ const gates = {}
152
+ for (const cardId of cardIds) {
153
+ const file = path.join(officeDir, "artifacts", cardId, "gates.md")
154
+ const markdown = await fs.readFile(file, "utf8").catch(() => null)
155
+ if (markdown) gates[cardId] = parseGates(markdown)
156
+ }
157
+ return gates
158
+ }
159
+
160
+ async function loadEvidence(cardIds) {
161
+ const evidence = {}
162
+ for (const cardId of cardIds) {
163
+ const file = path.join(officeDir, "evidence", cardId + ".json")
164
+ const raw = await readJson(file, null)
165
+ if (raw && Array.isArray(raw.nodes)) evidence[cardId] = raw.nodes
166
+ }
167
+ return evidence
168
+ }
169
+
170
+ function normalizeAgent(raw) {
171
+ return {
172
+ id: raw.id,
173
+ displayName: raw.displayName || raw.id,
174
+ role: raw.role || "",
175
+ state: raw.state || null,
176
+ currentCard: raw.currentCard || null
177
+ }
178
+ }
179
+
180
+ export async function buildDataset() {
181
+ const state = await readJson(path.join(officeDir, "state.json"), null)
182
+ if (!state) throw new Error("Falta .oficina/state.json: no hay corrida para exportar.")
183
+ if (!state.cards || typeof state.cards !== "object") throw new Error("state.json no tiene `cards`.")
184
+
185
+ const roster = await readJson(path.join(officeDir, "roster.json"), { agents: {} })
186
+ const cards = Object.values(state.cards)
187
+ const cardIds = cards.map((card) => card.id)
188
+
189
+ const agents = {}
190
+ for (const agent of Object.values(roster.agents || {})) {
191
+ if (agent && agent.id) agents[agent.id] = normalizeAgent(agent)
192
+ }
193
+
194
+ const gates = await loadGates(cardIds)
195
+ const evidence = await loadEvidence(cardIds)
196
+
197
+ return {
198
+ meta: {
199
+ runId: state.runId || "",
200
+ objective: state.objective || "",
201
+ status: state.status || "",
202
+ iteration: state.iteration || 0,
203
+ updatedAt: state.updatedAt || null,
204
+ generatedAt: new Date().toISOString(),
205
+ generator: "dashboard/build-kanban-data.mjs"
206
+ },
207
+ agents,
208
+ cards,
209
+ gates,
210
+ evidence
211
+ }
212
+ }
213
+
214
+ function embedInto(html, dataset) {
215
+ const begin = html.indexOf(BEGIN)
216
+ const end = html.indexOf(END)
217
+ if (begin === -1 || end === -1 || end < begin) {
218
+ throw new Error("kanban.html no tiene los markers " + BEGIN + " / " + END)
219
+ }
220
+ const payload = JSON.stringify(dataset)
221
+ const script = '<script type="application/json" id="kanban-dataset">' + payload + "</script>"
222
+ return html.slice(0, begin + BEGIN.length) + "\n" + script + "\n" + html.slice(end)
223
+ }
224
+
225
+ function summarize(dataset) {
226
+ const counts = {}
227
+ for (const card of dataset.cards) counts[card.state] = (counts[card.state] || 0) + 1
228
+ const order = ["todo", "in_progress", "review", "done", "rework", "blocked", "escalated"]
229
+ const byState = order.map((id) => id + "=" + (counts[id] || 0)).join(" ")
230
+ return {
231
+ cards: dataset.cards.length,
232
+ agents: Object.keys(dataset.agents).length,
233
+ gates: Object.keys(dataset.gates).length,
234
+ evidence: Object.keys(dataset.evidence).length,
235
+ byState
236
+ }
237
+ }
238
+
239
+ async function main() {
240
+ const dataset = await buildDataset()
241
+ const html = await fs.readFile(htmlPath, "utf8")
242
+ const embedded = embedInto(html, dataset)
243
+
244
+ const normalized = globalThis.KanbanSource.normalizeDataset(dataset)
245
+ if (normalized.cards.length !== dataset.cards.length) {
246
+ throw new Error("normalizeDataset perdió cards: " + normalized.cards.length + "/" + dataset.cards.length)
247
+ }
248
+
249
+ if (CHECK) {
250
+ const current = await fs.readFile(htmlPath, "utf8")
251
+ const match = /<script[^>]+id="kanban-dataset"[^>]*>([\s\S]*?)<\/script>/.exec(current)
252
+ if (!match) throw new Error("kanban.html no contiene el script #kanban-dataset")
253
+ const inFile = JSON.parse(match[1])
254
+ const a = inFile.cards.map((card) => card.id).sort().join(",")
255
+ const b = dataset.cards.map((card) => card.id).sort().join(",")
256
+ if (a !== b) {
257
+ throw new Error("Dataset embebido desactualizado. cards embebidas: [" + a + "] vs state.json: [" + b + "]")
258
+ }
259
+ console.log("OK datos embebidos al día (card ids coinciden con .oficina/state.json)")
260
+ console.log(" " + JSON.stringify(summarize(dataset)))
261
+ return
262
+ }
263
+
264
+ await fs.writeFile(jsonPath, JSON.stringify(dataset, null, 2) + "\n", "utf8")
265
+ await fs.writeFile(htmlPath, embedded, "utf8")
266
+
267
+ const summary = summarize(dataset)
268
+ console.log("OK dashboard/kanban-data.json " + summary.cards + " cards · " + summary.agents + " agentes")
269
+ console.log("OK dashboard/kanban.html dataset embebido (" + summary.byState + ")")
270
+ console.log(" gates.md leídos: " + summary.gates + " · evidence/*.json leídos: " + summary.evidence)
271
+ }
272
+
273
+ const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
274
+ if (isMain) {
275
+ main().catch((err) => {
276
+ console.error("FAIL " + err.message)
277
+ process.exitCode = 1
278
+ })
279
+ }