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.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.es.md +12 -0
- package/README.md +12 -0
- package/bin/hall.html +3 -0
- package/bin/navegador.mjs +61 -16
- package/bin/test-adapter.py +9 -0
- package/bin/test-committee.py +83 -2
- package/bin/test-contracts.py +10 -3
- package/bin/test-navegador.py +34 -0
- package/bin/test-seat.py +56 -0
- package/city/web/dist-hall/hall.js +74 -70
- package/city/web/src/bienvenida.ts +9 -1
- package/city/web/src/es.ts +3 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/channel/adapter-prompts.ts +3 -1
- package/plugin/channel/adapter.js +3 -1
- package/plugin/channel/bus.js +3 -1
- package/plugin/channel/client.js +5 -1
- package/plugin/channel/committee/floor.ts +27 -5
- package/plugin/channel/committee/types.ts +27 -1
- package/plugin/channel/committee-cli.ts +5 -1
- package/plugin/channel/local-hub.js +26 -6
- package/plugin/channel/runtime-gateway.js +3 -1
- package/plugin/scripts/city-session.sh +40 -5
- package/plugin/scripts/consulta.py +14 -2
- package/plugin/skills/city/SKILL.md +17 -4
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
|
-
|
|
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(
|
|
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
|
|
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')), {
|
|
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(
|
|
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(
|
|
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 &&
|
|
320
|
-
casa
|
|
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' &&
|
|
472
|
-
reception?.
|
|
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 &&
|
|
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 &&
|
|
691
|
-
|
|
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(
|
|
702
|
-
|
|
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
|
}
|
package/bin/test-adapter.py
CHANGED
|
@@ -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'
|
package/bin/test-committee.py
CHANGED
|
@@ -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
|
-
|
|
318
|
-
and
|
|
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',
|
package/bin/test-contracts.py
CHANGED
|
@@ -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
|
|
478
|
-
|
|
479
|
-
|
|
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()
|
package/bin/test-navegador.py
CHANGED
|
@@ -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],
|