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,35 @@
1
+ /* Sonda 2: .app y sus hijos */
2
+ import { chromium } from "playwright";
3
+
4
+ const browser = await chromium.launch();
5
+ const page = await browser.newPage({ viewport: { width: 390, height: 844 } });
6
+ await page.goto("http://localhost:7333", { waitUntil: "networkidle" });
7
+ await page.waitForTimeout(1500);
8
+
9
+ const probe = await page.evaluate(() => {
10
+ const info = (el) => {
11
+ const r = el.getBoundingClientRect();
12
+ const cs = getComputedStyle(el);
13
+ return {
14
+ sel: el.tagName.toLowerCase() + (el.id ? "#" + el.id : "") + (el.className && typeof el.className === "string" ? "." + el.className.split(" ")[0] : ""),
15
+ w: Math.round(r.width),
16
+ scrollW: el.scrollWidth,
17
+ minW: cs.minWidth,
18
+ display: cs.display,
19
+ overflowX: cs.overflowX,
20
+ };
21
+ };
22
+ const app = document.querySelector(".app");
23
+ const out = { app: info(app), children: [...app.children].map(info) };
24
+ const board = document.querySelector(".board");
25
+ const bsec = document.querySelector(".board-section");
26
+ out.board = board ? info(board) : null;
27
+ out.boardCols = board ? [...board.children].map(info) : null;
28
+ out.boardSection = bsec ? info(bsec) : null;
29
+ const main = document.querySelector(".main");
30
+ out.main = main ? info(main) : null;
31
+ out.mainPanels = main ? [...main.children].map(info) : null;
32
+ return out;
33
+ });
34
+ console.log(JSON.stringify(probe, null, 2));
35
+ await browser.close();
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * serve.mjs — servidor estatico + endpoint de snapshot para el dashboard.
4
+ *
5
+ * node dashboard/scripts/serve.mjs [--port 7331] [--host 127.0.0.1]
6
+ *
7
+ * Por que existe: los navegadores bloquean fetch() sobre file:// (CORS), asi que
8
+ * el panel necesita http para leer .oficina/ en vivo. Este server tiene cero
9
+ * dependencias, es read-only y escucha en loopback por defecto.
10
+ *
11
+ * Rutas:
12
+ * GET /api/snapshot -> { state, roster, messages[], artifacts }
13
+ * GET /dashboard/... -> archivos del dashboard
14
+ * GET /.oficina/... -> artifacts (handoff.md, evidence.md) servidos como texto
15
+ */
16
+ import http from "node:http";
17
+ import fs from "node:fs/promises";
18
+ import path from "node:path";
19
+ import { fileURLToPath } from "node:url";
20
+ import { buildSnapshot } from "./snapshot-data.mjs";
21
+
22
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
23
+ const REPO_ROOT = path.resolve(HERE, "..", "..");
24
+
25
+ const MIME = {
26
+ ".html": "text/html; charset=utf-8",
27
+ ".css": "text/css; charset=utf-8",
28
+ ".js": "text/javascript; charset=utf-8",
29
+ ".mjs": "text/javascript; charset=utf-8",
30
+ ".json": "application/json; charset=utf-8",
31
+ ".md": "text/markdown; charset=utf-8",
32
+ ".svg": "image/svg+xml",
33
+ ".png": "image/png",
34
+ ".ico": "image/x-icon",
35
+ ".txt": "text/plain; charset=utf-8",
36
+ };
37
+
38
+ function arg(name, fallback) {
39
+ const i = process.argv.indexOf(`--${name}`);
40
+ if (i === -1) return fallback;
41
+ const v = process.argv[i + 1];
42
+ return v && !v.startsWith("--") ? v : fallback;
43
+ }
44
+
45
+ const PORT = Number(arg("port", process.env.PORT || 7331));
46
+ const HOST = String(arg("host", "127.0.0.1"));
47
+
48
+ /** Resuelve dentro de la raiz del repo; cualquier escape devuelve null. */
49
+ function safeJoin(root, urlPath) {
50
+ const decoded = decodeURIComponent(urlPath.split("?")[0]);
51
+ const resolved = path.resolve(root, "." + decoded);
52
+ const rel = path.relative(root, resolved);
53
+ if (rel.startsWith("..") || path.isAbsolute(rel)) return null;
54
+ return resolved;
55
+ }
56
+
57
+ async function serveFile(res, file) {
58
+ try {
59
+ const stat = await fs.stat(file);
60
+ if (stat.isDirectory()) return serveFile(res, path.join(file, "index.html"));
61
+ const body = await fs.readFile(file);
62
+ res.writeHead(200, {
63
+ "content-type": MIME[path.extname(file).toLowerCase()] || "application/octet-stream",
64
+ "cache-control": "no-store",
65
+ "content-length": body.length,
66
+ });
67
+ res.end(body);
68
+ } catch {
69
+ res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
70
+ res.end("404 " + path.relative(REPO_ROOT, file));
71
+ }
72
+ }
73
+
74
+ const server = http.createServer(async (req, res) => {
75
+ const url = req.url || "/";
76
+ const pathname = url.split("?")[0];
77
+
78
+ try {
79
+ if (pathname === "/api/snapshot") {
80
+ const snap = await buildSnapshot(REPO_ROOT);
81
+ const body = JSON.stringify(snap);
82
+ res.writeHead(200, {
83
+ "content-type": "application/json; charset=utf-8",
84
+ "cache-control": "no-store",
85
+ "content-length": Buffer.byteLength(body),
86
+ });
87
+ res.end(body);
88
+ return;
89
+ }
90
+
91
+ if (pathname === "/" || pathname === "") {
92
+ res.writeHead(302, { location: "/dashboard/leadership.html" });
93
+ res.end();
94
+ return;
95
+ }
96
+
97
+ const allowed = ["/dashboard/", "/.oficina/"];
98
+ if (!allowed.some((p) => pathname.startsWith(p))) {
99
+ res.writeHead(403, { "content-type": "text/plain; charset=utf-8" });
100
+ res.end("403 solo /dashboard y /.oficina");
101
+ return;
102
+ }
103
+
104
+ const file = safeJoin(REPO_ROOT, pathname);
105
+ if (!file) {
106
+ res.writeHead(400, { "content-type": "text/plain; charset=utf-8" });
107
+ res.end("400 ruta invalida");
108
+ return;
109
+ }
110
+ await serveFile(res, file);
111
+ } catch (err) {
112
+ res.writeHead(500, { "content-type": "text/plain; charset=utf-8" });
113
+ res.end("500 " + (err && err.message ? err.message : "error"));
114
+ }
115
+ });
116
+
117
+ server.listen(PORT, HOST, () => {
118
+ const base = `http://${HOST}:${PORT}`;
119
+ console.log(`[dashboard] ${base}/dashboard/leadership.html`);
120
+ console.log(`[dashboard] snapshot: ${base}/api/snapshot`);
121
+ console.log(`[dashboard] raiz: ${REPO_ROOT}`);
122
+ });
@@ -0,0 +1,93 @@
1
+ /*
2
+ * snapshot-data.mjs — arma el snapshot que consumen los paneles del dashboard.
3
+ *
4
+ * Un unico objeto { state, roster, messages[], artifacts } evita que el panel
5
+ * lea state.json y el bus en momentos distintos y muestre una foto rasgada.
6
+ *
7
+ * Lo usan:
8
+ * - dashboard/scripts/serve.mjs -> GET /api/snapshot (live)
9
+ * - dashboard/scripts/snapshot.mjs -> dashboard/data/snapshot.json (file://)
10
+ */
11
+ import fs from "node:fs/promises";
12
+ import path from "node:path";
13
+
14
+ /** Canales de coordinadores + peer. Una carpeta ausente no es un error. */
15
+ export const CHANNELS = ["coord-command", "coord-arch", "coord-flow", "peer"];
16
+
17
+ const MAX_MESSAGES = 500;
18
+
19
+ async function readJson(file) {
20
+ try {
21
+ const raw = await fs.readFile(file, "utf8");
22
+ return JSON.parse(raw);
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ async function listDir(dir) {
29
+ try {
30
+ const entries = await fs.readdir(dir, { withFileTypes: true });
31
+ return entries;
32
+ } catch {
33
+ return [];
34
+ }
35
+ }
36
+
37
+ /** Lee todos los mensajes del bus, ordenados por canal y nombre (seq). */
38
+ export async function readBus(oficinaDir) {
39
+ const messages = [];
40
+ for (const channel of CHANNELS) {
41
+ const dir = path.join(oficinaDir, "bus", channel);
42
+ const files = (await listDir(dir))
43
+ .filter((e) => e.isFile() && e.name.endsWith(".json"))
44
+ .map((e) => e.name)
45
+ .sort();
46
+ for (const name of files) {
47
+ const msg = await readJson(path.join(dir, name));
48
+ if (!msg || typeof msg !== "object" || !msg.id) continue;
49
+ messages.push({ channel, file: name, message: msg });
50
+ }
51
+ }
52
+ // Los paneles muestran lo mas reciente: recortar sin perder el orden original.
53
+ const sorted = messages
54
+ .map((m) => ({ m, t: Date.parse(m.message.createdAt) || 0 }))
55
+ .sort((a, b) => a.t - b.t)
56
+ .map((x) => x.m);
57
+ return sorted.length > MAX_MESSAGES ? sorted.slice(sorted.length - MAX_MESSAGES) : sorted;
58
+ }
59
+
60
+ /** Lista de artifacts por card: { "T-06": { dir, files[] } }. */
61
+ export async function readArtifacts(oficinaDir) {
62
+ const root = path.join(oficinaDir, "artifacts");
63
+ const out = {};
64
+ const cards = (await listDir(root)).filter((e) => e.isDirectory());
65
+ for (const card of cards) {
66
+ const dir = path.join(root, card.name);
67
+ const files = (await listDir(dir)).filter((e) => e.isFile()).map((e) => e.name).sort();
68
+ out[card.name] = { dir: `../.oficina/artifacts/${card.name}/`, files };
69
+ }
70
+ return out;
71
+ }
72
+
73
+ /**
74
+ * @param {string} repoRoot raiz del repo (contiene .oficina/)
75
+ * @returns {Promise<{generatedAt:string, source:string, state:object|null, roster:object|null, messages:object[], artifacts:object}>}
76
+ */
77
+ export async function buildSnapshot(repoRoot) {
78
+ const oficinaDir = path.join(repoRoot, ".oficina");
79
+ const [state, roster, messages, artifacts] = await Promise.all([
80
+ readJson(path.join(oficinaDir, "state.json")),
81
+ readJson(path.join(oficinaDir, "roster.json")),
82
+ readBus(oficinaDir),
83
+ readArtifacts(oficinaDir),
84
+ ]);
85
+ return {
86
+ generatedAt: new Date().toISOString(),
87
+ source: "live",
88
+ state,
89
+ roster: roster || { agents: {} },
90
+ messages,
91
+ artifacts,
92
+ };
93
+ }
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * snapshot.mjs — congela un snapshot de .oficina/ en dashboard/data/snapshot.json.
4
+ *
5
+ * node dashboard/scripts/snapshot.mjs
6
+ *
7
+ * Sirve para abrir leadership.html con doble click (file://), donde el
8
+ * navegador no deja hacer fetch() de ../.oficina/state.json. El snapshot es
9
+ * una foto: para datos en vivo usar dashboard/scripts/serve.mjs.
10
+ */
11
+ import fs from "node:fs/promises";
12
+ import path from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+ import { buildSnapshot } from "./snapshot-data.mjs";
15
+
16
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
17
+ const REPO_ROOT = path.resolve(HERE, "..", "..");
18
+ const OUT = path.resolve(HERE, "..", "data", "snapshot.json");
19
+
20
+ const snap = await buildSnapshot(REPO_ROOT);
21
+ await fs.mkdir(path.dirname(OUT), { recursive: true });
22
+ await fs.writeFile(OUT, JSON.stringify(snap, null, 2), "utf8");
23
+
24
+ const cards = snap.state && snap.state.cards ? Object.keys(snap.state.cards).length : 0;
25
+ const decisions = snap.state && Array.isArray(snap.state.decisions) ? snap.state.decisions.length : 0;
26
+ console.log(
27
+ `[snapshot] ${path.relative(REPO_ROOT, OUT)} · ${cards} cards · ${decisions} decisions · ${snap.messages.length} mensajes · ${Object.keys(snap.artifacts).length} artifacts`
28
+ );
@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env node
2
+ /* ============================================================
3
+ Oficina · servidor del dashboard
4
+ Node puro, cero dependencias. Sirve dashboard/ + API:
5
+ GET /api/state — agrega state.json, roster.json, bus/, leases
6
+ GET /api/events — SSE: avisa cuando cambia algo en .oficina/
7
+ GET /api/artifacts?card=T-00 — parsea gates.md → [{name,status}]
8
+ Uso: node server.mjs [--port 7333] [--oficina <abs>] [--no-open]
9
+ --oficina dir .oficina del proyecto (default: ../.oficina)
10
+ --no-open no abre el navegador (el plugin lo abre cuando arranca
11
+ la corrida; el arranque manual sí abre)
12
+ Al escuchar escribe <officeDir>/dashboard.json (url + port) para el
13
+ handshake con el plugin. Si el puerto está ocupado por OTRO proyecto
14
+ negocia el siguiente; si es el MISMO proyecto, solo abre el browser.
15
+ ============================================================ */
16
+ import http from "node:http";
17
+ import fs from "node:fs";
18
+ import path from "node:path";
19
+ import { spawn } from "node:child_process";
20
+ import { fileURLToPath } from "node:url";
21
+
22
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
+ const ROOT = path.resolve(__dirname, "..");
24
+
25
+ /* ---- CLI args ---- */
26
+ const argv = process.argv.slice(2);
27
+ let portArg = 7333;
28
+ let oficinaArg = null;
29
+ let openArg = true;
30
+ for (let i = 0; i < argv.length; i++) {
31
+ if (argv[i] === "--oficina") oficinaArg = argv[++i];
32
+ else if (argv[i] === "--port") portArg = Number(argv[++i]);
33
+ else if (argv[i] === "--no-open") openArg = false;
34
+ else if (argv[i] === "--open") openArg = true;
35
+ else if (!argv[i].startsWith("--")) portArg = Number(argv[i]);
36
+ }
37
+
38
+ const OFICINA = path.resolve(oficinaArg || path.join(ROOT, ".oficina"));
39
+ const DASH = __dirname;
40
+
41
+ function openBrowser(url) {
42
+ try {
43
+ const stdio = { detached: true, stdio: "ignore", windowsHide: true };
44
+ if (process.platform === "win32") spawn("cmd", ["/c", "start", "", url], stdio).unref();
45
+ else if (process.platform === "darwin") spawn("open", [url], stdio).unref();
46
+ else spawn("xdg-open", [url], stdio).unref();
47
+ } catch { /* sin navegador no hay drama: la URL queda en el log */ }
48
+ }
49
+
50
+ /* handshake con el plugin: url y puerto reales */
51
+ function writeHandshake(port) {
52
+ try {
53
+ fs.writeFileSync(path.join(OFICINA, "dashboard.json"), JSON.stringify({ url: `http://localhost:${port}`, port }));
54
+ } catch { /* .oficina no existe todavía: el dashboard queda en modo demo */ }
55
+ }
56
+
57
+ const MIME = {
58
+ ".html": "text/html; charset=utf-8",
59
+ ".css": "text/css; charset=utf-8",
60
+ ".js": "text/javascript; charset=utf-8",
61
+ ".mjs": "text/javascript; charset=utf-8",
62
+ ".json": "application/json; charset=utf-8",
63
+ ".md": "text/markdown; charset=utf-8",
64
+ };
65
+
66
+ function readJson(p) {
67
+ try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return null; }
68
+ }
69
+
70
+ function listBus() {
71
+ const out = [];
72
+ const dir = path.join(OFICINA, "bus");
73
+ if (!fs.existsSync(dir)) return out;
74
+ for (const sub of fs.readdirSync(dir, { withFileTypes: true })) {
75
+ if (!sub.isDirectory()) continue;
76
+ const subDir = path.join(dir, sub.name);
77
+ for (const f of fs.readdirSync(subDir).sort()) {
78
+ if (!f.endsWith(".json")) continue;
79
+ const m = readJson(path.join(subDir, f));
80
+ if (m) out.push(m);
81
+ }
82
+ }
83
+ out.sort((a, b) => (a.seq ?? 0) - (b.seq ?? 0));
84
+ return out;
85
+ }
86
+
87
+ function aggregate() {
88
+ const stateFile = readJson(path.join(OFICINA, "state.json")) || {};
89
+ const rosterFile = readJson(path.join(OFICINA, "roster.json")) || {};
90
+ return {
91
+ runId: stateFile.runId || rosterFile.runId || null,
92
+ objective: stateFile.objective || "",
93
+ status: stateFile.status || "unknown",
94
+ cards: stateFile.cards || {},
95
+ agents: rosterFile.agents || {},
96
+ bus: listBus(),
97
+ };
98
+ }
99
+
100
+ function parseGates(cardId) {
101
+ const dir = path.join(OFICINA, "artifacts", cardId);
102
+ const out = { gates: [], evidence: false, handoff: false };
103
+ if (!fs.existsSync(dir)) return out;
104
+ const gatesFile = path.join(dir, "gates.md");
105
+ if (fs.existsSync(gatesFile)) {
106
+ const text = fs.readFileSync(gatesFile, "utf8");
107
+ for (const line of text.split("\n")) {
108
+ // tabla real: | # | Fase | Comando | Resultado | Detalle |
109
+ if (!/^\|\s*\d+\s*\|/.test(line)) continue;
110
+ const cells = line.split("|").map((s) => s.trim());
111
+ const fase = cells[2] || "";
112
+ const result = (cells[4] || "").toUpperCase();
113
+ if (!fase) continue;
114
+ const status = result.startsWith("PASS") ? "pass" : result.startsWith("FAIL") ? "fail" : "na";
115
+ out.gates.push({ name: fase, status, command: cells[3] || "" });
116
+ }
117
+ }
118
+ out.evidence = fs.existsSync(path.join(dir, "evidence.md"));
119
+ out.handoff = fs.existsSync(path.join(dir, "handoff.md"));
120
+ return out;
121
+ }
122
+
123
+ /* sirve un artifact crudo (evidence.md / handoff.md) para abrirlo en pestaña */
124
+ function serveArtifactFile(req, res, url, name) {
125
+ const card = url.searchParams.get("card");
126
+ const file = path.join(OFICINA, "artifacts", card || "", name);
127
+ if (!card || !file.startsWith(path.join(OFICINA, "artifacts")) || !fs.existsSync(file)) {
128
+ res.writeHead(404); res.end("not found"); return;
129
+ }
130
+ res.writeHead(200, { "Content-Type": "text/markdown; charset=utf-8" });
131
+ res.end(fs.readFileSync(file));
132
+ }
133
+
134
+ /* ---- Watch: recursively notify SSE clients ---- */
135
+ const sseClients = new Set();
136
+ let watchReady = false;
137
+ function watchOficina() {
138
+ if (!fs.existsSync(OFICINA)) return;
139
+ try {
140
+ fs.watch(OFICINA, { recursive: true }, () => {
141
+ const msg = "event: change\ndata: {}\n\n";
142
+ for (const res of sseClients) res.write(msg);
143
+ });
144
+ watchReady = true;
145
+ } catch { /* fs.watch recursivo no disponible → los clientes usan polling */ }
146
+ }
147
+
148
+ /* ---- Static ---- */
149
+ function serveStatic(req, res) {
150
+ let urlPath = new URL(req.url, "http://x").pathname;
151
+ if (urlPath === "/") urlPath = "/dashboard.html";
152
+ const filePath = path.join(DASH, path.normalize(urlPath).replace(/^[/\\]/, ""));
153
+ if (!filePath.startsWith(DASH)) { res.writeHead(403); res.end(); return; }
154
+ fs.readFile(filePath, (err, data) => {
155
+ if (err) { res.writeHead(404); res.end("not found"); return; }
156
+ res.writeHead(200, { "Content-Type": MIME[path.extname(filePath)] || "application/octet-stream" });
157
+ res.end(data);
158
+ });
159
+ }
160
+
161
+ /* ---- API ---- */
162
+ function serveApi(req, res, url) {
163
+ const sendJson = (obj, code = 200) => {
164
+ res.writeHead(code, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
165
+ res.end(JSON.stringify(obj));
166
+ };
167
+
168
+ if (url.pathname === "/api/state") {
169
+ sendJson({ oficina: OFICINA, ...aggregate() });
170
+ return;
171
+ }
172
+ if (url.pathname === "/api/artifacts") {
173
+ const card = url.searchParams.get("card");
174
+ if (!card) { sendJson({ gates: [] }, 400); return; }
175
+ sendJson(parseGates(card));
176
+ return;
177
+ }
178
+ if (url.pathname === "/api/evidence") { serveArtifactFile(req, res, url, "evidence.md"); return; }
179
+ if (url.pathname === "/api/handoff") { serveArtifactFile(req, res, url, "handoff.md"); return; }
180
+ if (url.pathname === "/api/events") {
181
+ res.writeHead(200, {
182
+ "Content-Type": "text/event-stream",
183
+ "Cache-Control": "no-store",
184
+ Connection: "keep-alive",
185
+ });
186
+ res.write("retry: 3000\n\n");
187
+ sseClients.add(res);
188
+ req.on("close", () => sseClients.delete(res));
189
+ return;
190
+ }
191
+ res.writeHead(404); res.end();
192
+ }
193
+
194
+ /* ---- Server ---- */
195
+ const server = http.createServer((req, res) => {
196
+ const url = new URL(req.url, "http://x");
197
+ if (url.pathname.startsWith("/api/")) serveApi(req, res, url);
198
+ else serveStatic(req, res);
199
+ });
200
+
201
+ /* ---- Arranque con negociación de puerto ---- */
202
+ function probe(port) {
203
+ return new Promise((resolve) => {
204
+ const req = http.get({ host: "127.0.0.1", port, path: "/api/state", timeout: 1500 }, (res) => {
205
+ let data = "";
206
+ res.on("data", (c) => { data += c; });
207
+ res.on("end", () => {
208
+ try { resolve({ ok: true, oficina: JSON.parse(data).oficina }); }
209
+ catch { resolve({ ok: true, oficina: null }); }
210
+ });
211
+ });
212
+ req.on("error", () => resolve({ ok: false }));
213
+ req.on("timeout", () => { req.destroy(); resolve({ ok: false }); });
214
+ });
215
+ }
216
+
217
+ async function boot() {
218
+ let port = portArg;
219
+ for (let i = 0; i < 10; i++) {
220
+ const p = await probe(port);
221
+ if (!p.ok) break; // puerto libre
222
+ if (p.oficina === OFICINA) {
223
+ // Mismo proyecto: ya está corriendo. Solo abrir el navegador.
224
+ writeHandshake(port);
225
+ console.log(`Oficina dashboard ya corriendo → http://localhost:${port}`);
226
+ if (openArg) openBrowser(`http://localhost:${port}`);
227
+ process.exit(0);
228
+ }
229
+ port = portArg + i + 1; // otro proyecto: negociar
230
+ }
231
+
232
+ server.listen(port, () => {
233
+ console.log(`Oficina dashboard → http://localhost:${port}`);
234
+ console.log(` datos: ${OFICINA}`);
235
+ writeHandshake(port);
236
+ watchOficina();
237
+ if (!watchReady) console.log(" fs.watch no disponible → los clientes usan polling de 4s");
238
+ if (openArg) openBrowser(`http://localhost:${port}`);
239
+ });
240
+ }
241
+
242
+ boot();
@@ -0,0 +1,54 @@
1
+ /* Verificación visual del dashboard (Impeccable: screenshot + defect scan) */
2
+ import { chromium } from "playwright";
3
+
4
+ const URL = "http://localhost:7333";
5
+ const out = "dashboard/shots";
6
+
7
+ const browser = await chromium.launch();
8
+ const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
9
+
10
+ const errors = [];
11
+ page.on("pageerror", (e) => errors.push("pageerror: " + e.message));
12
+ page.on("console", (m) => { if (m.type() === "error") errors.push("console: " + m.text()); });
13
+
14
+ await page.goto(URL, { waitUntil: "networkidle" });
15
+ await page.waitForTimeout(2000);
16
+
17
+ // 1. vista desktop con datos reales
18
+ await page.screenshot({ path: `${out}/desktop-real.png`, fullPage: true });
19
+
20
+ // 2. click en una card → gates render
21
+ await page.click(".card >> nth=0");
22
+ await page.waitForTimeout(800);
23
+ await page.screenshot({ path: `${out}/desktop-gates.png` });
24
+
25
+ // 3. móvil 390x844
26
+ await page.setViewportSize({ width: 390, height: 844 });
27
+ await page.waitForTimeout(600);
28
+ await page.screenshot({ path: `${out}/mobile.png`, fullPage: true });
29
+
30
+ // 4. tema claro
31
+ await page.setViewportSize({ width: 1440, height: 900 });
32
+ await page.keyboard.press("t");
33
+ await page.waitForTimeout(600);
34
+ await page.screenshot({ path: `${out}/desktop-light.png` });
35
+
36
+ // 5. sin errores JS
37
+ console.log("errores JS:", errors.length ? errors : "ninguno");
38
+
39
+ // 6. checks de contenido
40
+ const checks = await page.evaluate(() => {
41
+ const $ = (s) => document.querySelector(s);
42
+ return {
43
+ runId: $("#run-id")?.textContent,
44
+ mesaNodes: document.querySelectorAll(".agent-node").length,
45
+ busMsgs: document.querySelectorAll(".msg").length,
46
+ boardCols: document.querySelectorAll(".col").length,
47
+ chiefItems: document.querySelectorAll(".chief-item").length,
48
+ demoBanner: !$("#demo-banner")?.hidden,
49
+ conn: $("#conn")?.dataset.state,
50
+ };
51
+ });
52
+ console.log("checks:", JSON.stringify(checks, null, 2));
53
+
54
+ await browser.close();
Binary file
Binary file