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,1257 @@
1
+ <!doctype html>
2
+ <html lang="es" data-theme="dark">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Kanban — Oficina de Agents</title>
7
+ <meta name="description" content="Tablero Kanban read-only de las cards de la Oficina: columnas por estado, criterios de aceptación, gates y evidencia.">
8
+ <style>
9
+ :root {
10
+ --bg: #0B0D10;
11
+ --surface: #14181D;
12
+ --surface-2: #101419;
13
+ --border: #232A32;
14
+ --border-strong: #2F3945;
15
+ --text: #E6EAF0;
16
+ --muted: #8B97A6;
17
+ --accent: #4ADE80;
18
+ --warn: #FBBF24;
19
+ --danger: #F87171;
20
+ --radius: 4px;
21
+ --sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
22
+ --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
23
+ --ease-in: cubic-bezier(0.16, 1, 0.3, 1);
24
+ }
25
+
26
+ * { box-sizing: border-box; }
27
+
28
+ html, body { height: 100%; }
29
+
30
+ body {
31
+ margin: 0;
32
+ background: var(--bg);
33
+ color: var(--text);
34
+ font-family: var(--sans);
35
+ font-size: 14px;
36
+ line-height: 1.5;
37
+ display: flex;
38
+ flex-direction: column;
39
+ }
40
+
41
+ .sr-only {
42
+ position: absolute;
43
+ width: 1px;
44
+ height: 1px;
45
+ padding: 0;
46
+ margin: -1px;
47
+ overflow: hidden;
48
+ clip: rect(0 0 0 0);
49
+ white-space: nowrap;
50
+ border: 0;
51
+ }
52
+
53
+ /* ── Header ─────────────────────────────────────────────────────────── */
54
+
55
+ .app-head {
56
+ display: flex;
57
+ flex-wrap: wrap;
58
+ align-items: baseline;
59
+ gap: 8px 20px;
60
+ padding: 16px 20px 12px;
61
+ border-bottom: 1px solid var(--border);
62
+ background: var(--surface-2);
63
+ }
64
+
65
+ .app-head h1 {
66
+ margin: 0;
67
+ font-size: 18px;
68
+ font-weight: 700;
69
+ letter-spacing: -0.02em;
70
+ }
71
+
72
+ .app-head .run-meta {
73
+ color: var(--muted);
74
+ font-family: var(--mono);
75
+ font-size: 11px;
76
+ letter-spacing: 0.02em;
77
+ }
78
+
79
+ .app-head .run-meta strong { color: var(--text); font-weight: 600; }
80
+
81
+ .counter {
82
+ display: inline-flex;
83
+ align-items: center;
84
+ gap: 6px;
85
+ font-family: var(--mono);
86
+ font-size: 11px;
87
+ color: var(--muted);
88
+ font-variant-numeric: tabular-nums;
89
+ }
90
+
91
+ .counter strong { color: var(--text); font-weight: 600; }
92
+
93
+ /* ── Toolbar ────────────────────────────────────────────────────────── */
94
+
95
+ .toolbar {
96
+ display: flex;
97
+ flex-wrap: wrap;
98
+ align-items: end;
99
+ gap: 10px 14px;
100
+ padding: 10px 20px;
101
+ border-bottom: 1px solid var(--border);
102
+ }
103
+
104
+ .field { display: flex; flex-direction: column; gap: 4px; }
105
+ .field > label {
106
+ font-family: var(--mono);
107
+ font-size: 10px;
108
+ letter-spacing: 0.12em;
109
+ text-transform: uppercase;
110
+ color: var(--muted);
111
+ }
112
+
113
+ .field select,
114
+ .field input {
115
+ background: var(--surface);
116
+ color: var(--text);
117
+ border: 1px solid var(--border-strong);
118
+ border-radius: var(--radius);
119
+ padding: 6px 8px;
120
+ font-family: var(--mono);
121
+ font-size: 12px;
122
+ min-width: 150px;
123
+ transition: border-color 220ms var(--ease-in);
124
+ }
125
+
126
+ .field select:hover,
127
+ .field input:hover { border-color: var(--muted); }
128
+
129
+ .field select:focus-visible,
130
+ .field input:focus-visible,
131
+ .btn:focus-visible,
132
+ .card:focus-visible {
133
+ outline: 2px solid var(--accent);
134
+ outline-offset: 2px;
135
+ }
136
+
137
+ .btn {
138
+ background: var(--surface);
139
+ color: var(--text);
140
+ border: 1px solid var(--border-strong);
141
+ border-radius: var(--radius);
142
+ padding: 6px 10px;
143
+ font-family: var(--mono);
144
+ font-size: 11px;
145
+ cursor: pointer;
146
+ transition: border-color 220ms var(--ease-in), transform 80ms var(--ease-in);
147
+ }
148
+
149
+ .btn:hover { border-color: var(--muted); }
150
+ .btn:active { transform: scale(0.97); }
151
+ .btn--ghost { color: var(--muted); }
152
+
153
+ .toolbar-note {
154
+ margin-left: auto;
155
+ font-family: var(--mono);
156
+ font-size: 11px;
157
+ color: var(--muted);
158
+ }
159
+
160
+ .toolbar-note .source { color: var(--text); }
161
+
162
+ /* ── Board ──────────────────────────────────────────────────────────── */
163
+
164
+ .board {
165
+ flex: 1;
166
+ display: grid;
167
+ grid-auto-flow: column;
168
+ grid-auto-columns: minmax(250px, 1fr);
169
+ gap: 12px;
170
+ padding: 14px 20px 20px;
171
+ overflow-x: auto;
172
+ align-items: start;
173
+ }
174
+
175
+ .column {
176
+ background: var(--surface-2);
177
+ border: 1px solid var(--border);
178
+ border-radius: var(--radius);
179
+ display: flex;
180
+ flex-direction: column;
181
+ min-height: 160px;
182
+ max-height: calc(100vh - 210px);
183
+ }
184
+
185
+ .column-head {
186
+ display: flex;
187
+ align-items: center;
188
+ justify-content: space-between;
189
+ gap: 8px;
190
+ padding: 8px 10px;
191
+ border-bottom: 1px solid var(--border);
192
+ }
193
+
194
+ .column-title {
195
+ margin: 0;
196
+ font-family: var(--mono);
197
+ font-size: 11px;
198
+ font-weight: 600;
199
+ letter-spacing: 0.12em;
200
+ color: var(--text);
201
+ }
202
+
203
+ .column[data-state="done"] .column-title { color: var(--accent); }
204
+ .column[data-state="blocked"] .column-title,
205
+ .column[data-state="escalated"] .column-title,
206
+ .column[data-state="rework"] .column-title { color: var(--warn); }
207
+
208
+ .column-count {
209
+ font-family: var(--mono);
210
+ font-size: 11px;
211
+ color: var(--muted);
212
+ font-variant-numeric: tabular-nums;
213
+ }
214
+
215
+ .column-body {
216
+ display: flex;
217
+ flex-direction: column;
218
+ gap: 8px;
219
+ padding: 8px;
220
+ overflow-y: auto;
221
+ min-height: 88px;
222
+ transition: background-color 220ms var(--ease-in), box-shadow 220ms var(--ease-in);
223
+ }
224
+
225
+ .column-body.drop-target {
226
+ background: rgba(74, 222, 128, 0.06);
227
+ box-shadow: inset 0 0 0 1px var(--accent);
228
+ }
229
+
230
+ .column-empty {
231
+ margin: 6px 2px;
232
+ font-family: var(--mono);
233
+ font-size: 11px;
234
+ color: var(--muted);
235
+ }
236
+
237
+ .column-hint { color: var(--border-strong); }
238
+
239
+ /* ── Card ───────────────────────────────────────────────────────────── */
240
+
241
+ .card {
242
+ background: var(--surface);
243
+ border: 1px solid var(--border);
244
+ border-radius: var(--radius);
245
+ padding: 9px 10px;
246
+ cursor: pointer;
247
+ transition: border-color 220ms var(--ease-in), transform 80ms var(--ease-in), opacity 220ms var(--ease-in);
248
+ }
249
+
250
+ .card:hover { border-color: var(--border-strong); }
251
+ .card:active { transform: scale(0.99); }
252
+ .card.dragging { opacity: 0.45; }
253
+
254
+ .card[data-moved="true"] { border-color: var(--warn); }
255
+
256
+ .card-top {
257
+ display: flex;
258
+ align-items: center;
259
+ justify-content: space-between;
260
+ gap: 8px;
261
+ }
262
+
263
+ .card-id {
264
+ font-family: var(--mono);
265
+ font-size: 11px;
266
+ font-weight: 600;
267
+ letter-spacing: 0.04em;
268
+ color: var(--text);
269
+ }
270
+
271
+ .badge {
272
+ font-family: var(--mono);
273
+ font-size: 10px;
274
+ letter-spacing: 0.06em;
275
+ padding: 1px 5px;
276
+ border: 1px solid var(--border-strong);
277
+ border-radius: var(--radius);
278
+ color: var(--muted);
279
+ text-transform: lowercase;
280
+ }
281
+
282
+ .badge--type { color: var(--muted); }
283
+
284
+ .badge--state { text-transform: uppercase; color: var(--text); }
285
+ .badge--state[data-state="done"] { color: var(--accent); border-color: var(--accent); }
286
+ .badge--state[data-state="blocked"],
287
+ .badge--state[data-state="rework"],
288
+ .badge--state[data-state="escalated"] { color: var(--warn); border-color: var(--warn); }
289
+
290
+ .card-title {
291
+ margin: 5px 0 7px;
292
+ font-size: 12.5px;
293
+ font-weight: 600;
294
+ line-height: 1.35;
295
+ letter-spacing: -0.01em;
296
+ }
297
+
298
+ .card-people {
299
+ display: flex;
300
+ align-items: center;
301
+ gap: 6px;
302
+ margin-bottom: 7px;
303
+ }
304
+
305
+ .person {
306
+ display: inline-flex;
307
+ align-items: center;
308
+ gap: 4px;
309
+ min-width: 0;
310
+ font-family: var(--mono);
311
+ font-size: 10px;
312
+ color: var(--muted);
313
+ }
314
+
315
+ .person--reviewer { opacity: 0.85; }
316
+ .person--reviewer .avatar { outline: 1px dashed var(--border-strong); outline-offset: 1px; }
317
+
318
+ .person-name {
319
+ max-width: 78px;
320
+ overflow: hidden;
321
+ text-overflow: ellipsis;
322
+ white-space: nowrap;
323
+ }
324
+
325
+ .person--unassigned .person-name { color: var(--border-strong); font-style: italic; }
326
+
327
+ .avatar {
328
+ display: inline-flex;
329
+ align-items: center;
330
+ justify-content: center;
331
+ width: 18px;
332
+ height: 18px;
333
+ border-radius: 50%;
334
+ background: hsl(var(--avatar-hue, 210) 26% 24%);
335
+ color: var(--text);
336
+ font-family: var(--mono);
337
+ font-size: 8.5px;
338
+ font-weight: 600;
339
+ letter-spacing: 0.02em;
340
+ flex: none;
341
+ }
342
+
343
+ .avatar--empty {
344
+ background: transparent;
345
+ border: 1px dashed var(--border-strong);
346
+ color: var(--border-strong);
347
+ }
348
+
349
+ .rejections {
350
+ margin-left: auto;
351
+ font-family: var(--mono);
352
+ font-size: 10px;
353
+ color: var(--muted);
354
+ font-variant-numeric: tabular-nums;
355
+ }
356
+
357
+ .rejections[data-rejections="0"] { color: var(--border-strong); }
358
+
359
+ .checklist {
360
+ list-style: none;
361
+ margin: 0 0 7px;
362
+ padding: 0;
363
+ display: flex;
364
+ flex-direction: column;
365
+ gap: 3px;
366
+ }
367
+
368
+ .check-item {
369
+ display: flex;
370
+ gap: 5px;
371
+ align-items: flex-start;
372
+ font-size: 11px;
373
+ line-height: 1.35;
374
+ color: var(--muted);
375
+ }
376
+
377
+ .check-box {
378
+ font-family: var(--mono);
379
+ font-size: 10px;
380
+ width: 11px;
381
+ flex: none;
382
+ text-align: center;
383
+ }
384
+
385
+ .check-item.is-checked .check-box { color: var(--accent); }
386
+ .check-item.is-checked .check-text { color: var(--text); }
387
+ .check-item.is-failed .check-box { color: var(--danger); }
388
+ .check-item.is-failed .check-text { color: var(--danger); }
389
+
390
+ .check-text {
391
+ display: -webkit-box;
392
+ -webkit-line-clamp: 2;
393
+ -webkit-box-orient: vertical;
394
+ overflow: hidden;
395
+ }
396
+
397
+ .check-more {
398
+ font-family: var(--mono);
399
+ font-size: 10px;
400
+ color: var(--border-strong);
401
+ padding-left: 16px;
402
+ }
403
+
404
+ .check-progress {
405
+ display: flex;
406
+ align-items: center;
407
+ gap: 6px;
408
+ padding-left: 16px;
409
+ }
410
+
411
+ .check-progress-label {
412
+ font-family: var(--mono);
413
+ font-size: 9.5px;
414
+ color: var(--muted);
415
+ white-space: nowrap;
416
+ font-variant-numeric: tabular-nums;
417
+ }
418
+
419
+ .check-progress-track {
420
+ flex: 1;
421
+ height: 3px;
422
+ background: var(--border);
423
+ border-radius: 2px;
424
+ overflow: hidden;
425
+ }
426
+
427
+ .check-progress-fill {
428
+ display: block;
429
+ height: 100%;
430
+ background: var(--accent);
431
+ }
432
+
433
+ .checklist-empty {
434
+ margin: 0 0 7px;
435
+ font-family: var(--mono);
436
+ font-size: 10px;
437
+ color: var(--border-strong);
438
+ }
439
+
440
+ .card-gates {
441
+ display: flex;
442
+ flex-wrap: wrap;
443
+ gap: 4px;
444
+ padding-top: 7px;
445
+ border-top: 1px solid var(--border);
446
+ }
447
+
448
+ .gate-badge {
449
+ display: inline-flex;
450
+ align-items: center;
451
+ gap: 4px;
452
+ font-family: var(--mono);
453
+ font-size: 9.5px;
454
+ letter-spacing: 0.04em;
455
+ padding: 1px 5px;
456
+ border-radius: var(--radius);
457
+ border: 1px solid currentColor;
458
+ white-space: nowrap;
459
+ }
460
+
461
+ .gate-badge .gate-key { opacity: 0.65; }
462
+
463
+ .gate-badge--pass { color: var(--accent); }
464
+ .gate-badge--fail { color: var(--danger); }
465
+ .gate-badge--not-run { color: var(--warn); }
466
+ .gate-badge--na { color: var(--muted); }
467
+
468
+ /* ── Modal ──────────────────────────────────────────────────────────── */
469
+
470
+ dialog#card-modal {
471
+ width: min(920px, calc(100vw - 32px));
472
+ max-height: calc(100vh - 48px);
473
+ padding: 0;
474
+ background: var(--surface);
475
+ color: var(--text);
476
+ border: 1px solid var(--border-strong);
477
+ border-radius: var(--radius);
478
+ overflow: hidden;
479
+ }
480
+
481
+ dialog#card-modal::backdrop { background: rgba(4, 6, 8, 0.72); }
482
+
483
+ .modal-shell { display: flex; flex-direction: column; max-height: inherit; }
484
+
485
+ .modal-head {
486
+ position: relative;
487
+ padding: 14px 18px 12px;
488
+ border-bottom: 1px solid var(--border);
489
+ background: var(--surface-2);
490
+ }
491
+
492
+ .modal-ident { display: flex; align-items: center; gap: 8px; }
493
+
494
+ .modal-title {
495
+ margin: 6px 0 4px;
496
+ font-size: 17px;
497
+ font-weight: 700;
498
+ letter-spacing: -0.02em;
499
+ line-height: 1.3;
500
+ max-width: 62ch;
501
+ }
502
+
503
+ .modal-meta {
504
+ margin: 0;
505
+ font-family: var(--mono);
506
+ font-size: 10.5px;
507
+ color: var(--muted);
508
+ font-variant-numeric: tabular-nums;
509
+ }
510
+
511
+ .modal-close {
512
+ position: absolute;
513
+ top: 12px;
514
+ right: 12px;
515
+ background: transparent;
516
+ border: 1px solid var(--border-strong);
517
+ border-radius: var(--radius);
518
+ color: var(--muted);
519
+ font-family: var(--mono);
520
+ font-size: 11px;
521
+ padding: 3px 7px;
522
+ cursor: pointer;
523
+ transition: color 220ms var(--ease-in), border-color 220ms var(--ease-in);
524
+ }
525
+
526
+ .modal-close:hover { color: var(--text); border-color: var(--muted); }
527
+
528
+ .modal-body {
529
+ padding: 14px 18px 20px;
530
+ overflow-y: auto;
531
+ display: flex;
532
+ flex-direction: column;
533
+ gap: 18px;
534
+ }
535
+
536
+ .detail-section h3 {
537
+ margin: 0 0 8px;
538
+ font-size: 12px;
539
+ font-weight: 600;
540
+ letter-spacing: 0.12em;
541
+ text-transform: uppercase;
542
+ color: var(--muted);
543
+ }
544
+
545
+ .detail-count {
546
+ font-family: var(--mono);
547
+ font-size: 10px;
548
+ color: var(--text);
549
+ letter-spacing: 0;
550
+ margin-left: 4px;
551
+ }
552
+
553
+ .scope-block { margin-bottom: 8px; }
554
+
555
+ .scope-block h4,
556
+ .gate-block-title {
557
+ margin: 0 0 4px;
558
+ font-family: var(--mono);
559
+ font-size: 10px;
560
+ font-weight: 600;
561
+ letter-spacing: 0.08em;
562
+ text-transform: uppercase;
563
+ color: var(--muted);
564
+ }
565
+
566
+ .scope-block--may h4 { color: var(--accent); }
567
+ .scope-block--forbidden h4 { color: var(--danger); }
568
+
569
+ .scope-list {
570
+ list-style: none;
571
+ margin: 0;
572
+ padding: 0;
573
+ display: flex;
574
+ flex-wrap: wrap;
575
+ gap: 4px;
576
+ }
577
+
578
+ .scope-list code,
579
+ .artifact-path,
580
+ .chip {
581
+ font-family: var(--mono);
582
+ font-size: 10.5px;
583
+ background: var(--bg);
584
+ border: 1px solid var(--border);
585
+ border-radius: var(--radius);
586
+ padding: 1px 5px;
587
+ color: var(--text);
588
+ }
589
+
590
+ .scope-block--forbidden .scope-list code { color: var(--danger); }
591
+ .scope-block--may .scope-list code { color: var(--accent); }
592
+
593
+ .detail-meta {
594
+ font-family: var(--mono);
595
+ font-size: 10.5px;
596
+ color: var(--muted);
597
+ margin: 6px 0 0;
598
+ }
599
+
600
+ .detail-gate-text { margin: 0 0 10px; }
601
+ .detail-gate-text code {
602
+ font-family: var(--mono);
603
+ font-size: 11px;
604
+ color: var(--muted);
605
+ }
606
+
607
+ .gate-block { margin-bottom: 12px; }
608
+
609
+ .gate-table {
610
+ width: 100%;
611
+ border-collapse: collapse;
612
+ font-size: 11px;
613
+ }
614
+
615
+ .gate-table th {
616
+ text-align: left;
617
+ font-family: var(--mono);
618
+ font-size: 9.5px;
619
+ letter-spacing: 0.08em;
620
+ text-transform: uppercase;
621
+ color: var(--muted);
622
+ font-weight: 600;
623
+ padding: 4px 6px;
624
+ border-bottom: 1px solid var(--border);
625
+ }
626
+
627
+ .gate-table td {
628
+ padding: 5px 6px;
629
+ border-bottom: 1px solid var(--border);
630
+ vertical-align: top;
631
+ color: var(--muted);
632
+ }
633
+
634
+ .gate-table td code { font-family: var(--mono); font-size: 10px; color: var(--text); }
635
+
636
+ .phase-reason {
637
+ margin: 3px 0 0;
638
+ font-family: var(--mono);
639
+ font-size: 10px;
640
+ color: var(--warn);
641
+ }
642
+
643
+ .detail-acceptance {
644
+ margin: 0;
645
+ padding: 0;
646
+ list-style: none;
647
+ display: flex;
648
+ flex-direction: column;
649
+ gap: 8px;
650
+ }
651
+
652
+ .acceptance-item {
653
+ border-left: 2px solid var(--border);
654
+ padding: 2px 0 2px 9px;
655
+ }
656
+
657
+ .acceptance-item[data-acceptance-state="pass"] { border-left-color: var(--accent); }
658
+ .acceptance-item[data-acceptance-state="fail"] { border-left-color: var(--danger); }
659
+ .acceptance-item[data-acceptance-state="not_run"] { border-left-color: var(--warn); }
660
+
661
+ .acceptance-text {
662
+ margin: 3px 0 4px;
663
+ font-size: 12px;
664
+ line-height: 1.45;
665
+ max-width: 78ch;
666
+ }
667
+
668
+ .acceptance-evidence-list {
669
+ margin: 0;
670
+ padding: 0 0 0 2px;
671
+ list-style: none;
672
+ display: flex;
673
+ flex-direction: column;
674
+ gap: 2px;
675
+ }
676
+
677
+ .acceptance-evidence {
678
+ display: flex;
679
+ flex-wrap: wrap;
680
+ gap: 6px;
681
+ align-items: baseline;
682
+ font-size: 10.5px;
683
+ }
684
+
685
+ .acceptance-evidence code { font-family: var(--mono); font-size: 10px; color: var(--muted); }
686
+
687
+ .detail-list {
688
+ margin: 0;
689
+ padding: 0;
690
+ list-style: none;
691
+ display: flex;
692
+ flex-direction: column;
693
+ gap: 9px;
694
+ }
695
+
696
+ .evidence-item {
697
+ border-left: 2px solid var(--border);
698
+ padding-left: 9px;
699
+ font-size: 11px;
700
+ }
701
+
702
+ .evidence-item[data-evidence-passed="true"] { border-left-color: var(--accent); }
703
+ .evidence-item[data-evidence-passed="false"] { border-left-color: var(--danger); }
704
+
705
+ .evidence-ref {
706
+ font-family: var(--mono);
707
+ font-size: 10.5px;
708
+ color: var(--text);
709
+ word-break: break-word;
710
+ }
711
+
712
+ .evidence-meta {
713
+ display: block;
714
+ font-family: var(--mono);
715
+ font-size: 9.5px;
716
+ color: var(--muted);
717
+ margin-top: 2px;
718
+ }
719
+
720
+ .evidence-note {
721
+ margin: 3px 0 0;
722
+ color: var(--muted);
723
+ font-size: 11px;
724
+ max-width: 78ch;
725
+ }
726
+
727
+ .evidence-req {
728
+ margin: 3px 0 0;
729
+ font-size: 10.5px;
730
+ color: var(--border-strong);
731
+ max-width: 78ch;
732
+ }
733
+
734
+ .detail-empty {
735
+ font-family: var(--mono);
736
+ font-size: 10.5px;
737
+ color: var(--border-strong);
738
+ margin: 0;
739
+ padding: 4px 0;
740
+ }
741
+
742
+ .artifact-list {
743
+ margin: 0 0 8px;
744
+ padding: 0;
745
+ list-style: none;
746
+ display: flex;
747
+ flex-wrap: wrap;
748
+ gap: 8px;
749
+ align-items: center;
750
+ font-family: var(--mono);
751
+ font-size: 11px;
752
+ }
753
+
754
+ .artifact-link {
755
+ color: var(--accent);
756
+ text-decoration: none;
757
+ border-bottom: 1px solid transparent;
758
+ transition: border-color 220ms var(--ease-in);
759
+ }
760
+
761
+ .artifact-link:hover { border-bottom-color: var(--accent); }
762
+
763
+ .detail-result {
764
+ margin: 8px 0 0;
765
+ font-family: var(--mono);
766
+ font-size: 10.5px;
767
+ color: var(--muted);
768
+ max-width: 78ch;
769
+ border-left: 2px solid var(--border);
770
+ padding-left: 9px;
771
+ }
772
+
773
+ /* ── Footer ─────────────────────────────────────────────────────────── */
774
+
775
+ .app-foot {
776
+ display: flex;
777
+ flex-wrap: wrap;
778
+ gap: 16px;
779
+ align-items: center;
780
+ padding: 8px 20px;
781
+ border-top: 1px solid var(--border);
782
+ font-family: var(--mono);
783
+ font-size: 10px;
784
+ color: var(--muted);
785
+ }
786
+
787
+ .app-foot .read-only { color: var(--warn); }
788
+
789
+ /* ── Responsive ─────────────────────────────────────────────────────── */
790
+
791
+ @media (max-width: 900px) {
792
+ .board {
793
+ grid-auto-columns: minmax(78vw, 1fr);
794
+ padding: 12px;
795
+ }
796
+ .toolbar-note { margin-left: 0; width: 100%; }
797
+ .column { max-height: none; }
798
+ .board { max-height: none; }
799
+ }
800
+
801
+ @media (prefers-reduced-motion: reduce) {
802
+ *, *::before, *::after {
803
+ transition-duration: 0.001ms !important;
804
+ animation-duration: 0.001ms !important;
805
+ }
806
+ .card, .btn, .column-body { transform: none !important; }
807
+ }
808
+ </style>
809
+ </head>
810
+ <body>
811
+
812
+ <header class="app-head">
813
+ <h1>Kanban de la Oficina</h1>
814
+ <p class="run-meta" id="run-meta">cargando corrida…</p>
815
+ <span class="counter">cards <strong id="count-total">0</strong></span>
816
+ <span class="counter">visibles <strong id="count-visible">0</strong></span>
817
+ </header>
818
+
819
+ <div class="toolbar" role="search" aria-label="Filtros del tablero">
820
+ <div class="field">
821
+ <label for="filter-owner">owner</label>
822
+ <select id="filter-owner" name="owner"><option value="all">todos</option></select>
823
+ </div>
824
+ <div class="field">
825
+ <label for="filter-type">tipo</label>
826
+ <select id="filter-type" name="type"><option value="all">todos</option></select>
827
+ </div>
828
+ <div class="field">
829
+ <label for="filter-state">estado</label>
830
+ <select id="filter-state" name="state"><option value="all">todos</option></select>
831
+ </div>
832
+ <div class="field">
833
+ <label for="filter-search">buscar</label>
834
+ <input id="filter-search" name="q" type="search" placeholder="id, título, tipo" autocomplete="off">
835
+ </div>
836
+ <button class="btn btn--ghost" id="reset-view" type="button">reset vista</button>
837
+ <button class="btn btn--ghost" id="refresh-data" type="button">recargar data</button>
838
+ <p class="toolbar-note">
839
+ fuente <span class="source" id="data-source">snapshot embebido</span>
840
+ </p>
841
+ </div>
842
+
843
+ <main class="board" id="board" aria-label="Tablero Kanban de cards">
844
+
845
+ <section class="column" data-state="todo" aria-labelledby="col-todo-title">
846
+ <div class="column-head">
847
+ <h2 class="column-title" id="col-todo-title">TODO</h2>
848
+ <span class="column-count" data-count="todo" aria-label="cards en TODO">0</span>
849
+ </div>
850
+ <div class="column-body" data-cards="todo" data-dropzone="true"></div>
851
+ </section>
852
+
853
+ <section class="column" data-state="in_progress" aria-labelledby="col-in_progress-title">
854
+ <div class="column-head">
855
+ <h2 class="column-title" id="col-in_progress-title">IN_PROGRESS</h2>
856
+ <span class="column-count" data-count="in_progress" aria-label="cards en IN_PROGRESS">0</span>
857
+ </div>
858
+ <div class="column-body" data-cards="in_progress" data-dropzone="true"></div>
859
+ </section>
860
+
861
+ <section class="column" data-state="review" aria-labelledby="col-review-title">
862
+ <div class="column-head">
863
+ <h2 class="column-title" id="col-review-title">REVIEW</h2>
864
+ <span class="column-count" data-count="review" aria-label="cards en REVIEW">0</span>
865
+ </div>
866
+ <div class="column-body" data-cards="review" data-dropzone="true"></div>
867
+ </section>
868
+
869
+ <section class="column" data-state="done" aria-labelledby="col-done-title">
870
+ <div class="column-head">
871
+ <h2 class="column-title" id="col-done-title">DONE</h2>
872
+ <span class="column-count" data-count="done" aria-label="cards en DONE">0</span>
873
+ </div>
874
+ <div class="column-body" data-cards="done" data-dropzone="true"></div>
875
+ </section>
876
+
877
+ <section class="column" data-state="rework" aria-labelledby="col-rework-title">
878
+ <div class="column-head">
879
+ <h2 class="column-title" id="col-rework-title">REWORK</h2>
880
+ <span class="column-count" data-count="rework" aria-label="cards en REWORK">0</span>
881
+ </div>
882
+ <div class="column-body" data-cards="rework" data-dropzone="true"></div>
883
+ </section>
884
+
885
+ <section class="column" data-state="blocked" aria-labelledby="col-blocked-title">
886
+ <div class="column-head">
887
+ <h2 class="column-title" id="col-blocked-title">BLOCKED</h2>
888
+ <span class="column-count" data-count="blocked" aria-label="cards en BLOCKED">0</span>
889
+ </div>
890
+ <div class="column-body" data-cards="blocked" data-dropzone="true"></div>
891
+ </section>
892
+
893
+ <section class="column" data-state="escalated" aria-labelledby="col-escalated-title">
894
+ <div class="column-head">
895
+ <h2 class="column-title" id="col-escalated-title">ESCALATED</h2>
896
+ <span class="column-count" data-count="escalated" aria-label="cards en ESCALATED">0</span>
897
+ </div>
898
+ <div class="column-body" data-cards="escalated" data-dropzone="true"></div>
899
+ </section>
900
+
901
+ </main>
902
+
903
+ <footer class="app-foot">
904
+ <span class="read-only">read-only: drag-drop es visual, no persiste</span>
905
+ <span>legend</span>
906
+ <span class="gate-badge gate-badge--pass">PASS</span>
907
+ <span class="gate-badge gate-badge--fail">FAIL</span>
908
+ <span class="gate-badge gate-badge--not-run">NOT_RUN</span>
909
+ <span class="gate-badge gate-badge--na">N/A</span>
910
+ <span>click en card abre el detalle completo</span>
911
+ </footer>
912
+
913
+ <p class="sr-only" id="live-region" role="status" aria-live="polite"></p>
914
+
915
+ <dialog id="card-modal" aria-labelledby="card-modal-title">
916
+ <div class="modal-shell">
917
+ <div id="card-modal-content"></div>
918
+ <button class="modal-close" id="card-modal-close" type="button">cerrar (esc)</button>
919
+ </div>
920
+ </dialog>
921
+
922
+ <!-- Dataset embebido: lo regenera `node dashboard/build-kanban-data.mjs` desde .oficina/.
923
+ Se embebe para que el tablero funcione abriendo el archivo con file:// (sin CORS). -->
924
+ <!--BEGIN_DATASET-->
925
+ <script type="application/json" id="kanban-dataset">{"meta":{"runId":"run-87fc0537","objective":"","status":"running","iteration":0,"updatedAt":"2026-10-29T17:39:24.040Z","generatedAt":"2026-10-29T17:42:05.317Z","generator":"dashboard/build-kanban-data.mjs"},"agents":{"e-testing-api-tester":{"id":"e-testing-api-tester","displayName":"API Tester","role":"testing-api-tester","state":"idle_available","currentCard":null},"e-design-ui-finish-gate-reviewer":{"id":"e-design-ui-finish-gate-reviewer","displayName":"UI Finish-Gate Reviewer","role":"design-ui-finish-gate-reviewer","state":"idle_available","currentCard":null},"e-xr-immersive-developer":{"id":"e-xr-immersive-developer","displayName":"XR Immersive Developer","role":"xr-immersive-developer","state":"working","currentCard":"T-07"}},"cards":[{"id":"T-00","title":"Cablear el guard anti-gate-verde-vacío en el runner de gates","type":"bugfix","state":"done","owner":"e-testing-api-tester","reviewer":null,"requiredCapabilities":["typescript","testing","vitest","node"],"scope":{"mayTouch":["src/core/gates.ts","src/core/office.ts","tests/gates.test.ts"],"forbidden":["src/hooks/**","src/cli.ts","dist/**","node_modules/**",".oficina/**"]},"acceptance":["runTechnicalGates participa del guard: si la fase test sale con exit 0 pero testsActuallyRan es false, la fase test se reporta como not_run con reason explícita de gate verde vacío (no PASS silencioso ni silencioso en otros campos)","si testsActuallyRan es true (p. ej. conteo legible '5 passed' o 'Tests: 5'), la fase test sigue en PASS sin regresión","consolidate con ese resultado produce technicalOutcome fail/not_run (passed=false) y renderGate muestra el motivo de forma legible","pmOfficeOffice.runGates / flujo Office.runGates queda cableado al guard vía runTechnicalGates (sin duplicar lógica)","regresión cubierta por tests vitest nuevos en tests/gates.test.ts (existen ya tests de testsActuallyRan; se agregan los del cableado)","npm run typecheck sin errores y npm test todo en verde (las 456 existentes + nuevas)"],"gate":"npm run typecheck && npm test (exit 0, 0 tests fallidos)","dependsOn":[],"attempts":1,"rejections":1,"artifacts":".oficina\\artifacts\\T-00","result":"APROBADO: REWORK 2 verificado (secuencia post-gate FAIL de secrecy): fixtures literales de secretos en tests/gates.test.ts neutralizados por concatenación — runtime byte-a-byte idéntico (evaluado directo: FAKE_SK_LIVE ≡ sk_live_abcdef123456, FAKE_AKIA ≡ AKIAIOSFODNN7EXAMPLE), los tests de comportamiento de runSecurityGate siguen detectando secretos reales (29/29 en el archivo). Scanner sin cambios (decisión correcta: no excluir tests/** — no oculta secretos reales). Escaneo real del revisor: tests/gates.test.ts → [], src/core/gates.ts → [], src/core/office.ts → [], repositorio entero → []. C-001 sigue resuelto (sin deltas en gates.ts/office.ts desde mi aprobación previa). Findings ≥80: ninguno. Verificación independiente: typecheck exit 0; npm test 27 archivos / 466 passed, exit 0. Review completo en .oficina/artifacts/T-00/review-rework2.md.","helpRequests":[],"helpHops":0},{"id":"T-01","title":"Explorar estructura .oficina/ y definir schema de datos para el dashboard","type":"exploration","state":"todo","owner":"e-design-ui-finish-gate-reviewer","reviewer":"e-testing-api-tester","requiredCapabilities":["typescript","node","filesystem"],"scope":{"mayTouch":[".oficina/**"],"forbidden":["src/**","dist/**","node_modules/**"]},"acceptance":["Mapear todos los archivos en .oficina/ (state.json, roster.json, bus/*, evidence/*, leases/*, context.md, cost.json)","Definir TypeScript interfaces para cada tipo de dato que el dashboard consumira","Identificar que archivos cambian en tiempo real y frecuencia de actualizacion","Documentar estructura de mensajes en bus/ y formato de envelopes"],"gate":"TypeScript interfaces validadas y documentadas en artifacts/T-01/handoff.md","dependsOn":[],"attempts":0,"rejections":0,"artifacts":".oficina\\artifacts\\T-01","result":null,"helpRequests":[],"helpHops":0},{"id":"T-02","title":"Arquitectura del dashboard: single-page app con polling/WebSocket sobre archivos","type":"design","state":"todo","owner":"e-design-ui-finish-gate-reviewer","reviewer":"e-testing-api-tester","requiredCapabilities":["frontend-architecture","typescript","real-time-systems"],"scope":{"mayTouch":["dashboard/**"],"forbidden":["src/**",".oficina/**","dist/**","node_modules/**"]},"acceptance":["Diseno de arquitectura: HTML unico + CSS + JS vanilla/Alpine.js sin build step","Estrategia de polling: intervalos adaptativos por tipo de dato (state.json: 500ms, bus: 1s, roster: 2s)","WebSocket fallback si se detecta servidor (opcional, documentar)","Tema oscuro/claro con design tokens consistentes (OKLCH)","Responsive: desktop paneles lado a lado, mobile tabs/accordion","Accesibilidad: ARIA live regions, keyboard navigation, WCAG AA contrast"],"gate":"Architecture decision document en artifacts/T-02/handoff.md con decisiones y trade-offs","dependsOn":["T-01"],"attempts":0,"rejections":0,"artifacts":".oficina\\artifacts\\T-02","result":null,"helpRequests":[],"helpHops":0},{"id":"T-03","title":"Implementar mesa visual en vivo (panel principal)","type":"implementation","state":"todo","owner":"e-design-ui-finish-gate-reviewer","reviewer":"e-testing-api-tester","requiredCapabilities":["typescript","frontend","svg-canvas","real-time-ui"],"scope":{"mayTouch":["dashboard/**"],"forbidden":["src/**",".oficina/**","dist/**","node_modules/**"]},"acceptance":["Panel que lee roster.json y state.json y muestra agentes con estado visual (working/idle/no-heartbeat/dead)","Cards sin owner visibles con badge de 'unassigned'","Avatares de agentes con iniciales, color por rol, indicador de heartbeat (pulse animation)","Tooltip con capabilities, currentCard, idleSince, helpGiven/Received","Actualizacion en tiempo real via polling (500ms)","Keyboard navigation entre agentes, Enter para ver detalle"],"gate":"dashboard/mesa.html funcional + npm run typecheck + npm test pass","dependsOn":["T-02"],"attempts":0,"rejections":0,"artifacts":".oficina\\artifacts\\T-03","result":null,"helpRequests":[],"helpHops":0},{"id":"T-04","title":"Implementar bus de mensajes visible con threading y prioridades","type":"implementation","state":"todo","owner":"e-design-ui-finish-gate-reviewer","reviewer":"e-testing-api-tester","requiredCapabilities":["typescript","frontend","data-visualization","real-time-ui"],"scope":{"mayTouch":["dashboard/**"],"forbidden":["src/**",".oficina/**","dist/**","node_modules/**"]},"acceptance":["Panel que lee bus/peer/ y canales de coordinadores (coord-command, coord-arch, coord-flow)","Visualizacion tipo sequence diagram: flechas entre avatares, colores por prioridad (critical=red, high=orange, normal=blue, low=gray)","Threading visual: inReplyTo agrupa mensajes en hilos colapsables/expandibles","Filtros por kind (HELP_REQUEST, HELP_ACCEPT, CLAIM, RELEASE, SPAWN_REQUEST, etc.)","Timeline con timestamps relativos, auto-scroll opcional","Click en mensaje muestra detalle completo (body, ttl, hops, idempotency_key)"],"gate":"dashboard/bus.html funcional + typecheck + tests pass","dependsOn":["T-02"],"attempts":0,"rejections":0,"artifacts":".oficina\\artifacts\\T-04","result":null,"helpRequests":[],"helpHops":0},{"id":"T-05","title":"Implementar panel de debate/brainstorming trazable (HELP_REQUEST broadcast + respuestas)","type":"implementation","state":"todo","owner":"e-xr-immersive-developer","reviewer":null,"requiredCapabilities":["typescript","frontend","chat-ui","threading"],"scope":{"mayTouch":["dashboard/**"],"forbidden":["src/**",".oficina/**","dist/**","node_modules/**"]},"acceptance":["Deteccion automatica de HELP_REQUEST con to:['*'] (broadcast)","Vista tipo chat: burbujas con avatar, timestamp, agente, expandible para ver body completo","Respuestas (HELP_ACCEPT, HELP_DECLINE, HELP_PARTIAL, HELP_QUESTION) anidadas bajo el request","Indicador visual de estado: pending -> accepted/declined/partial -> completed","Colapso/expansion de hilos completos, contador de respuestas","ARIA live region para anuncios de nuevos mensajes en hilos abiertos"],"gate":"dashboard/debate.html funcional + typecheck + tests pass","dependsOn":["T-02"],"attempts":0,"rejections":0,"artifacts":".oficina\\artifacts\\T-05","result":null,"helpRequests":[],"helpHops":0},{"id":"T-06","title":"Implementar panel de intervencion del jefe (Commander/Integrator/Architect decisions)","type":"implementation","state":"todo","owner":"e-xr-immersive-developer","reviewer":null,"requiredCapabilities":["typescript","frontend","decision-tracking"],"scope":{"mayTouch":["dashboard/**"],"forbidden":["src/**",".oficina/**","dist/**","node_modules/**"]},"acceptance":["Tres secciones: Commander (scope/prioridades/escalations), Integrator (routing/leases/gates/deadlocks), Architect (tech decisions/patterns/contracts)","Lee decisions de state.json + mensajes en canales de coordinadores","Cada decision: timestamp, actor, cardId, decision, razon, siguiente accion","Filtro por coordinador, por card, por tipo de decision","Enlace a card afectada y a artifacts correspondientes"],"gate":"dashboard/leadership.html funcional + typecheck + tests pass","dependsOn":["T-02"],"attempts":0,"rejections":0,"artifacts":".oficina\\artifacts\\T-06","result":null,"helpRequests":[],"helpHops":0},{"id":"T-07","title":"Implementar Kanban visual de cards con drag-drop y acceptance criteria","type":"implementation","state":"in_progress","owner":"e-xr-immersive-developer","reviewer":null,"requiredCapabilities":["typescript","frontend","kanban","drag-drop"],"scope":{"mayTouch":["dashboard/**"],"forbidden":["src/**",".oficina/**","dist/**","node_modules/**"]},"acceptance":["Columnas: TODO, IN_PROGRESS, REVIEW, DONE, REWORK, BLOCKED, ESCALATED","Cards muestran: id, titulo, owner avatar, reviewer avatar, rejection count, acceptance checklist","Drag-drop opcional entre columnas (solo visual, no persiste - read-only dashboard)","Click en card abre modal con detalle completo: scope, acceptance, gate, artifacts, evidence links","Badges de estado gate: PASS/FAIL/NOT_RUN con colores","Filtro por owner, por tipo, por estado"],"gate":"dashboard/kanban.html funcional + typecheck + tests pass","dependsOn":["T-02"],"attempts":0,"rejections":0,"artifacts":".oficina\\artifacts\\T-07","result":null,"helpRequests":[],"helpHops":0},{"id":"T-08","title":"Implementar animacion de Factory creando agentes nuevos","type":"implementation","state":"todo","owner":"e-xr-immersive-developer","reviewer":null,"requiredCapabilities":["typescript","frontend","animation","svg-canvas"],"scope":{"mayTouch":["dashboard/**"],"forbidden":["src/**",".oficina/**","dist/**","node_modules/**"]},"acceptance":["Deteccion: cuando hay card sin match en catalogo (cobertura < 60% en roster/state)","Animacion secuencial: Factory creando agente... -> archivo .md apareciendo en .opencode/agents/ -> entrada al catalogo dinamico -> match en proxima card","Visualizacion: progress steps con checkmarks, archivo escribiendose con cursor parpadeante","Toast/notification cuando factory completa y agente aparece en roster","Click en notificacion abre ficha del agente fabricado"],"gate":"dashboard/factory.html funcional + typecheck + tests pass","dependsOn":["T-02"],"attempts":0,"rejections":0,"artifacts":".oficina\\artifacts\\T-08","result":null,"helpRequests":[],"helpHops":0},{"id":"T-09","title":"Implementar panel lateral de Gates y Evidencia con spinners y resultados","type":"implementation","state":"todo","owner":"e-testing-api-tester","reviewer":"e-design-ui-finish-gate-reviewer","requiredCapabilities":["typescript","frontend","real-time-ui","testing"],"scope":{"mayTouch":["dashboard/**"],"forbidden":["src/**",".oficina/**","dist/**","node_modules/**"]},"acceptance":["Panel lateral fijo (desktop) / bottom sheet (mobile) mostrando 7 gates","Fases: Build, Type check, Lint, Tests+coverage, Security grep, Diff review, Requisitos","Spinners animados mientras corren, iconos PASS/FAIL/NOT_RUN/N/A al terminar","Click en fase muestra output real (pega de evidence.md/gates.md)","Enlace a evidence.md y handoff.md de la card","Gate 7 (Requisitos): lista acceptance criteria con check/cross y link a evidencia registrada"],"gate":"dashboard/gates.html funcional + typecheck + tests pass","dependsOn":["T-02"],"attempts":0,"rejections":0,"artifacts":".oficina\\artifacts\\T-09","result":null,"helpRequests":[],"helpHops":0},{"id":"T-10","title":"Integrar todo en dashboard.html unico + README-DASHBOARD.md","type":"implementation","state":"todo","owner":"e-testing-api-tester","reviewer":"e-design-ui-finish-gate-reviewer","requiredCapabilities":["typescript","frontend","integration","documentation"],"scope":{"mayTouch":["dashboard/**"],"forbidden":["src/**",".oficina/**","dist/**","node_modules/**"]},"acceptance":["dashboard.html unico con todos los paneles (layout: header + sidebar tabs + main area)","Navegacion por tabs: Mesa | Bus | Debate | Jefe | Kanban | Factory | Gates","Persistencia de tab activo en localStorage","Tema oscuro/claro con toggle persistido","Atajos de teclado: 1-7 tabs, Espacio pause/resume polling, R refresh, ? ayuda","README-DASHBOARD.md con: como usarlo, que muestra cada panel, atajos, troubleshooting"],"gate":"dashboard.html abre en browser file:// + typecheck + tests pass + README completo","dependsOn":["T-03","T-04","T-05","T-06","T-07","T-08","T-09"],"attempts":0,"rejections":0,"artifacts":".oficina\\artifacts\\T-10","result":null,"helpRequests":[],"helpHops":0}],"gates":{"T-00":{"ranAt":"2026-10-29T03:28:55.630Z","technical":[{"phase":"build","command":"npm run build","outcome":"pass","detail":"exit 0 en 11254ms","reason":""},{"phase":"typecheck","command":"npm run typecheck","outcome":"pass","detail":"exit 0 en 10481ms","reason":""},{"phase":"lint","command":null,"outcome":"na","detail":"el proyecto no define script `lint`","reason":""},{"phase":"test","command":"npm run test","outcome":"pass","detail":"exit 0 en 38518ms","reason":""},{"phase":"secrecy","command":"scan:secretos","outcome":"pass","detail":"sin secretos en el scope","reason":""},{"phase":"diff","command":"git diff --stat HEAD","outcome":"pass","detail":"exit 0 en 448ms","reason":""}],"security":[{"phase":"secrecy","command":"scan:credenciales asignadas","outcome":"pass","detail":"credenciales asignadas — limpio","reason":""},{"phase":"secrecy","command":"scan:claves de cloud","outcome":"pass","detail":"claves de cloud — limpio","reason":""},{"phase":"secrecy","command":"scan:tokens de provider","outcome":"pass","detail":"tokens de provider — limpio","reason":""}],"requirements":[],"technicalOutcome":"pass","securityOutcome":"pass","requirementsOutcome":"not_run","verdict":"pass"}},"evidence":{"T-00":[{"id":"ev-t-00-0-test-tests-gates-test-ts-guard-anti-gate-verde-vacio-","cardId":"T-00","requirement":"runTechnicalGates participa del guard: si la fase test sale con exit 0 pero testsActuallyRan es false, la fase test se reporta como not_run con reason explícita de gate verde vacío (no PASS silencioso ni silencioso en otros campos)","requirementIndex":0,"mandatory":false,"kind":"test","level":2,"ref":"tests/gates.test.ts > Guard anti-gate-verde-vacío > degrada a not_run la fase test que sale 0 sin tests","note":"runTechnicalGates marca la fase test como not_run con reason EMPTY_TEST_GATE_REASON cuando exit 0 y no hay conteo; conserva exitCode 0 y output real.","recordedAt":"1970-01-01T00:00:00.000Z","recordedBy":"e-testing-api-tester","passed":true},{"id":"ev-t-00-1-test-tests-gates-test-ts-guard-anti-gate-verde-vacio-","cardId":"T-00","requirement":"si testsActuallyRan es true (p. ej. conteo legible '5 passed' o 'Tests: 5'), la fase test sigue en PASS sin regresión","requirementIndex":1,"mandatory":false,"kind":"test","level":2,"ref":"tests/gates.test.ts > Guard anti-gate-verde-vacío > no degrada cuando el conteo es legible ('5 passed' / 'Tests: 5')","note":"Con conteo legible la fase test sigue en PASS; sin regresión.","recordedAt":"1970-01-01T00:00:00.000Z","recordedBy":"e-testing-api-tester","passed":true},{"id":"ev-t-00-2-test-tests-gates-test-ts-guard-anti-gate-verde-vacio-","cardId":"T-00","requirement":"consolidate con ese resultado produce technicalOutcome fail/not_run (passed=false) y renderGate muestra el motivo de forma legible","requirementIndex":2,"mandatory":false,"kind":"test","level":2,"ref":"tests/gates.test.ts > Guard anti-gate-verde-vacío > consolidate propaga not_run / renderGate muestra el motivo","note":"consolidate devuelve technicalOutcome=not_run y passed=false; renderGate incluye columna Motivo con el texto del gate verde vacío.","recordedAt":"1970-01-01T00:00:00.000Z","recordedBy":"e-testing-api-tester","passed":true},{"id":"ev-t-00-3-test-tests-gates-test-ts-guard-anti-gate-verde-vacio-","cardId":"T-00","requirement":"pmOfficeOffice.runGates / flujo Office.runGates queda cableado al guard vía runTechnicalGates (sin duplicar lógica)","requirementIndex":3,"mandatory":false,"kind":"test","level":2,"ref":"tests/gates.test.ts > Guard anti-gate-verde-vacío > Office.runGates hereda el guard vía runTechnicalGates","note":"Office.runGates no reimplementa el guard: delega en runTechnicalGates; gates.md en disco muestra el motivo.","recordedAt":"1970-01-01T00:00:00.000Z","recordedBy":"e-testing-api-tester","passed":true},{"id":"ev-t-00-4-artifact-tests-gates-test-ts","cardId":"T-00","requirement":"regresión cubierta por tests vitest nuevos en tests/gates.test.ts (existen ya tests de testsActuallyRan; se agregan los del cableado)","requirementIndex":4,"mandatory":false,"kind":"artifact","level":2,"ref":"tests/gates.test.ts","note":"8 tests vitest nuevos en el describe 'Guard anti-gate-verde-vacío' (27 en el archivo).","recordedAt":"1970-01-01T00:00:00.000Z","recordedBy":"e-testing-api-tester","passed":true},{"id":"ev-t-00-5-test-npm-run-typecheck-npm-test","cardId":"T-00","requirement":"npm run typecheck sin errores y npm test todo en verde (las 456 existentes + nuevas)","requirementIndex":5,"mandatory":false,"kind":"test","level":2,"ref":"npm run typecheck && npm test","note":"typecheck exit 0; vitest 27 archivos / 464 tests en verde, 0 fallidos.","recordedAt":"1970-01-01T00:00:00.000Z","recordedBy":"e-testing-api-tester","passed":true}]}}</script>
926
+ <!--END_DATASET-->
927
+
928
+ <script src="./kanban-source.js"></script>
929
+ <script>
930
+ (function () {
931
+ "use strict"
932
+
933
+ var Core = globalThis.KanbanSource
934
+ var board = document.getElementById("board")
935
+ var modal = document.getElementById("card-modal")
936
+ var modalContent = document.getElementById("card-modal-content")
937
+ var live = document.getElementById("live-region")
938
+ var ownerSelect = document.getElementById("filter-owner")
939
+ var typeSelect = document.getElementById("filter-type")
940
+ var stateSelect = document.getElementById("filter-state")
941
+ var searchInput = document.getElementById("filter-search")
942
+ var sourceLabel = document.getElementById("data-source")
943
+
944
+ var view = Core.normalizeDataset({})
945
+ var filters = { owner: "all", type: "all", state: "all", q: "" }
946
+ var lastFocused = null
947
+
948
+ // ── Dataset ────────────────────────────────────────────────────────────
949
+
950
+ function embeddedDataset() {
951
+ var el = document.getElementById("kanban-dataset")
952
+ if (!el) return { cards: [], agents: {} }
953
+ try {
954
+ return JSON.parse(el.textContent)
955
+ } catch (err) {
956
+ return { cards: [], agents: {} }
957
+ }
958
+ }
959
+
960
+ function datasetUrl() {
961
+ var params = new URLSearchParams(location.search)
962
+ return params.get("data")
963
+ }
964
+
965
+ function describe(dataset) {
966
+ var meta = dataset.meta || {}
967
+ var run = document.getElementById("run-meta")
968
+ var objective = meta.objective || "(sin objetivo registrado)"
969
+ run.innerHTML = "run <strong>" + Core.escapeHtml(meta.runId || "—") + "</strong> · estado <strong>" +
970
+ Core.escapeHtml(meta.status || "—") + "</strong> · " + Core.escapeHtml(objective)
971
+ }
972
+
973
+ function load(dataset, label) {
974
+ view = Core.normalizeDataset(dataset)
975
+ describe(dataset)
976
+ sourceLabel.textContent = label
977
+ buildFilterOptions()
978
+ render()
979
+ }
980
+
981
+ function loadFrom(url, label) {
982
+ return fetch(url, { cache: "no-store" })
983
+ .then(function (res) {
984
+ if (!res.ok) throw new Error("HTTP " + res.status)
985
+ return res.json()
986
+ })
987
+ .then(function (dataset) {
988
+ load(dataset, label)
989
+ announce("Datos recargados desde " + label)
990
+ })
991
+ .catch(function (err) {
992
+ load(embeddedDataset(), "snapshot embebido (fetch falló: " + err.message + ")")
993
+ announce("No se pudo leer " + label + ". Se usa el snapshot embebido.")
994
+ })
995
+ }
996
+
997
+ function bootstrap() {
998
+ var url = datasetUrl()
999
+ if (url) {
1000
+ loadFrom(url, url)
1001
+ } else {
1002
+ load(embeddedDataset(), "snapshot embebido")
1003
+ }
1004
+ }
1005
+
1006
+ // ── Filtros ────────────────────────────────────────────────────────────
1007
+
1008
+ function option(value, label) {
1009
+ var opt = document.createElement("option")
1010
+ opt.value = value
1011
+ opt.textContent = label
1012
+ return opt
1013
+ }
1014
+
1015
+ function buildFilterOptions() {
1016
+ while (ownerSelect.options.length > 1) ownerSelect.remove(1)
1017
+ var owners = {}
1018
+ view.cards.forEach(function (card) {
1019
+ if (card.owner) owners[card.owner.id] = card.owner.name
1020
+ })
1021
+ Object.keys(owners).sort().forEach(function (id) {
1022
+ ownerSelect.appendChild(option(id, owners[id] + " (" + id + ")"))
1023
+ })
1024
+ if (view.cards.some(function (card) { return !card.owner })) {
1025
+ ownerSelect.appendChild(option("unassigned", "sin asignar"))
1026
+ }
1027
+
1028
+ while (typeSelect.options.length > 1) typeSelect.remove(1)
1029
+ var types = {}
1030
+ view.cards.forEach(function (card) { types[card.type] = true })
1031
+ Object.keys(types).sort().forEach(function (type) {
1032
+ typeSelect.appendChild(option(type, type))
1033
+ })
1034
+
1035
+ while (stateSelect.options.length > 1) stateSelect.remove(1)
1036
+ Core.COLUMNS.forEach(function (column) {
1037
+ stateSelect.appendChild(option(column.id, column.label))
1038
+ })
1039
+
1040
+ ownerSelect.value = filters.owner
1041
+ typeSelect.value = filters.type
1042
+ stateSelect.value = filters.state
1043
+ }
1044
+
1045
+ function readFilters() {
1046
+ filters.owner = ownerSelect.value
1047
+ filters.type = typeSelect.value
1048
+ filters.state = stateSelect.value
1049
+ filters.q = searchInput.value.trim()
1050
+ }
1051
+
1052
+ // ── Render ─────────────────────────────────────────────────────────────
1053
+
1054
+ function render() {
1055
+ var groups = Core.groupByColumn(Core.filterCards(view.cards, filters))
1056
+ var active = filters.owner !== "all" || filters.type !== "all" || filters.state !== "all" || filters.q !== ""
1057
+
1058
+ Core.COLUMN_IDS.forEach(function (id) {
1059
+ var body = board.querySelector('[data-cards="' + id + '"]')
1060
+ var column = Core.COLUMNS.filter(function (c) { return c.id === id })[0]
1061
+ body.innerHTML = Core.columnBodyHtml(column, groups[id])
1062
+ var counter = board.querySelector('[data-count="' + id + '"]')
1063
+ var total = view.cards.filter(function (card) { return card.state === id }).length
1064
+ counter.textContent = active ? groups[id].length + "/" + total : String(total)
1065
+ })
1066
+
1067
+ var visible = Core.filterCards(view.cards, filters).length
1068
+ document.getElementById("count-total").textContent = String(view.cards.length)
1069
+ document.getElementById("count-visible").textContent = String(visible)
1070
+
1071
+ var msg = visible + " de " + view.cards.length + " cards visibles"
1072
+ if (active) msg += " con filtros activos"
1073
+ live.textContent = msg
1074
+ }
1075
+
1076
+ function announce(message) {
1077
+ live.textContent = message
1078
+ }
1079
+
1080
+ function cardById(id) {
1081
+ for (var i = 0; i < view.cards.length; i++) {
1082
+ if (view.cards[i].id === id) return view.cards[i]
1083
+ }
1084
+ return null
1085
+ }
1086
+
1087
+ // ── Modal ──────────────────────────────────────────────────────────────
1088
+
1089
+ function openCard(id) {
1090
+ var card = cardById(id)
1091
+ if (!card) return
1092
+ lastFocused = document.activeElement
1093
+ modalContent.innerHTML = Core.modalHtml(card)
1094
+ if (typeof modal.showModal === "function") {
1095
+ modal.showModal()
1096
+ } else {
1097
+ modal.setAttribute("open", "open")
1098
+ }
1099
+ var closeBtn = document.getElementById("card-modal-close")
1100
+ if (closeBtn) closeBtn.focus()
1101
+ announce("Detalle de " + card.id + " abierto")
1102
+ }
1103
+
1104
+ function closeModal() {
1105
+ if (typeof modal.close === "function") modal.close()
1106
+ else modal.removeAttribute("open")
1107
+ if (lastFocused && typeof lastFocused.focus === "function") lastFocused.focus()
1108
+ announce("Detalle cerrado")
1109
+ }
1110
+
1111
+ document.getElementById("card-modal-close").addEventListener("click", closeModal)
1112
+ modal.addEventListener("click", function (event) {
1113
+ if (event.target === modal) closeModal()
1114
+ })
1115
+ modal.addEventListener("close", function () {
1116
+ if (lastFocused && typeof lastFocused.focus === "function") lastFocused.focus()
1117
+ })
1118
+
1119
+ // ── Interacción del tablero ────────────────────────────────────────────
1120
+
1121
+ board.addEventListener("click", function (event) {
1122
+ var card = event.target.closest(".card")
1123
+ if (card) openCard(card.getAttribute("data-card-id"))
1124
+ })
1125
+
1126
+ board.addEventListener("keydown", function (event) {
1127
+ var card = event.target.closest ? event.target.closest(".card") : null
1128
+ if (!card) return
1129
+ if (event.key === "Enter" || event.key === " " || event.key === "Spacebar") {
1130
+ event.preventDefault()
1131
+ openCard(card.getAttribute("data-card-id"))
1132
+ }
1133
+ })
1134
+
1135
+ // ── Drag & drop (visual, no persiste) ──────────────────────────────────
1136
+ // DRAG-DROP:start
1137
+ var dragged = null
1138
+
1139
+ function onDragStart(event) {
1140
+ var card = event.target.closest ? event.target.closest(".card") : null
1141
+ if (!card) return
1142
+ dragged = card
1143
+ card.classList.add("dragging")
1144
+ if (event.dataTransfer) {
1145
+ event.dataTransfer.effectAllowed = "move"
1146
+ try { event.dataTransfer.setData("text/plain", card.getAttribute("data-card-id")) } catch (err) { /* noop */ }
1147
+ }
1148
+ }
1149
+
1150
+ function onDragEnd() {
1151
+ if (dragged) dragged.classList.remove("dragging")
1152
+ dragged = null
1153
+ board.querySelectorAll(".column-body.drop-target").forEach(function (body) {
1154
+ body.classList.remove("drop-target")
1155
+ })
1156
+ }
1157
+
1158
+ function onDragOver(event) {
1159
+ var zone = event.target.closest ? event.target.closest("[data-dropzone]") : null
1160
+ if (!zone) return
1161
+ event.preventDefault()
1162
+ if (event.dataTransfer) event.dataTransfer.dropEffect = "move"
1163
+ board.querySelectorAll(".column-body.drop-target").forEach(function (body) {
1164
+ if (body !== zone) body.classList.remove("drop-target")
1165
+ })
1166
+ zone.classList.add("drop-target")
1167
+ }
1168
+
1169
+ function onDragLeave(event) {
1170
+ var zone = event.target.closest ? event.target.closest("[data-dropzone]") : null
1171
+ if (!zone) return
1172
+ if (event.relatedTarget && zone.contains(event.relatedTarget)) return
1173
+ zone.classList.remove("drop-target")
1174
+ }
1175
+
1176
+ // El drop solo mueve el nodo en el DOM. No hay red ni storage: el tablero
1177
+ // es read-only por contrato, y un reload descarta cualquier movimiento.
1178
+ function onDrop(event) {
1179
+ var zone = event.target.closest ? event.target.closest("[data-dropzone]") : null
1180
+ if (!zone || !dragged) return
1181
+ event.preventDefault()
1182
+ var card = dragged
1183
+ var targetState = zone.getAttribute("data-cards")
1184
+ var originState = card.getAttribute("data-state")
1185
+ var originBody = card.parentElement
1186
+ zone.appendChild(card)
1187
+ card.setAttribute("data-visual-state", targetState)
1188
+ card.setAttribute("data-moved", targetState !== originState ? "true" : "false")
1189
+ var note = card.querySelector(".card-moved-note")
1190
+ if (targetState !== originState) {
1191
+ if (!note) {
1192
+ note = document.createElement("p")
1193
+ note.className = "card-moved-note check-more"
1194
+ note.textContent = "movida visualmente desde " + originState + " (no persiste)"
1195
+ card.appendChild(note)
1196
+ } else {
1197
+ note.textContent = "movida visualmente desde " + originState + " (no persiste)"
1198
+ }
1199
+ } else if (note) {
1200
+ note.remove()
1201
+ }
1202
+ if (originBody) originBody.classList.remove("drop-target")
1203
+ zone.classList.remove("drop-target")
1204
+ dragged = null
1205
+ refreshCountsFromDom()
1206
+ announce("Card " + card.getAttribute("data-card-id") + " movida a " + targetState + " (cambio visual, no se guarda)")
1207
+ }
1208
+ // DRAG-DROP:end
1209
+
1210
+ function refreshCountsFromDom() {
1211
+ Core.COLUMN_IDS.forEach(function (id) {
1212
+ var body = board.querySelector('[data-cards="' + id + '"]')
1213
+ var count = body ? body.querySelectorAll(".card").length : 0
1214
+ var counter = board.querySelector('[data-count="' + id + '"]')
1215
+ if (counter) counter.textContent = String(count)
1216
+ })
1217
+ }
1218
+
1219
+ board.addEventListener("dragstart", onDragStart)
1220
+ board.addEventListener("dragend", onDragEnd)
1221
+ board.addEventListener("dragover", onDragOver)
1222
+ board.addEventListener("dragleave", onDragLeave)
1223
+ board.addEventListener("drop", onDrop)
1224
+
1225
+ // ── Wiring de filtros ──────────────────────────────────────────────────
1226
+
1227
+ function onFilterChange() {
1228
+ readFilters()
1229
+ render()
1230
+ }
1231
+
1232
+ ownerSelect.addEventListener("change", onFilterChange)
1233
+ typeSelect.addEventListener("change", onFilterChange)
1234
+ stateSelect.addEventListener("change", onFilterChange)
1235
+ searchInput.addEventListener("input", onFilterChange)
1236
+
1237
+ document.getElementById("reset-view").addEventListener("click", function () {
1238
+ filters = { owner: "all", type: "all", state: "all", q: "" }
1239
+ ownerSelect.value = "all"
1240
+ typeSelect.value = "all"
1241
+ stateSelect.value = "all"
1242
+ searchInput.value = ""
1243
+ render()
1244
+ announce("Vista restablecida: movimientos visuales descartados")
1245
+ })
1246
+
1247
+ document.getElementById("refresh-data").addEventListener("click", function () {
1248
+ var url = datasetUrl()
1249
+ if (url) loadFrom(url, url)
1250
+ else load(embeddedDataset(), "snapshot embebido")
1251
+ })
1252
+
1253
+ bootstrap()
1254
+ })()
1255
+ </script>
1256
+ </body>
1257
+ </html>