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,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
|
+
}
|