agents-city 0.3.0 → 0.4.0

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.
@@ -12,6 +12,10 @@
12
12
  */
13
13
 
14
14
  import './es'; // the Spanish dictionary registers itself on import
15
+ import { FormularioDeCasa } from './casa';
16
+ import { confirma, pregunta } from './dialogo';
17
+ import { Demos } from './demo';
18
+ import { Explorador } from './explorador';
15
19
  import { idioma, plural, ponIdioma, t as _ } from './idioma';
16
20
  import {
17
21
  ESFUERZOS,
@@ -290,6 +294,7 @@ const SECCIONES: Array<[string, string]> = [
290
294
  ['red', 'Roads'],
291
295
  ['committee', 'Committee'],
292
296
  ['gente', 'Houses'],
297
+ ['demos', 'Demos'],
293
298
  ['ciudades', 'Cities'],
294
299
  ];
295
300
 
@@ -759,15 +764,17 @@ function rail(): void {
759
764
  q('#rail').innerHTML = SECCIONES.filter(([id]) => id !== 'ciudades')
760
765
  .map(([id, et]) => {
761
766
  const n =
762
- id === 'gente'
763
- ? String(Object.keys(E.skills).length)
764
- : id === 'committee'
765
- ? String(E.deliberations.length)
766
- : id === 'red'
767
- ? String(E.roads.length)
768
- : id === 'barrios'
769
- ? String(E.parcelas.length)
770
- : '';
767
+ id === 'demos'
768
+ ? ''
769
+ : id === 'gente'
770
+ ? String(Object.keys(E.skills).length)
771
+ : id === 'committee'
772
+ ? String(E.deliberations.length)
773
+ : id === 'red'
774
+ ? String(E.roads.length)
775
+ : id === 'barrios'
776
+ ? String(E.parcelas.length)
777
+ : '';
771
778
  return `<li class="${id === SECCION ? 'aqui' : ''}" data-s="${id}">
772
779
  <span>${esc(_(et))}</span>${n ? `<span class="n">${n}</span>` : ''}</li>`;
773
780
  })
@@ -802,7 +809,24 @@ function rail(): void {
802
809
  const nueva = q<HTMLAnchorElement>('#alWizard');
803
810
  nueva.onclick = async (ev) => {
804
811
  ev.preventDefault();
805
- const name = window.prompt('Name of the new city');
812
+ const dicho = await pregunta({
813
+ titulo: _('Start another city'),
814
+ cuerpo: [
815
+ _(
816
+ 'A city is a place with its own seat, its own houses and its own map. Yours stay where they are.',
817
+ ),
818
+ ],
819
+ campos: [
820
+ {
821
+ id: 'name',
822
+ etiqueta: _('What is it called?'),
823
+ pista: _('home, clients, the lab — whatever you would say out loud'),
824
+ requerido: true,
825
+ },
826
+ ],
827
+ aceptar: _('Create it'),
828
+ });
829
+ const name = dicho?.name;
806
830
  if (!name?.trim()) return;
807
831
  const r = await api<Respuesta>('/api/ciudades', {
808
832
  method: 'POST',
@@ -814,6 +838,9 @@ function rail(): void {
814
838
  }
815
839
 
816
840
  function pinta(): void {
841
+ // A playing demo left running in a section nobody is looking at is a timer
842
+ // firing into a detached node — and a soundtrack to the wrong screen.
843
+ if (SECCION !== 'demos') demos?.para();
817
844
  rail();
818
845
  q<HTMLElement>('.cuerpo').style.padding = '';
819
846
  q<HTMLElement>('#lienzo').style.maxWidth = '';
@@ -926,19 +953,23 @@ VISTAS.ciudades = () => {
926
953
  return;
927
954
  }
928
955
  const p = previa.preview;
929
- const texto =
930
- `Start over ${p.city}?\n\nYou lose:\n` +
931
- p.loses.map((l) => ` · ${l}`).join('\n') +
932
- `\n\nRight now that is ${p.agents} agent(s), ${p.roads} road(s) and ` +
933
- `${p.deliberations} committee act(s).\n\nYou keep:\n` +
934
- p.keeps.map((l) => ` · ${l}`).join('\n') +
935
- `\n\nType the city's name to confirm:`;
936
- const escrito = window.prompt(texto, '');
937
- if (escrito === null) return;
938
- if (escrito.trim() !== p.city) {
939
- toast('That is not the name — nothing was changed', true);
940
- return;
941
- }
956
+ const dicho = await pregunta({
957
+ titulo: _('Start over {city}?', { city: p.city }),
958
+ cuerpo: [
959
+ _('You lose:') + ' ' + p.loses.join(', ') + '.',
960
+ _('Right now that is {agents} house(s), {roads} road(s) and {acts} committee act(s).', {
961
+ agents: p.agents,
962
+ roads: p.roads,
963
+ acts: p.deliberations,
964
+ }),
965
+ _('You keep:') + ' ' + p.keeps.join(', ') + '.',
966
+ ],
967
+ campos: [{ id: 'nombre', etiqueta: _('Type the city’s name to confirm'), requerido: true }],
968
+ exige: p.city,
969
+ aceptar: _('Start over'),
970
+ peligro: true,
971
+ });
972
+ if (dicho === null) return;
942
973
  const r = await api<{ ok?: boolean; backup?: string; error?: string }>(
943
974
  '/api/ciudad-reinicia',
944
975
  { method: 'POST', body: JSON.stringify({ confirm: p.city }) },
@@ -954,8 +985,12 @@ VISTAS.ciudades = () => {
954
985
  todos<HTMLButtonElement>('.archiva').forEach((boton) => {
955
986
  boton.onclick = async () => {
956
987
  const nombre = boton.dataset.nombre ?? '';
957
- if (!window.confirm(`Archive ${nombre}? It moves into .backups — nothing is deleted.`))
958
- return;
988
+ const vale = await confirma(
989
+ _('Archive {city}?', { city: nombre }),
990
+ [_('It moves into .backups. Nothing is deleted, and you can put it back by hand.')],
991
+ { aceptar: _('Archive it') },
992
+ );
993
+ if (!vale) return;
959
994
  const r = await api<{ ok?: boolean; backup?: string; error?: string }>(
960
995
  '/api/ciudad-archiva',
961
996
  { method: 'POST', body: JSON.stringify({ city: boton.dataset.ciudad ?? '' }) },
@@ -970,6 +1005,15 @@ VISTAS.ciudades = () => {
970
1005
  });
971
1006
  };
972
1007
 
1008
+ /** The demo shelf, built once so a half-played story survives a repaint. */
1009
+ let demos: Demos | null = null;
1010
+
1011
+ VISTAS.demos = () => {
1012
+ if (!demos) demos = new Demos({ api, esc, pinta: renderActividad });
1013
+ q('#lienzo').innerHTML = '<div id="demoHueco"></div>';
1014
+ demos.monta(q<HTMLElement>('#demoHueco'));
1015
+ };
1016
+
973
1017
  VISTAS.resumen = () => {
974
1018
  const mia = E.tarjetas.find((t) => t.user === E.yo);
975
1019
  const sinUnidad = E.parcelas.filter((p) => p.unidad === 'none' || p.unidad === 'mine').length;
@@ -1789,7 +1833,17 @@ VISTAS.gente = () => {
1789
1833
  if (valor === '__otro__') {
1790
1834
  // The CLI resolves any alias at launch; the prompt just has to be an
1791
1835
  // alias shape, and the server refuses anything that is not.
1792
- const propio = window.prompt('Model alias (resolved by the CLI at launch):', '');
1836
+ const dicho = await pregunta({
1837
+ titulo: _('Which engine?'),
1838
+ cuerpo: [
1839
+ _('An alias the CLI resolves when the window opens. Anything it accepts works here.'),
1840
+ ],
1841
+ campos: [
1842
+ { id: 'alias', etiqueta: _('Model alias'), pista: 'claude-opus-5', requerido: true },
1843
+ ],
1844
+ aceptar: _('Use it'),
1845
+ });
1846
+ const propio = dicho?.alias ?? null;
1793
1847
  if (propio === null) {
1794
1848
  pinta();
1795
1849
  return;
@@ -1851,7 +1905,12 @@ VISTAS.gente = () => {
1851
1905
  todos<HTMLButtonElement>('.rpgQuitar').forEach((boton) => {
1852
1906
  boton.onclick = async () => {
1853
1907
  const nombre = boton.dataset.skill ?? '';
1854
- if (!window.confirm(`Remove the skill ${nombre} from this agent's home?`)) return;
1908
+ const vale = await confirma(
1909
+ _('Remove the skill {skill}?', { skill: nombre }),
1910
+ [_('It is deleted from this agent’s own home. Nothing outside that folder is touched.')],
1911
+ { aceptar: _('Remove it'), peligro: true },
1912
+ );
1913
+ if (!vale) return;
1855
1914
  const r = await api<{ ok?: boolean; error?: string }>('/api/skill', {
1856
1915
  method: 'POST',
1857
1916
  body: JSON.stringify({ agent: boton.dataset.agente ?? '', remove: nombre }),
@@ -1870,27 +1929,38 @@ VISTAS.gente = () => {
1870
1929
  const alta = document.getElementById('altaAgente');
1871
1930
  if (alta)
1872
1931
  alta.onclick = async () => {
1873
- const nombre = window.prompt("The agent's name:", '');
1874
- if (nombre === null || !nombre.trim()) return;
1875
- const clase =
1876
- window.prompt('What kind of work does it do? code, knowledge or coordinator', 'code') ?? '';
1877
- if (!clase.trim()) return;
1878
- const rol = window.prompt('Its role (its specialty, not authority):', 'blank') ?? '';
1879
- if (!rol.trim()) return;
1880
- try {
1881
- const r = await api<{ ok?: boolean; error?: string }>('/api/agentes', {
1882
- method: 'POST',
1883
- body: JSON.stringify({ name: nombre.trim(), kind: clase.trim(), role: rol.trim() }),
1884
- });
1885
- if (!r.ok) {
1886
- toast(r.error || 'Could not add the agent', true);
1887
- return;
1888
- }
1889
- toast(`${nombre.trim()} has a house now — give it something to work on`);
1890
- void refresca();
1891
- } catch (e) {
1892
- toast(String(e), true);
1893
- }
1932
+ // The very same form the first-run guide uses, in a dialog. It used to be
1933
+ // three chained prompt boxes here — name, kind, role — with no engine and
1934
+ // no way to pick a folder, which is a second, worse implementation of a
1935
+ // question the product had already answered well once.
1936
+ const forma = new FormularioDeCasa({ api, esc, aviso: toast, yo: E.yo });
1937
+ let hecho: { slug: string; nombre: string } | null = null;
1938
+ await pregunta({
1939
+ titulo: _('Build a house'),
1940
+ cuerpo: [
1941
+ _(
1942
+ 'One worker, its own window, its own corner of your disk. Everything here can be changed afterwards.',
1943
+ ),
1944
+ ],
1945
+ contenido: '<div id="altaCasa"></div>',
1946
+ aceptar: _('Build it'),
1947
+ ancho: 640,
1948
+ enlaza: (raiz, cierra) => {
1949
+ const hueco = raiz.querySelector<HTMLElement>('#altaCasa');
1950
+ if (hueco) forma.monta(hueco);
1951
+ const boton = raiz.querySelector<HTMLButtonElement>('[data-dlg="si"]');
1952
+ if (!boton) return;
1953
+ boton.onclick = async () => {
1954
+ boton.disabled = true;
1955
+ hecho = await forma.guarda();
1956
+ boton.disabled = false;
1957
+ if (hecho) cierra({});
1958
+ };
1959
+ },
1960
+ });
1961
+ if (!hecho) return;
1962
+ toast(_('{name} has a house now', { name: (hecho as { nombre: string }).nombre }));
1963
+ void refresca();
1894
1964
  };
1895
1965
 
1896
1966
  // Mounts: what an agent works on, added and removed where it is read.
@@ -1911,16 +1981,49 @@ VISTAS.gente = () => {
1911
1981
  }
1912
1982
  };
1913
1983
  todos<HTMLButtonElement>('.rpgMonta').forEach((boton) => {
1914
- boton.onclick = () => {
1915
- const ruta = window.prompt('A repo, a worktree or a folder of documents:', '~/');
1916
- if (ruta === null || !ruta.trim()) return;
1917
- void montaje({ agent: boton.dataset.agente ?? '', add: ruta.trim() }, 'Mounted');
1984
+ boton.onclick = async () => {
1985
+ // The same walk the house form uses: no guessed list, nothing filtered.
1986
+ const elegidas: string[] = [];
1987
+ const exp = new Explorador(
1988
+ { api, esc },
1989
+ (ruta) => {
1990
+ const ya = elegidas.indexOf(ruta);
1991
+ if (ya >= 0) elegidas.splice(ya, 1);
1992
+ else elegidas.push(ruta);
1993
+ },
1994
+ () => elegidas,
1995
+ );
1996
+ const dicho = await pregunta({
1997
+ titulo: _('What else does it work on?'),
1998
+ cuerpo: [
1999
+ _(
2000
+ 'A repository, a worktree, a folder of documents, one exact file. It is linked, never copied.',
2001
+ ),
2002
+ ],
2003
+ contenido: '<div id="montaExp"></div>',
2004
+ aceptar: _('Mount it'),
2005
+ ancho: 620,
2006
+ enlaza: (raiz) => {
2007
+ const hueco = raiz.querySelector<HTMLElement>('#montaExp');
2008
+ if (hueco) exp.monta(hueco);
2009
+ },
2010
+ });
2011
+ if (dicho === null || !elegidas.length) return;
2012
+ for (const ruta of elegidas)
2013
+ await montaje({ agent: boton.dataset.agente ?? '', add: ruta }, _('Mounted'));
1918
2014
  };
1919
2015
  });
1920
2016
  todos<HTMLButtonElement>('.rpgDesmonta').forEach((boton) => {
1921
- boton.onclick = () => {
2017
+ boton.onclick = async () => {
1922
2018
  const etiqueta = boton.dataset.mount ?? '';
1923
- if (!window.confirm(`Stop this agent working on ${etiqueta}?`)) return;
2019
+ // Unmounting removes a symlink and a card key. What it points at is not
2020
+ // touched — say so, because "remove" reads as "delete my folder".
2021
+ const vale = await confirma(
2022
+ _('Stop this agent working on {what}?', { what: etiqueta }),
2023
+ [_('The link goes. The folder it points at stays exactly where it is.')],
2024
+ { aceptar: _('Unmount it') },
2025
+ );
2026
+ if (!vale) return;
1924
2027
  // Unmounting removes a symlink and a card key. What it points at is not
1925
2028
  // touched — say so, because "remove" reads as "delete my folder".
1926
2029
  void montaje(
@@ -57,8 +57,24 @@ const COLOR_POR_DEFECTO = 0xc8b48a;
57
57
  //: rather than a palette swap. What day mode changes is the margin around the
58
58
  //: plan — it takes the ground's own colour, so a light Hall frames a deliberate
59
59
  //: viewport instead of leaving paper around a black island.
60
+ /** The ground the city is drawn on, one per skin.
61
+ *
62
+ * `SUELO_CLARO` used to be `0x0b1119` — DARKER than the night floor. Asking the
63
+ * map for daylight made it dimmer, which is how a light mode ships without
64
+ * anybody noticing it was never wired: the Hall dutifully sent `light`, the map
65
+ * dutifully applied it, and the result was a slightly darker night. */
60
66
  const SUELO = 0x121821;
61
- const SUELO_CLARO = 0x0b1119;
67
+ const SUELO_CLARO = 0xf3f0e8;
68
+ /** The colour the ground markings are drawn in: the plaza's ellipse, the block
69
+ * outlines. Warm sand on night, a warm brown on parchment. */
70
+ const TRAZO = 0xc8b48a;
71
+ const TRAZO_CLARO = 0x8a7247;
72
+ let esDeDia = true;
73
+
74
+ /** The pen the ground is marked with right now. */
75
+ export function trazo(): number {
76
+ return esDeDia ? TRAZO_CLARO : TRAZO;
77
+ }
62
78
 
63
79
  /** The Hall's theme message: the map is framed by it and must match its light. */
64
80
  function esTema(dato: unknown): dato is { type: string; theme: 'light' | 'dark' } {
@@ -67,7 +83,8 @@ function esTema(dato: unknown): dato is { type: string; theme: 'light' | 'dark'
67
83
  }
68
84
 
69
85
  function ponTema(cual: 'light' | 'dark'): void {
70
- const suelo = cual === 'light' ? SUELO_CLARO : SUELO;
86
+ esDeDia = cual === 'light';
87
+ const suelo = esDeDia ? SUELO_CLARO : SUELO;
71
88
  document.documentElement.dataset.tema = cual;
72
89
  document.body.style.background = '#' + suelo.toString(16).padStart(6, '0');
73
90
  // The renderer may not exist yet (the message can beat the boot); the body
@@ -349,7 +366,13 @@ async function arranca() {
349
366
  const app = new Application();
350
367
  appGlobal = app;
351
368
  const lienzo = document.querySelector('main') as HTMLElement;
352
- await app.init({ background: SUELO, antialias: true, resizeTo: lienzo });
369
+ // Day, unless the Hall has already said otherwise: the boot must agree with
370
+ // the stylesheet's own default, or the canvas flashes the wrong skin.
371
+ await app.init({
372
+ background: esDeDia ? SUELO_CLARO : SUELO,
373
+ antialias: true,
374
+ resizeTo: lienzo,
375
+ });
353
376
  lienzo.appendChild(app.canvas);
354
377
 
355
378
  mundo = new Container();
@@ -524,9 +547,9 @@ async function arranca() {
524
547
  plaza = { x: 0, y: yFondo + 70 };
525
548
  const suelo2 = new Graphics()
526
549
  .ellipse(0, 6, 128, 44)
527
- .fill({ color: 0xc8b48a, alpha: 0.05 })
550
+ .fill({ color: trazo(), alpha: esDeDia ? 0.08 : 0.05 })
528
551
  .ellipse(0, 6, 128, 44)
529
- .stroke({ color: 0xc8b48a, alpha: 0.22, width: 1 });
552
+ .stroke({ color: trazo(), alpha: esDeDia ? 0.35 : 0.22, width: 1 });
530
553
  suelo2.position.set(plaza.x, plaza.y);
531
554
  mundo.addChildAt(suelo2, mundo.children.indexOf(capaGente));
532
555
 
package/demo/graba.py ADDED
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env python3
2
+ """Record each demo story, by running it for real.
3
+
4
+ demo/graba.py record every story into demo/grabaciones/
5
+ demo/graba.py software just that one
6
+
7
+ The Hall can play a demo back in the browser. It must not do that by re-deriving
8
+ what the committee *would* have said — that would be a second implementation of
9
+ the state machine, and the second one is always the one that drifts and lies.
10
+
11
+ So the recording is made by the real thing: an ephemeral city, the real local
12
+ WebSocket bus, the real committee state machine, the real activity feed. What
13
+ lands in `demo/grabaciones/<story>.jsonl` is exactly the stream a spectator saw,
14
+ with the volatile parts (ids, timestamps, thread) normalised so the file is
15
+ stable from one recording to the next and a diff means something changed in the
16
+ product rather than in the clock.
17
+
18
+ Regenerate after touching `demo/stories.py` or the committee's own events; the
19
+ demo suite fails when a recording no longer matches its story.
20
+ """
21
+
22
+ import glob
23
+ import json
24
+ import os
25
+ import shutil
26
+ import subprocess
27
+ import sys
28
+ import tempfile
29
+
30
+ AQUI = os.path.dirname(os.path.abspath(__file__))
31
+ RAIZ = os.path.dirname(AQUI)
32
+ SHOW = os.path.join(AQUI, "show.py")
33
+ DESTINO = os.path.join(AQUI, "grabaciones")
34
+
35
+ sys.path.insert(0, AQUI)
36
+ from stories import STORIES # noqa: E402
37
+
38
+ #: What is stripped before writing: everything that changes every run and means
39
+ #: nothing to a viewer. `seq` stays — the order is the point.
40
+ VOLATIL = ("id", "sourceId", "at", "city", "thread")
41
+
42
+
43
+ def _clave(fixture, clave):
44
+ ruta = os.path.join(AQUI, fixture, "city.yml")
45
+ for linea in open(ruta, encoding="utf-8"):
46
+ if linea.startswith(clave + ":"):
47
+ return linea.split(":", 1)[1].strip()
48
+ return ""
49
+
50
+
51
+ def graba(historia):
52
+ """Run one story over the real bus and return the events it produced."""
53
+ fixture = STORIES[historia]["city"]
54
+ base = tempfile.mkdtemp(prefix="agents-city-graba-")
55
+ datos = os.path.join(base, "city")
56
+ app = os.path.join(base, "app")
57
+ shutil.copytree(os.path.join(AQUI, fixture), datos)
58
+ os.makedirs(app)
59
+ entorno = dict(
60
+ os.environ,
61
+ AGENTS_CITY_HOME=app,
62
+ AGENTS_CITY_DATA=datos,
63
+ AGENTS_CITY_USER=_clave(fixture, "owner"),
64
+ CITY_ADDRESS=f"{_clave(fixture, 'owner')}/{_clave(fixture, 'slug')}",
65
+ )
66
+ for clave in ("CITY_BUS_URL", "CITY_BUS_TOKEN", "CITY_DIR"):
67
+ entorno.pop(clave, None)
68
+ try:
69
+ r = subprocess.run(
70
+ [sys.executable, SHOW, "--story", historia, "--no-wait", "--step", "0"],
71
+ capture_output=True,
72
+ text=True,
73
+ env=entorno,
74
+ timeout=180,
75
+ )
76
+ if r.returncode:
77
+ raise RuntimeError((r.stderr or r.stdout).strip() or "the story did not play")
78
+ sueltos = glob.glob(os.path.join(app, ".runtime", "bus", "*", "activity.jsonl"))
79
+ if not sueltos:
80
+ raise RuntimeError("the bus recorded no activity")
81
+ eventos = []
82
+ for linea in open(sueltos[0], encoding="utf-8"):
83
+ linea = linea.strip()
84
+ if not linea:
85
+ continue
86
+ evento = json.loads(linea)
87
+ for clave in VOLATIL:
88
+ evento.pop(clave, None)
89
+ eventos.append(evento)
90
+ return eventos
91
+ finally:
92
+ _apaga(app)
93
+ shutil.rmtree(base, ignore_errors=True)
94
+
95
+
96
+ def _apaga(app):
97
+ """The hub detaches on purpose in a real city; here it must not outlive us."""
98
+ import signal
99
+
100
+ for ruta in glob.glob(os.path.join(app, ".runtime", "bus", "*", "endpoint.json")):
101
+ try:
102
+ pid = int(json.load(open(ruta, encoding="utf-8")).get("pid", 0))
103
+ if pid > 1:
104
+ os.kill(pid, signal.SIGTERM)
105
+ except (OSError, ValueError, TypeError, json.JSONDecodeError):
106
+ pass
107
+
108
+
109
+ def escribe(historia, eventos):
110
+ os.makedirs(DESTINO, exist_ok=True)
111
+ ruta = os.path.join(DESTINO, f"{historia}.jsonl")
112
+ with open(ruta, "w", encoding="utf-8") as f:
113
+ for evento in eventos:
114
+ f.write(json.dumps(evento, ensure_ascii=False, sort_keys=True) + "\n")
115
+ return ruta
116
+
117
+
118
+ def main(argv):
119
+ cuales = argv or sorted(STORIES)
120
+ for historia in cuales:
121
+ if historia not in STORIES:
122
+ print(f" no such story: {historia}", file=sys.stderr)
123
+ return 2
124
+ print(f" recording {historia}…")
125
+ eventos = graba(historia)
126
+ ruta = escribe(historia, eventos)
127
+ print(f" {len(eventos)} events -> {os.path.relpath(ruta, RAIZ)}")
128
+ return 0
129
+
130
+
131
+ if __name__ == "__main__":
132
+ sys.exit(main(sys.argv[1:]))
@@ -0,0 +1,22 @@
1
+ {"actor": "seat", "details": ["Desired outcome: Presentar en plazo algo completo y firmado, o saber HOY que no se puede y decírselo a la clienta con alternativas, no con excusas.", "Definition of done: Expediente completo y presentado, o la clienta avisada hoy con plan B · Nada pendiente de 'a ver si hay suerte'", "Invited: contratos, litigios, archivo"], "kind": "committee.opened", "phase": "collecting", "protocol": "agents-city-activity/1", "role": "chair", "seq": 1, "summary": "¿Llegamos al plazo de mañana a las 9 con la reclamación de la comunidad de vecinos?", "title": "The chair opened a committee", "tone": "question"}
2
+ {"actor": "litigios", "details": ["1/3 positions received"], "kind": "committee.position.submitted", "phase": "collecting", "protocol": "agents-city-activity/1", "role": "member", "seq": 2, "summary": "Its content stays sealed until every selected specialist has answered.", "target": "seat", "title": "litigios submitted an independent position", "tone": "work"}
3
+ {"actor": "contratos", "details": ["2/3 positions received"], "kind": "committee.position.submitted", "phase": "collecting", "protocol": "agents-city-activity/1", "role": "member", "seq": 3, "summary": "Its content stays sealed until every selected specialist has answered.", "target": "seat", "title": "contratos submitted an independent position", "tone": "work"}
4
+ {"actor": "archivo", "details": ["3/3 positions received"], "kind": "committee.position.submitted", "phase": "review", "protocol": "agents-city-activity/1", "role": "member", "seq": 4, "summary": "The collection barrier is complete; all positions can now be revealed together.", "target": "seat", "title": "archivo submitted an independent position", "tone": "work"}
5
+ {"actor": "seat", "details": ["The chair can now compare evidence, conflicts and unknowns without anchoring bias."], "kind": "committee.positions.revealed", "phase": "review", "protocol": "agents-city-activity/1", "role": "chair", "seq": 5, "summary": "3 specialists completed the blind first round.", "title": "All independent positions were revealed together", "tone": "evidence"}
6
+ {"actor": "contratos", "details": ["Stance: conditional", "Evidence: El informe se corrompió al escanearlo: el original en papel existe", "Evidence: El perito contestó un email esta mañana: está localizable", "Expected impact: Informe completo esta tarde si alguien va a su oficina", "Visible when: Con la última página escaneada de nuevo", "Risk: Quedarse esperando un reenvío que no llega"], "kind": "committee.position.revealed", "phase": "review", "protocol": "agents-city-activity/1", "role": "member", "seq": 6, "summary": "La página en blanco del perito es lo grave: sin sus conclusiones firmadas, la reclamación cojea. Hay que localizarle HOY; su oficina cierra a las 18h.", "target": "committee", "title": "contratos proposes", "tone": "evidence"}
7
+ {"actor": "litigios", "details": ["Stance: support", "Evidence: Los dos vecinos sin firmar viven en el mismo edificio", "Evidence: Uno ya dijo que sí por teléfono la semana pasada", "Expected impact: Las firmas, resueltas esta misma tarde en persona", "Visible when: Esta noche, con las firmas en el expediente", "Risk: Que uno de los dos no esté en casa"], "kind": "committee.position.revealed", "phase": "review", "protocol": "agents-city-activity/1", "role": "member", "seq": 7, "summary": "Se puede llegar, pero solo si esta tarde es solo para esto: las dos firmas se consiguen yendo a los domicilios, no esperando emails.", "target": "committee", "title": "litigios proposes", "tone": "evidence"}
8
+ {"actor": "archivo", "details": ["Stance: conditional", "Evidence: En un caso de 2024 se presentó con firmas pendientes y se subsanaron después", "Evidence: El registro telemático admite hasta las 23:59, no hasta las 9:00", "Expected impact: Saber cuánto margen real hay antes de gastar la tarde", "Visible when: En una hora, con el requisito confirmado", "Unknown: Si el plazo de las 9:00 es de registro o de reparto"], "kind": "committee.position.revealed", "phase": "review", "protocol": "agents-city-activity/1", "role": "member", "seq": 8, "summary": "Antes de correr: comprobar QUÉ exige exactamente el juzgado para tener por presentada la reclamación. Puede que las dos firmas que faltan ni siquiera sean obligatorias para presentar.", "target": "committee", "title": "archivo proposes", "tone": "evidence"}
9
+ {"actor": "seat", "details": ["Agreement: Ir a por las firmas en persona", "Agreement: Ir a la oficina del perito hoy", "Agreement: Confirmar el requisito exacto del juzgado", "Conflict: Cuánto de esto es imprescindible y cuánto es prudencia", "Unknown: Hora real límite: ¿9:00 o 23:59 telemático?"], "kind": "committee.synthesis.published", "phase": "deliberating", "protocol": "agents-city-activity/1", "role": "chair", "seq": 9, "summary": "Nadie propone rendirse. Tres frentes a la vez: firmas en persona esta tarde, el perito antes de las 18h, y archivo confirmando qué es imprescindible presentar y hasta qué hora de verdad.", "title": "The chair published the synthesis", "tone": "evidence"}
10
+ {"actor": "archivo", "details": ["Basis: new evidence", "Evidence: Registro telemático hasta las 23:59 de mañana; las firmas son subsanables en 10 días"], "kind": "committee.floor.requested", "phase": "deliberating", "protocol": "agents-city-activity/1", "role": "member", "seq": 10, "summary": "Ya está confirmado el requisito con el juzgado.", "target": "seat", "title": "archivo requested the floor", "tone": "floor"}
11
+ {"actor": "seat", "details": ["Request: Ya está confirmado el requisito con el juzgado.", "Evidence: Registro telemático hasta las 23:59 de mañana; las firmas son subsanables en 10 días"], "kind": "committee.floor.granted", "phase": "deliberating", "protocol": "agents-city-activity/1", "role": "chair", "seq": 11, "summary": "One scoped reply was granted for new evidence.", "target": "archivo", "title": "The chair gave archivo the floor", "tone": "floor"}
12
+ {"actor": "archivo", "details": ["Evidence: Diligencia de subsanación admitida en este juzgado, caso 2024", "Consequence: Toda la presión se concentra en el perito antes de las 18h.", "Every committee member heard this intervention and may request a bounded reply."], "kind": "committee.floor.spoke", "phase": "deliberating", "protocol": "agents-city-activity/1", "role": "member", "seq": 12, "summary": "Tenemos 15 horas más de las que creíamos y las firmas no bloquean la presentación. Lo ÚNICO imprescindible es el informe del perito.", "target": "committee", "title": "archivo spoke on the granted floor", "tone": "floor"}
13
+ {"actor": "litigios", "details": ["Basis: risk", "Evidence: El registro telemático estuvo caído dos veces este mes"], "kind": "committee.floor.requested", "phase": "deliberating", "protocol": "agents-city-activity/1", "role": "member", "seq": 13, "summary": "Hay una trampa en confiarlo todo al registro telemático.", "target": "seat", "title": "litigios requested the floor", "tone": "floor"}
14
+ {"actor": "seat", "details": ["Request: Hay una trampa en confiarlo todo al registro telemático.", "Evidence: El registro telemático estuvo caído dos veces este mes"], "kind": "committee.floor.granted", "phase": "deliberating", "protocol": "agents-city-activity/1", "role": "chair", "seq": 14, "summary": "One scoped reply was granted for risk.", "target": "litigios", "title": "The chair gave litigios the floor", "tone": "floor"}
15
+ {"actor": "litigios", "details": ["Evidence: Incidencias del registro: días 4 y 19 de este mes", "Consequence: La meta interna pasa a ser mañana a las 10:00, no a las 23:59.", "Every committee member heard this intervention and may request a bounded reply."], "kind": "committee.floor.spoke", "phase": "deliberating", "protocol": "agents-city-activity/1", "role": "member", "seq": 15, "summary": "Si apuramos a las 23:00 y el registro se cae, no hay plan B. Lo que esté listo se presenta por la mañana, no por la noche.", "target": "committee", "title": "litigios spoke on the granted floor", "tone": "floor"}
16
+ {"actor": "seat", "details": ["Rationale: El requisito real deja margen, pero el margen no se gasta: se presenta por la mañana con el informe completo.", "Decisive contributors: archivo, contratos, litigios", "Independent verifier: archivo"], "kind": "committee.decision.recorded", "phase": "verifying", "protocol": "agents-city-activity/1", "role": "chair", "seq": 16, "summary": "Esta tarde: una persona a la oficina del perito antes de las 18h y otra a por las dos firmas en persona. Se presenta mañana a las 10:00 con lo imprescindible completo; las firmas, si no llegan, se subsanan en los 10 días. A la presidenta se la llama HOY contando el plan tal cual.", "target": "archivo", "title": "The chair recorded a decision", "tone": "decision"}
17
+ {"actor": "archivo", "details": ["Evidence: El original del perito tiene las conclusiones, pero firmadas por su socio, que NO es el perito designado en el expediente", "Residual risk: El perito designado está de viaje hasta mañana"], "kind": "committee.verification.fail", "phase": "verification_failed", "protocol": "agents-city-activity/1", "role": "member", "seq": 17, "summary": "Cotejo del original en papel con la designación del expediente · La firma válida tiene que ser del perito designado", "target": "seat", "title": "archivo reported verification FAIL", "tone": "verification"}
18
+ {"actor": "seat", "details": [], "kind": "committee.replanned", "phase": "review", "protocol": "agents-city-activity/1", "role": "chair", "seq": 18, "summary": "El informe existe pero lo firma quien no debe. Presentarlo así es regalarle a la otra parte un motivo de impugnación. Se replantea.", "title": "The chair reopened the plan", "tone": "decision"}
19
+ {"actor": "seat", "details": ["Agreement: La firma tiene que ser del perito designado, sin atajos", "Unknown: Retraso del vuelo del perito"], "kind": "committee.synthesis.published", "phase": "deliberating", "protocol": "agents-city-activity/1", "role": "chair", "seq": 19, "summary": "El perito designado vuelve mañana y puede firmar a primera hora en el aeropuerto. Plan ajustado: firma a las 8:30, presentación a las 11:00, y la llamada a la clienta se adelanta a esta tarde.", "title": "The chair published the synthesis", "tone": "evidence"}
20
+ {"actor": "seat", "details": ["Rationale: Un expediente impecable a las 11:00 vale más que uno impugnable a las 9:00.", "Decisive contributors: archivo, litigios", "Independent verifier: archivo"], "kind": "committee.decision.recorded", "phase": "verifying", "protocol": "agents-city-activity/1", "role": "chair", "seq": 20, "summary": "Mañana 8:30, firma del perito designado a su llegada; 11:00, presentación telemática con el expediente completo; las dos firmas de vecinos ya conseguidas esta tarde se incorporan. Si el vuelo se retrasa, se presenta a las 16:00 — todavía siete horas dentro de plazo.", "target": "archivo", "title": "The chair recorded a decision", "tone": "decision"}
21
+ {"actor": "archivo", "details": ["Evidence: Firmas de los dos vecinos conseguidas esta tarde en persona", "Evidence: Perito designado confirmado en el vuelo de las 8:05; firma a las 8:30", "Residual risk: Retraso del vuelo — cubierto por el margen de la tarde"], "kind": "committee.verification.pass", "phase": "verified", "protocol": "agents-city-activity/1", "role": "member", "seq": 21, "summary": "Expediente montado y revisado dos veces esta noche · Presentación ensayada en el registro telemático con un borrador · La presidenta avisada: dejó de llorar cuando oyó el plan con horas", "target": "seat", "title": "archivo reported verification PASS", "tone": "verification"}
22
+ {"actor": "seat", "details": [], "kind": "committee.closed", "phase": "closed", "protocol": "agents-city-activity/1", "role": "chair", "seq": 22, "summary": "Reclamación presentada dentro de plazo, completa y sin flancos: firmas conseguidas en persona, informe firmado por quien debía, y la clienta informada con horas, no con promesas.", "title": "The chair closed the committee", "tone": "decision"}
@@ -0,0 +1,22 @@
1
+ {"actor": "seat", "details": ["Desired outcome: Que nadie con algo urgente se vaya sin ver a un médico, y que quien deba volver salga con una cita real en la mano.", "Definition of done: Sala de espera por debajo del aforo antes de mediodía · Cada persona reprogramada con día y hora, no con promesas", "Invited: urgencias, laboratorio, farmacia"], "kind": "committee.opened", "phase": "collecting", "protocol": "agents-city-activity/1", "role": "chair", "seq": 1, "summary": "¿Cómo atendemos hoy a las 60 personas citadas a la misma hora?", "title": "The chair opened a committee", "tone": "question"}
2
+ {"actor": "urgencias", "details": ["1/3 positions received"], "kind": "committee.position.submitted", "phase": "collecting", "protocol": "agents-city-activity/1", "role": "member", "seq": 2, "summary": "Its content stays sealed until every selected specialist has answered.", "target": "seat", "title": "urgencias submitted an independent position", "tone": "work"}
3
+ {"actor": "laboratorio", "details": ["2/3 positions received"], "kind": "committee.position.submitted", "phase": "collecting", "protocol": "agents-city-activity/1", "role": "member", "seq": 3, "summary": "Its content stays sealed until every selected specialist has answered.", "target": "seat", "title": "laboratorio submitted an independent position", "tone": "work"}
4
+ {"actor": "farmacia", "details": ["3/3 positions received"], "kind": "committee.position.submitted", "phase": "review", "protocol": "agents-city-activity/1", "role": "member", "seq": 4, "summary": "The collection barrier is complete; all positions can now be revealed together.", "target": "seat", "title": "farmacia submitted an independent position", "tone": "work"}
5
+ {"actor": "seat", "details": ["The chair can now compare evidence, conflicts and unknowns without anchoring bias."], "kind": "committee.positions.revealed", "phase": "review", "protocol": "agents-city-activity/1", "role": "chair", "seq": 5, "summary": "3 specialists completed the blind first round.", "title": "All independent positions were revealed together", "tone": "evidence"}
6
+ {"actor": "urgencias", "details": ["Stance: support", "Evidence: Hay tres personas con más de 80 años de pie en el pasillo", "Evidence: Un niño con fiebre alta lleva 40 minutos esperando", "Expected impact: Lo urgente se ve hoy; lo demás se reprograma con respeto", "Visible when: En cuanto la enfermera esté en la puerta", "Risk: Alguien urgente que no sabe explicar lo que le pasa"], "kind": "committee.position.revealed", "phase": "review", "protocol": "agents-city-activity/1", "role": "member", "seq": 6, "summary": "Triaje en la puerta ahora mismo: una enfermera pregunta a cada persona qué le pasa y separa lo que no puede esperar de lo que sí.", "target": "committee", "title": "urgencias proposes", "tone": "evidence"}
7
+ {"actor": "laboratorio", "details": ["Stance: conditional", "Evidence: 34 resultados listos sin entregar", "Evidence: 12 de las personas de la sala solo venían a recogerlos", "Expected impact: 12 personas salen en minutos con lo que venían a buscar", "Visible when: Con la lista impresa en recepción", "Risk: Entregar un resultado a la persona equivocada con las prisas"], "kind": "committee.position.revealed", "phase": "review", "protocol": "agents-city-activity/1", "role": "member", "seq": 7, "summary": "Que nadie repita pruebas por el caos: los análisis de esta semana ya están hechos y valen. Puedo imprimir la lista de resultados pendientes de entregar en diez minutos.", "target": "committee", "title": "laboratorio proposes", "tone": "evidence"}
8
+ {"actor": "farmacia", "details": ["Stance: conditional", "Evidence: 9 personas de la sala vienen solo a renovar recetas", "Evidence: Todas son medicaciones crónicas ya pautadas", "Expected impact: 9 personas más salen atendidas sin tocar la agenda médica", "Visible when: Al abrir la ventanilla de renovación", "Risk: Renovar sin mirar una interacción nueva"], "kind": "committee.position.revealed", "phase": "review", "protocol": "agents-city-activity/1", "role": "member", "seq": 8, "summary": "Las recetas de crónicos no necesitan consulta: renovarlas hoy en ventanilla con la historia delante. La señora de la medicación del corazón no debería esperar a un médico para su papel.", "target": "committee", "title": "farmacia proposes", "tone": "evidence"}
9
+ {"actor": "seat", "details": ["Agreement: Triaje inmediato", "Agreement: Entregar los 34 resultados", "Agreement: Renovar las 9 recetas crónicas", "Conflict: Qué decir a los ~35 restantes", "Unknown: Si la agenda de tarde también está duplicada"], "kind": "committee.synthesis.published", "phase": "deliberating", "protocol": "agents-city-activity/1", "role": "chair", "seq": 9, "summary": "Tres movimientos que no se pisan: triaje en la puerta, resultados en recepción y recetas en ventanilla. La sala baja de 60 a unos 35 sin cancelar a nadie. Falta decidir qué se hace con esos 35.", "title": "The chair published the synthesis", "tone": "evidence"}
10
+ {"actor": "urgencias", "details": ["Basis: new evidence", "Evidence: Un señor con dolor en el pecho llevaba 30 minutos callado en la fila"], "kind": "committee.floor.requested", "phase": "deliberating", "protocol": "agents-city-activity/1", "role": "member", "seq": 10, "summary": "El triaje de la puerta acaba de encontrar algo.", "target": "seat", "title": "urgencias requested the floor", "tone": "floor"}
11
+ {"actor": "seat", "details": ["Request: El triaje de la puerta acaba de encontrar algo.", "Evidence: Un señor con dolor en el pecho llevaba 30 minutos callado en la fila"], "kind": "committee.floor.granted", "phase": "deliberating", "protocol": "agents-city-activity/1", "role": "chair", "seq": 11, "summary": "One scoped reply was granted for new evidence.", "target": "urgencias", "title": "The chair gave urgencias the floor", "tone": "floor"}
12
+ {"actor": "urgencias", "details": ["Evidence: Atendido a los dos minutos del triaje", "Consequence: El orden del plan no se negocia: puerta primero.", "Every committee member heard this intervention and may request a bounded reply."], "kind": "committee.floor.spoke", "phase": "deliberating", "protocol": "agents-city-activity/1", "role": "member", "seq": 12, "summary": "Ya está dentro y atendido. Pero confirma la regla: el triaje va primero SIEMPRE, antes que cualquier reprogramación.", "target": "committee", "title": "urgencias spoke on the granted floor", "tone": "floor"}
13
+ {"actor": "farmacia", "details": ["Basis: dependency", "Evidence: Dos terminales de recepción siguen caídos por lo de las citas"], "kind": "committee.floor.requested", "phase": "deliberating", "protocol": "agents-city-activity/1", "role": "member", "seq": 13, "summary": "La ventanilla de recetas depende de poder ver la historia.", "target": "seat", "title": "farmacia requested the floor", "tone": "floor"}
14
+ {"actor": "seat", "details": ["Request: La ventanilla de recetas depende de poder ver la historia.", "Evidence: Dos terminales de recepción siguen caídos por lo de las citas"], "kind": "committee.floor.granted", "phase": "deliberating", "protocol": "agents-city-activity/1", "role": "chair", "seq": 14, "summary": "One scoped reply was granted for dependency.", "target": "farmacia", "title": "The chair gave farmacia the floor", "tone": "floor"}
15
+ {"actor": "farmacia", "details": ["Evidence: El terminal de laboratorio está libre y funciona", "Consequence: Laboratorio presta su terminal dos horas y todo cuadra.", "Every committee member heard this intervention and may request a bounded reply."], "kind": "committee.floor.spoke", "phase": "deliberating", "protocol": "agents-city-activity/1", "role": "member", "seq": 15, "summary": "Necesito un terminal que funcione en la ventanilla o renovaré a ciegas, y a ciegas no renuevo.", "target": "committee", "title": "farmacia spoke on the granted floor", "tone": "floor"}
16
+ {"actor": "seat", "details": ["Rationale: Cada persona de la sala cae en uno de los cuatro caminos, y ninguno depende de arreglar el sistema de citas esta mañana.", "Decisive contributors: urgencias, laboratorio, farmacia", "Independent verifier: laboratorio"], "kind": "committee.decision.recorded", "phase": "verifying", "protocol": "agents-city-activity/1", "role": "chair", "seq": 16, "summary": "Ahora mismo: triaje en la puerta. En paralelo, resultados en recepción y recetas en ventanilla con el terminal del laboratorio. A los ~35 restantes se les reprograma hoy con día y hora en mano, empezando por los mayores.", "target": "laboratorio", "title": "The chair recorded a decision", "tone": "decision"}
17
+ {"actor": "laboratorio", "details": ["Evidence: La agenda de TARDE también está duplicada: 48 citas para 24 huecos", "Evidence: Reprogramar a los 35 de la mañana hacia la tarde los mete en otra sala llena", "Residual risk: Repetir el caos a las 16h con la misma gente"], "kind": "committee.verification.fail", "phase": "verification_failed", "protocol": "agents-city-activity/1", "role": "member", "seq": 17, "summary": "Revisión de la agenda de tarde antes de reprogramar hacia ella", "target": "seat", "title": "laboratorio reported verification FAIL", "tone": "verification"}
18
+ {"actor": "seat", "details": [], "kind": "committee.replanned", "phase": "review", "protocol": "agents-city-activity/1", "role": "chair", "seq": 18, "summary": "Reprogramar hacia una tarde que también está rota es mover el caos de sitio, no resolverlo. Se replantea con la tarde a la vista.", "title": "The chair reopened the plan", "tone": "decision"}
19
+ {"actor": "seat", "details": ["Agreement: No reprogramar hacia la tarde de hoy", "Unknown: Cuánta gente de la tarde contesta al teléfono a tiempo"], "kind": "committee.synthesis.published", "phase": "deliberating", "protocol": "agents-city-activity/1", "role": "chair", "seq": 19, "summary": "La reprogramación salta la tarde de hoy: va directa a huecos reales de mañana y pasado, y la agenda de tarde se poda ahora llamando a quien tenga cita duplicada antes de que venga.", "title": "The chair published the synthesis", "tone": "evidence"}
20
+ {"actor": "seat", "details": ["Rationale: Se arregla la mañana sin fabricar el mismo problema a las 16h.", "Decisive contributors: laboratorio, urgencias", "Independent verifier: laboratorio"], "kind": "committee.decision.recorded", "phase": "verifying", "protocol": "agents-city-activity/1", "role": "chair", "seq": 20, "summary": "Los ~35 de la mañana salen con cita para mañana o pasado, no para esta tarde. Dos personas de admisión llaman ahora a la lista de la tarde para deshacer los duplicados antes de que nadie más venga.", "target": "laboratorio", "title": "The chair recorded a decision", "tone": "decision"}
21
+ {"actor": "laboratorio", "details": ["Evidence: 12:40: sala con 19 personas, por debajo del aforo", "Evidence: Tarde podada a 26 citas; 22 duplicados avisados por teléfono", "Residual risk: 4 duplicados de tarde sin contestar el teléfono"], "kind": "committee.verification.pass", "phase": "verified", "protocol": "agents-city-activity/1", "role": "member", "seq": 21, "summary": "Todos los urgentes vistos · 35 reprogramados con día y hora en mano · Recetas crónicas renovadas, incluida la de la señora del corazón", "target": "seat", "title": "laboratorio reported verification PASS", "tone": "verification"}
22
+ {"actor": "seat", "details": [], "kind": "committee.closed", "phase": "closed", "protocol": "agents-city-activity/1", "role": "chair", "seq": 22, "summary": "Mañana resuelta sin cerrar la puerta: urgencias vistas, resultados entregados, recetas renovadas, reprogramación real y tarde podada.", "title": "The chair closed the committee", "tone": "decision"}