agents-city 0.5.6 → 0.5.8

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.
@@ -9,7 +9,7 @@
9
9
  {
10
10
  "name": "city",
11
11
  "source": "./plugin",
12
- "version": "0.5.6",
12
+ "version": "0.5.8",
13
13
  "description": "Operate one autonomous city seat, its domain, role, repo support agents, goal, recognised skills and explicit roads to other cities."
14
14
  }
15
15
  ]
package/README.es.md CHANGED
@@ -1828,6 +1828,18 @@ global del plugin porque varias ciudades pueden ejecutarse a la vez.
1828
1828
  sexta pregunta del asistente o con `agents-city seat --seat-yolo on|off`;
1829
1829
  `--no-yolo` sigue frenando la sesión entera.
1830
1830
 
1831
+ `ui.<agente>` elige qué se abre en una ventana: `tui` es el Claude Code de esa
1832
+ persona — sus plugins, su statusline, sus comandos — y `gateway` es el prompt de
1833
+ la ciudad con el runtime headless detrás. La silla usa `tui` por defecto y una
1834
+ casa `gateway`, y los dos defectos son los útiles: una silla es donde te sientas,
1835
+ y una casa es donde llega el trabajo sin que nadie se siente en ella, con la
1836
+ conversación entera apareciendo en el Hall.
1837
+
1838
+ El intercambio se dice, no se esconde. El TUI de Claude Code no tiene puerta de
1839
+ entrada, así que una ventana en `tui` recibe su trabajo como un pegado protegido
1840
+ en el pane, y el lanzador avisa de que la entrega nativa no está disponible ahí.
1841
+ Es el mismo trato que la silla lleva haciendo desde siempre.
1842
+
1831
1843
  `seat_reach` dice si la silla trabaja con sus propias manos. Está `closed`
1832
1844
  mientras no escribas `open`. Cerrado significa que un asiento tiene las
1833
1845
  herramientas de una silla y ninguna más: su carpeta de ciudad, las puertas de
package/README.md CHANGED
@@ -1808,6 +1808,18 @@ launches the chair itself without permission prompts — set at the wizard's
1808
1808
  sixth question or with `agents-city seat --seat-yolo on|off`; `--no-yolo`
1809
1809
  still brakes the whole session.
1810
1810
 
1811
+ `ui.<agent>` chooses what opens in a window: `tui` is that person's own Claude
1812
+ Code — their plugins, their statusline, their slash commands — and `gateway` is
1813
+ the city's own prompt with the runtime headless behind it. The chair defaults to
1814
+ `tui` and a house to `gateway`, and both defaults are the useful ones: a chair is
1815
+ where you sit, and a house is where work arrives without anybody sitting in it,
1816
+ with the whole conversation showing up in the Hall.
1817
+
1818
+ The trade is stated rather than hidden. Claude Code's TUI has no inbound door,
1819
+ so a `tui` window receives its work as a protected paste into the pane, and the
1820
+ launcher warns that native delivery is unavailable for it. That is the same
1821
+ bargain the chair has always made.
1822
+
1811
1823
  `seat_reach` says whether the chair works with its own hands. It is `closed`
1812
1824
  unless you write `open`. Closed means a seat holds a chair's tools and nothing
1813
1825
  else: its own city folder, this product's own doors (`agents-city …` — that is
package/bin/hall.html CHANGED
@@ -386,6 +386,9 @@ input[type=text]:focus,select:focus{outline:none;border-color:var(--lampara)}
386
386
  /* ── the guided first run ────────────────────────────────────────────────── */
387
387
  .bv{display:flex;flex-direction:column;gap:26px;max-width:min(760px,100%)}
388
388
  .bv .prosa{max-width:100%}
389
+ /* The consequence of a choice, under the button that makes it: quieter than the
390
+ question and still legible, because a warning nobody reads is decoration. */
391
+ .bv .prosa.apunte{font-size:13px;margin-top:10px;opacity:.82}
389
392
  .bv code.mono{word-break:break-all}
390
393
  /* The live rail has nothing to show before a city exists, and it costs half
391
394
  the screen exactly when the guide needs it. */
package/bin/navegador.mjs CHANGED
@@ -35,11 +35,31 @@ function dondeEstaChrome() {
35
35
  return CANDIDATOS.find((p) => existsSync(p)) || '';
36
36
  }
37
37
 
38
- /** Chrome prints its DevTools endpoint on stderr; that is the handshake. */
39
- function esperaEndpoint(proceso, limite = 20000) {
38
+ /** Chrome prints its DevTools endpoint on stderr; that is the handshake.
39
+ *
40
+ * Sixty seconds, not twenty. This is a browser cold-starting on a shared CI
41
+ * runner that is also finishing two thousand other checks, and twenty seconds
42
+ * was a coin toss we kept winning until we did not. Waiting longer costs
43
+ * nothing when it works, because the wait ends at the handshake.
44
+ *
45
+ * And a timeout now carries whatever Chrome DID say. "chrome never announced
46
+ * its port" is a sentence about the observer: it names what we stopped waiting
47
+ * for and drops the one piece of evidence — a missing library, a sandbox
48
+ * refusal, a profile it could not write — that says which of those it was.
49
+ */
50
+ function esperaEndpoint(proceso, limite = Number(process.env.CITY_BROWSER_WAIT_MS) || 60000) {
40
51
  return new Promise((listo, falla) => {
41
52
  let visto = '';
42
- const reloj = setTimeout(() => falla(new Error('chrome never announced its port')), limite);
53
+ const reloj = setTimeout(
54
+ () =>
55
+ falla(
56
+ new Error(
57
+ `chrome never announced its port in ${limite / 1000}s` +
58
+ (visto.trim() ? `; it said: ${visto.trim().slice(-400)}` : ', and said nothing'),
59
+ ),
60
+ ),
61
+ limite,
62
+ );
43
63
  proceso.stderr.on('data', (trozo) => {
44
64
  visto += trozo.toString();
45
65
  const m = visto.match(/ws:\/\/[^\s]+/);
@@ -65,7 +85,9 @@ class Cdp {
65
85
  const espera = this.pendientes.get(mensaje.id);
66
86
  if (!espera) return;
67
87
  this.pendientes.delete(mensaje.id);
68
- mensaje.error ? espera.falla(new Error(JSON.stringify(mensaje.error))) : espera.listo(mensaje.result);
88
+ mensaje.error
89
+ ? espera.falla(new Error(JSON.stringify(mensaje.error)))
90
+ : espera.listo(mensaje.result);
69
91
  });
70
92
  }
71
93
 
@@ -73,7 +95,9 @@ class Cdp {
73
95
  const socket = new WebSocket(url);
74
96
  await new Promise((listo, falla) => {
75
97
  socket.addEventListener('open', listo, { once: true });
76
- socket.addEventListener('error', () => falla(new Error('cannot reach chrome')), { once: true });
98
+ socket.addEventListener('error', () => falla(new Error('cannot reach chrome')), {
99
+ once: true,
100
+ });
77
101
  });
78
102
  return new Cdp(socket);
79
103
  }
@@ -102,7 +126,9 @@ const dormir = (ms) => new Promise((r) => setTimeout(r, ms));
102
126
 
103
127
  let fallos = 0;
104
128
  function comprueba(texto, bien, detalle = '') {
105
- console.log(`${bien ? ' ok ·' : ' FAIL·'} ${texto}${bien || !detalle ? '' : `\n ${detalle}`}`);
129
+ console.log(
130
+ `${bien ? ' ok ·' : ' FAIL·'} ${texto}${bien || !detalle ? '' : `\n ${detalle}`}`,
131
+ );
106
132
  if (!bien) fallos += 1;
107
133
  }
108
134
 
@@ -195,7 +221,11 @@ async function main() {
195
221
  test: boton('.rpgTest'),
196
222
  };
197
223
  })()`);
198
- comprueba('the houses view renders its sheets', (abierto?.fichas ?? 0) > 0, JSON.stringify(abierto));
224
+ comprueba(
225
+ 'the houses view renders its sheets',
226
+ (abierto?.fichas ?? 0) > 0,
227
+ JSON.stringify(abierto),
228
+ );
199
229
  for (const [control, etiqueta] of [
200
230
  ['alta', 'the build-a-house button'],
201
231
  ['instrucciones', 'the CLAUDE.md / AGENTS.md editors'],
@@ -316,8 +346,11 @@ async function main() {
316
346
  })()`);
317
347
  comprueba(
318
348
  'the house form walks the disk — nothing offered, you pick what you want',
319
- (casa?.filas ?? 0) > 0 && !!casa?.sube && !!casa?.tomaAqui &&
320
- casa.dondeDespues !== casa.dondeAntes && (casa?.elegidas ?? 0) > 0,
349
+ (casa?.filas ?? 0) > 0 &&
350
+ !!casa?.sube &&
351
+ !!casa?.tomaAqui &&
352
+ casa.dondeDespues !== casa.dondeAntes &&
353
+ (casa?.elegidas ?? 0) > 0,
321
354
  JSON.stringify(casa),
322
355
  );
323
356
  comprueba(
@@ -468,13 +501,16 @@ async function main() {
468
501
  })()`);
469
502
  comprueba(
470
503
  'thirty connected people use one recipient picker, not thirty message boxes',
471
- reception?.navRole === 'button' && reception?.navTabIndex === '0' &&
472
- reception?.composers === 1 && reception?.recipients === 30,
504
+ reception?.navRole === 'button' &&
505
+ reception?.navTabIndex === '0' &&
506
+ reception?.composers === 1 &&
507
+ reception?.recipients === 30,
473
508
  JSON.stringify(reception),
474
509
  );
475
510
  comprueba(
476
511
  'pending messages stay above composing and use distinct route/reject language',
477
- reception?.messages === 1 && reception?.reviewFirst === true &&
512
+ reception?.messages === 1 &&
513
+ reception?.reviewFirst === true &&
478
514
  /route|dirigir/i.test(reception?.route ?? '') &&
479
515
  /reason|motivo/i.test(reception?.rejection ?? ''),
480
516
  JSON.stringify(reception),
@@ -687,8 +723,11 @@ async function main() {
687
723
  })()`);
688
724
  comprueba(
689
725
  'the language switch really translates the page, and remembers it',
690
- !!lengua && !lengua.falta && lengua.despues !== lengua.antes &&
691
- /casas|resumen|mapa/i.test(lengua.despues) && lengua.guardado === lengua.lang &&
726
+ !!lengua &&
727
+ !lengua.falta &&
728
+ lengua.despues !== lengua.antes &&
729
+ /casas|resumen|mapa/i.test(lengua.despues) &&
730
+ lengua.guardado === lengua.lang &&
692
731
  lengua.etiqueta !== lengua.etiquetaAntes,
693
732
  JSON.stringify(lengua),
694
733
  );
@@ -698,8 +737,14 @@ async function main() {
698
737
  JSON.stringify(lengua?.marca),
699
738
  );
700
739
 
701
- const errores = await cdp.evalua('window.__erroresDePagina ? window.__erroresDePagina.length : 0');
702
- comprueba('the page raised no uncaught errors while we drove it', errores === 0, String(errores));
740
+ const errores = await cdp.evalua(
741
+ 'window.__erroresDePagina ? window.__erroresDePagina.length : 0',
742
+ );
743
+ comprueba(
744
+ 'the page raised no uncaught errors while we drove it',
745
+ errores === 0,
746
+ String(errores),
747
+ );
703
748
  } finally {
704
749
  proceso.kill('SIGKILL');
705
750
  }
@@ -117,6 +117,15 @@ def main():
117
117
  and 'agents-city committee respond' in body
118
118
  and 'Do not contact another repo agent directly' in body,
119
119
  body[-160:])
120
+ # A member read its brief and answered "let me start working on it",
121
+ # then ran thirteen shell commands. Investigating is what it was
122
+ # asked for; deciding by doing is not, and nothing in the brief had
123
+ # ever said so. Gathering an opinion has to leave the repo as it
124
+ # found it, or the committee is ratifying work already done.
125
+ afirma(f'· and {runtime} is told that gathering a position changes nothing',
126
+ 'Look, do not touch' in body
127
+ and 'leave the repo as it found it' in body,
128
+ body[-300:])
120
129
  esperado = (
121
130
  'operating role is seo'
122
131
  if actor == 'legacy-two'
@@ -220,6 +220,82 @@ def main():
220
220
  })
221
221
  afirma('· the chair resolves the request explicitly', denied.returncode == 0)
222
222
 
223
+ # ── the record, which had nobody checking it ────────────────────────
224
+ #
225
+ # Execution gets an independent verifier. The SUMMARY got nobody: a
226
+ # position quietly softened, a condition dropped, a stance reported as
227
+ # something it was not, and the member it belonged to had no valid basis
228
+ # to say so. Not new evidence, not a contradiction in the facts — a
229
+ # challenge to the chair's account of what they said.
230
+ mal_citado = valor(ejecuta(env, 'web', 'floor-request', thread, {
231
+ 'basis': 'misrepresented',
232
+ 'reason': 'the synthesis reports me as unconditional; my stance was conditional',
233
+ 'evidence': [],
234
+ }))
235
+ mal_id = mal_citado.get('myFloorRequests', [{}])[-1].get('id', '')
236
+ afirma('· a member can say the synthesis is not what it said', bool(mal_id))
237
+ peticion = next((r for r in mal_citado.get('myFloorRequests', [])
238
+ if r.get('id') == mal_id), {})
239
+ afirma(
240
+ "· and its own position travels with it, so the chair is shown what it summarised",
241
+ peticion.get('position', {}).get('recommendation')
242
+ == web_position['recommendation'],
243
+ json.dumps(peticion)[:300],
244
+ )
245
+ bloquea = ejecuta(env, 'seat', 'decide', thread, {
246
+ 'outcome': 'ship', 'rationale': 'gates pass', 'owner': 'alice',
247
+ 'executor': 'seat', 'verifier': 'ops',
248
+ 'verificationQuestion': 'does replay pass?',
249
+ 'selectedEvidence': ['run 482'], 'reopenIf': ['replay fails'],
250
+ })
251
+ afirma('· non-happy: and the chair cannot decide over an unanswered one',
252
+ bloquea.returncode != 0 and 'pending floor request' in bloquea.stderr)
253
+ ejecuta(env, 'seat', 'floor-deny', thread, {
254
+ 'requestId': mal_id, 'reason': 'the stance is quoted verbatim in the act',
255
+ })
256
+ # Resolved, so the pending guard is out of the way and what refuses the
257
+ # second one is the rule this basis carries: once.
258
+ otra_vez = ejecuta(env, 'web', 'floor-request', thread, {
259
+ 'basis': 'misrepresented', 'reason': 'still not right', 'evidence': [],
260
+ })
261
+ afirma('· non-happy: a correction that repeats is a filibuster with better manners',
262
+ otra_vez.returncode != 0
263
+ and 'already challenged how its position' in otra_vez.stderr,
264
+ otra_vez.stderr.strip())
265
+
266
+ # The one case where a member has nothing on the record: the chair
267
+ # synthesised without it, said why, and now it turns up. There is no
268
+ # summary of its position to be wrong, because it never gave one — and a
269
+ # basis that would let it claim otherwise would let anybody claim
270
+ # anything.
271
+ segundo = valor(ejecuta(env, 'seat', 'open', payload={
272
+ 'question': 'Do we cache the banner?',
273
+ 'desiredOutcome': 'A yes or no',
274
+ 'definitionOfDone': ['a decision'],
275
+ 'participants': ['api', 'ops'],
276
+ 'authority': 'recommend',
277
+ }))
278
+ hilo2 = segundo.get('id', '')
279
+ ejecuta(env, 'api', 'respond', hilo2, {
280
+ 'stance': 'support', 'recommendation': 'cache it',
281
+ 'evidence': ['cdn hit rate'], 'expectedImpact': 'fewer origin hits',
282
+ 'visibleWhen': 'next deploy', 'withdrawIf': 'the banner changes hourly',
283
+ })
284
+ sin_esperar = ejecuta(env, 'seat', 'synthesize', hilo2, {
285
+ 'summary': 'api says cache', 'agreements': ['cache it'],
286
+ 'conflicts': [], 'unknowns': [],
287
+ 'proceedWithout': 'ops has not answered in an hour and the window closes',
288
+ })
289
+ afirma('· a chair may proceed without a position, having said why',
290
+ sin_esperar.returncode == 0, sin_esperar.stderr.strip())
291
+ tarde = ejecuta(env, 'ops', 'floor-request', hilo2, {
292
+ 'basis': 'misrepresented', 'reason': 'that is not my view', 'evidence': [],
293
+ })
294
+ afirma('· non-happy: and one who said nothing has nothing to be misquoted about',
295
+ tarde.returncode != 0 and 'submitted no position' in tarde.stderr,
296
+ tarde.stderr.strip()[:200])
297
+ ejecuta(env, 'seat', 'cancel', hilo2, {'reason': 'fixture'})
298
+
223
299
  decision = {
224
300
  'outcome': 'ship after staging replay',
225
301
  'rationale': 'migration and rollback evidence agree',
@@ -313,9 +389,14 @@ def main():
313
389
  len(events) >= 16 and events[-1]['type'] == 'committee.close',
314
390
  f'{len(events)} events')
315
391
  history = valor(ejecuta(env, 'seat', 'history'))
392
+ # Indexed defensively: an earlier transition that stops working leaves
393
+ # these lists empty, and an IndexError here would replace every failure
394
+ # this suite had collected with a traceback about the last one.
395
+ reciente = (history.get('recent') or [{}])[0]
396
+ contribuyentes = (history.get('contributorCounts') or [{}])[0]
316
397
  afirma('· the chair gets concise cross-decision memory against capture',
317
- history.get('recent', [{}])[0].get('deliberation') == thread
318
- and history.get('contributorCounts', [{}])[0].get('decisions') == 2
398
+ reciente.get('deliberation') == thread
399
+ and contribuyentes.get('decisions') == 2
319
400
  and 'not proof of capture' in history.get('note', ''), str(history))
320
401
  denied_history = ejecuta(env, 'api', 'history')
321
402
  afirma('· decision influence history is chair-only',
@@ -474,9 +474,16 @@ def asiento_con_su_arnes():
474
474
  and "crossSessionInbound" not in fuente
475
475
  and "SendMessage,ListAgents" not in fuente, '')
476
476
 
477
- # A house is not a chair: it must still be reachable from the bus.
478
- afirma('· an agent house still goes through the gateway',
479
- 'gateway_line "$win" "$path" "$CLAUDE_REPO"' in fuente, '')
477
+ # A house is not a chair, and its DEFAULT must keep it reachable from the
478
+ # bus: work arrives without anybody sitting in that window. The choice is
479
+ # now offered — `ui.<house>: tui` opens the person's own Claude Code — so
480
+ # what this checks is the default, not the absence of a choice.
481
+ casa_claude = fuente[fuente.index('lanza_casa_claude() {'):fuente.index("# The chair's Claude")]
482
+ afirma('· a house defaults to the gateway, which is what makes work reach it',
483
+ 'ui_de "$win" gateway' in casa_claude
484
+ and 'gateway_line "$win" "$ruta" "$orden"' in casa_claude, casa_claude[:400])
485
+ afirma('· and a house that keeps its own CLI is given a way to be handed work',
486
+ '"$RUNTIME" fallback "$win"' in casa_claude, casa_claude[:400])
480
487
 
481
488
  # `ui.<window>`, lifted out and exercised.
482
489
  casa = tempfile.mkdtemp()
@@ -145,9 +145,43 @@ def bundle_al_dia():
145
145
  return "rebuilt from src/hall.ts"
146
146
 
147
147
 
148
+ def un_chrome_que_no_arranca():
149
+ """What the driver says when the browser never answers.
150
+
151
+ This suite has exactly one way to fail without telling anybody anything, and
152
+ CI found it: `chrome never announced its port` names what we stopped waiting
153
+ for and drops the only evidence of why — a missing library, a sandbox
154
+ refusal, a profile it could not write. A stub browser that talks and never
155
+ announces proves the message carries what it heard.
156
+
157
+ CHROME_PATH is the first candidate the driver considers, which is what makes
158
+ this testable without a browser at all.
159
+ """
160
+ print(" and a browser that never starts says why")
161
+ casa = tempfile.mkdtemp()
162
+ falso = os.path.join(casa, "chrome")
163
+ with open(falso, "w", encoding="utf-8") as f:
164
+ f.write("#!/bin/bash\n"
165
+ "echo 'FATAL: could not open display, and this is the useful half' >&2\n"
166
+ "sleep 30\n")
167
+ os.chmod(falso, 0o755)
168
+ salida = subprocess.run(
169
+ ["node", os.path.join(RAIZ, "bin", "navegador.mjs"), "http://127.0.0.1:1/"],
170
+ capture_output=True, text=True, timeout=90,
171
+ env=dict(os.environ, CHROME_PATH=falso, CITY_BROWSER_WAIT_MS="1500"),
172
+ )
173
+ shutil.rmtree(casa, ignore_errors=True)
174
+ todo = salida.stdout + salida.stderr
175
+ afirma("· the timeout names what it waited for",
176
+ "never announced its port" in todo, todo[-300:])
177
+ afirma("· and repeats what the browser actually said",
178
+ "could not open display" in todo, todo[-300:])
179
+
180
+
148
181
  def main():
149
182
  print()
150
183
  print(" the hall, in a browser that clicks")
184
+ un_chrome_que_no_arranca()
151
185
  print(f" · {bundle_al_dia()}")
152
186
  datos = tempfile.mkdtemp()
153
187
  casa = tempfile.mkdtemp()
package/bin/test-seat.py CHANGED
@@ -1495,6 +1495,51 @@ def arranque_escalonado():
1495
1495
  and "--channels" not in asiento,
1496
1496
  asiento[:160],
1497
1497
  )
1498
+ # And the same choice, for a house. It is the point of the product — orchestrate
1499
+ # somebody's CLI, do not replace it — and until now `ui_de` was a general
1500
+ # function consulted for exactly one window.
1501
+ card.pon_campo(ficha, "ui.api", "tui")
1502
+ lineas = corre(CITY_SETTLE="0", CITY_STAGGER="0")
1503
+ api_tui = next((l for l in lineas if "CITY_BUS_ACTOR=api" in l), "")
1504
+ afirma(
1505
+ "· happy: `ui.api: tui` opens that house's own Claude Code, not a prompt in front of it",
1506
+ "city-runtime.sh gateway api" not in api_tui and "claude" in api_tui,
1507
+ api_tui[:300],
1508
+ )
1509
+ aviso = subprocess.run(
1510
+ ["bash", guion, "ana", "--claude", "--no-sync"],
1511
+ capture_output=True, text=True, cwd=casa,
1512
+ env=dict(os.environ, HOME=casa, AGENTS_CITY_DATA=datos, CITY_CODE_DIR=codigo,
1513
+ PATH=fbin + os.pathsep + os.environ["PATH"],
1514
+ CITY_SETTLE="0", CITY_STAGGER="0"),
1515
+ ).stderr
1516
+ afirma(
1517
+ "· and the city says out loud what that costs, rather than hiding it",
1518
+ "api uses the explicit terminal fallback" in aviso
1519
+ and "native delivery is unavailable" in aviso,
1520
+ aviso[-400:],
1521
+ )
1522
+ afirma(
1523
+ "· non-happy: while its cage and its bus identity are exactly as before",
1524
+ "CITY_BUS_ACTOR=api" in api_tui and "CITY_AGENT_ROLE=data-engineer" in api_tui,
1525
+ api_tui[:300],
1526
+ )
1527
+ card.pon_campo(ficha, "ui.api", "")
1528
+ api_pasarela = next(
1529
+ (l for l in corre(CITY_SETTLE="0", CITY_STAGGER="0") if "CITY_BUS_ACTOR=api" in l), "")
1530
+ afirma(
1531
+ "· non-happy: and clearing the key puts the gateway back, which is the default",
1532
+ "city-runtime.sh gateway api" in api_pasarela, api_pasarela[:300],
1533
+ )
1534
+ card.pon_campo(ficha, "ui.api", "ventana-preciosa")
1535
+ api_raro = next(
1536
+ (l for l in corre(CITY_SETTLE="0", CITY_STAGGER="0") if "CITY_BUS_ACTOR=api" in l), "")
1537
+ afirma(
1538
+ "· non-happy: a value that is neither tui nor gateway is not a third mode",
1539
+ "city-runtime.sh gateway api" in api_raro, api_raro[:300],
1540
+ )
1541
+ card.pon_campo(ficha, "ui.api", "")
1542
+
1498
1543
  # The old shape is still one card key away, for anybody who wants the bus
1499
1544
  # able to drive their chair.
1500
1545
  card.pon_campo(ficha, "ui.seat", "gateway")
@@ -1521,6 +1566,11 @@ def arranque_escalonado():
1521
1566
  corre(**extra)
1522
1567
  return open(registro).read()
1523
1568
 
1569
+ primera = crudo(CITY_SETTLE="0", CITY_STAGGER="0")
1570
+ afirma(
1571
+ "· happy: a city opened for the first time gets its tmux comforts",
1572
+ "set-option" in primera, primera[:300],
1573
+ )
1524
1574
  log = crudo(FAKE_SESSION="1", FAKE_WINDOWS="seat api", CITY_SETTLE="0", CITY_STAGGER="0")
1525
1575
  afirma(
1526
1576
  "· happy: an agent added to a running city gets its window",
@@ -1542,6 +1592,12 @@ def arranque_escalonado():
1542
1592
  "new-session" not in log,
1543
1593
  log[:600],
1544
1594
  )
1595
+ afirma(
1596
+ "· non-happy: and a running session's tmux options are left exactly as found",
1597
+ "set-option" not in log,
1598
+ "re-applying `set -g mouse on` under a live full-screen app types raw SGR "
1599
+ "sequences into its prompt: " + log[:400],
1600
+ )
1545
1601
  afirma(
1546
1602
  "· the new house is what you are looking at when it opens",
1547
1603
  "select-window" in log and ":docs" in log.split("select-window")[-1][:40],