agents-city 0.5.7 → 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/test-adapter.py +9 -0
- package/bin/test-committee.py +83 -2
- package/bin/test-contracts.py +10 -3
- package/bin/test-seat.py +45 -0
- 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 +30 -4
- 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/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-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")
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "city",
|
|
3
3
|
"displayName": "Agents City",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.8",
|
|
5
5
|
"description": "One autonomous city seat with a work domain, role, goal, repo support agents, recognised skills and explicit roads to other cities.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "jlcases",
|
|
@@ -12,6 +12,7 @@ export function promptFor(envelope: BusEnvelope, operatingRole = 'blank'): strin
|
|
|
12
12
|
RULES,
|
|
13
13
|
role,
|
|
14
14
|
`Your first position is isolated: do not seek or read another member's answer. Inspect your own repo and use any matching skill installed there. Abstention is valid.`,
|
|
15
|
+
`Look, do not touch. A position is what you have found and what you would do — reading, running read-only checks, and reporting. Nothing here asks you to change a file, land a branch or run a migration, and doing it now would decide the very thing the chair convened this committee to decide. The act of gathering an opinion must leave the repo as it found it; work follows a decision, and this is not one yet.`,
|
|
15
16
|
`Brief:\n${pretty(envelope.payload.brief)}`,
|
|
16
17
|
`Submit evidence, not a chat reply. Run:`,
|
|
17
18
|
` agents-city committee schema respond`,
|
|
@@ -25,8 +26,9 @@ export function promptFor(envelope: BusEnvelope, operatingRole = 'blank'): strin
|
|
|
25
26
|
`${HEADER} The chair published the synthesis for ${thread}.`,
|
|
26
27
|
RULES,
|
|
27
28
|
pretty(envelope.payload.synthesis),
|
|
28
|
-
`Stay silent unless you have new evidence, a contradiction, a material risk or a dependency. If
|
|
29
|
+
`Stay silent unless you have new evidence, a contradiction, a material risk or a dependency. Wanting to add something is not one of those: your turn to contribute was your isolated position, and a floor taken for an opinion is how a committee becomes a chat room. If you do have one of the four, request—not take—the floor:`,
|
|
29
30
|
` agents-city committee floor-request ${thread} --input <request.json>`,
|
|
31
|
+
`There is a fifth basis and it is about this text rather than the world: \`misrepresented\`, for when the synthesis does not say what you said. Not "I disagree with the summary" — that it puts words in your mouth, drops your condition, or reports a stance you did not take. You get one, it does not spend your rebuttals even where the chair allowed none, and your own position is attached automatically so the chair is shown what it summarised rather than your memory of it.`,
|
|
30
32
|
`Use: agents-city committee schema floor-request`,
|
|
31
33
|
].join('\n\n');
|
|
32
34
|
}
|
|
@@ -3722,6 +3722,7 @@ function promptFor(envelope, operatingRole = "blank") {
|
|
|
3722
3722
|
RULES,
|
|
3723
3723
|
role,
|
|
3724
3724
|
`Your first position is isolated: do not seek or read another member's answer. Inspect your own repo and use any matching skill installed there. Abstention is valid.`,
|
|
3725
|
+
`Look, do not touch. A position is what you have found and what you would do \u2014 reading, running read-only checks, and reporting. Nothing here asks you to change a file, land a branch or run a migration, and doing it now would decide the very thing the chair convened this committee to decide. The act of gathering an opinion must leave the repo as it found it; work follows a decision, and this is not one yet.`,
|
|
3725
3726
|
`Brief:
|
|
3726
3727
|
${pretty(envelope.payload.brief)}`,
|
|
3727
3728
|
`Submit evidence, not a chat reply. Run:`,
|
|
@@ -3736,8 +3737,9 @@ ${pretty(envelope.payload.brief)}`,
|
|
|
3736
3737
|
`${HEADER} The chair published the synthesis for ${thread}.`,
|
|
3737
3738
|
RULES,
|
|
3738
3739
|
pretty(envelope.payload.synthesis),
|
|
3739
|
-
`Stay silent unless you have new evidence, a contradiction, a material risk or a dependency. If
|
|
3740
|
+
`Stay silent unless you have new evidence, a contradiction, a material risk or a dependency. Wanting to add something is not one of those: your turn to contribute was your isolated position, and a floor taken for an opinion is how a committee becomes a chat room. If you do have one of the four, request\u2014not take\u2014the floor:`,
|
|
3740
3741
|
` agents-city committee floor-request ${thread} --input <request.json>`,
|
|
3742
|
+
`There is a fifth basis and it is about this text rather than the world: \`misrepresented\`, for when the synthesis does not say what you said. Not "I disagree with the summary" \u2014 that it puts words in your mouth, drops your condition, or reports a stance you did not take. You get one, it does not spend your rebuttals even where the chair allowed none, and your own position is attached automatically so the chair is shown what it summarised rather than your memory of it.`,
|
|
3741
3743
|
`Use: agents-city committee schema floor-request`
|
|
3742
3744
|
].join("\n\n");
|
|
3743
3745
|
}
|
package/plugin/channel/bus.js
CHANGED
|
@@ -19816,6 +19816,7 @@ function promptFor(envelope, operatingRole = "blank") {
|
|
|
19816
19816
|
RULES,
|
|
19817
19817
|
role,
|
|
19818
19818
|
`Your first position is isolated: do not seek or read another member's answer. Inspect your own repo and use any matching skill installed there. Abstention is valid.`,
|
|
19819
|
+
`Look, do not touch. A position is what you have found and what you would do \u2014 reading, running read-only checks, and reporting. Nothing here asks you to change a file, land a branch or run a migration, and doing it now would decide the very thing the chair convened this committee to decide. The act of gathering an opinion must leave the repo as it found it; work follows a decision, and this is not one yet.`,
|
|
19819
19820
|
`Brief:
|
|
19820
19821
|
${pretty(envelope.payload.brief)}`,
|
|
19821
19822
|
`Submit evidence, not a chat reply. Run:`,
|
|
@@ -19830,8 +19831,9 @@ ${pretty(envelope.payload.brief)}`,
|
|
|
19830
19831
|
`${HEADER} The chair published the synthesis for ${thread}.`,
|
|
19831
19832
|
RULES,
|
|
19832
19833
|
pretty(envelope.payload.synthesis),
|
|
19833
|
-
`Stay silent unless you have new evidence, a contradiction, a material risk or a dependency. If
|
|
19834
|
+
`Stay silent unless you have new evidence, a contradiction, a material risk or a dependency. Wanting to add something is not one of those: your turn to contribute was your isolated position, and a floor taken for an opinion is how a committee becomes a chat room. If you do have one of the four, request\u2014not take\u2014the floor:`,
|
|
19834
19835
|
` agents-city committee floor-request ${thread} --input <request.json>`,
|
|
19836
|
+
`There is a fifth basis and it is about this text rather than the world: \`misrepresented\`, for when the synthesis does not say what you said. Not "I disagree with the summary" \u2014 that it puts words in your mouth, drops your condition, or reports a stance you did not take. You get one, it does not spend your rebuttals even where the chair allowed none, and your own position is attached automatically so the chair is shown what it summarised rather than your memory of it.`,
|
|
19835
19837
|
`Use: agents-city committee schema floor-request`
|
|
19836
19838
|
].join("\n\n");
|
|
19837
19839
|
}
|
package/plugin/channel/client.js
CHANGED
|
@@ -4146,7 +4146,11 @@ function schema(verb) {
|
|
|
4146
4146
|
unknowns: []
|
|
4147
4147
|
},
|
|
4148
4148
|
synthesize: { summary: "", agreements: [], conflicts: [], unknowns: [], proceedWithout: "" },
|
|
4149
|
-
"floor-request": {
|
|
4149
|
+
"floor-request": {
|
|
4150
|
+
basis: "new_evidence | contradiction | risk | dependency | misrepresented",
|
|
4151
|
+
reason: "what the committee does not yet know, or what the synthesis got wrong about you",
|
|
4152
|
+
evidence: [""]
|
|
4153
|
+
},
|
|
4150
4154
|
"floor-grant": { requestId: "" },
|
|
4151
4155
|
"floor-deny": { requestId: "", reason: "" },
|
|
4152
4156
|
reply: { claim: "", evidence: [""], consequence: "" },
|
|
@@ -11,20 +11,42 @@ export function requestFloor(
|
|
|
11
11
|
requireMember(state, actor, role);
|
|
12
12
|
if (state.status !== 'deliberating')
|
|
13
13
|
throw new Error('the floor opens only after the chair synthesis');
|
|
14
|
-
const
|
|
14
|
+
const basis = oneOf(payload.basis, FLOOR_BASES, 'basis');
|
|
15
15
|
const alreadyPending = state.floor.requests.some(
|
|
16
16
|
(request) => request.actor === actor && ['pending', 'granted'].includes(request.status),
|
|
17
17
|
);
|
|
18
18
|
if (alreadyPending) throw new Error(`${actor} already has a pending floor request`);
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
const mia = basis === 'misrepresented' ? state.positions[actor] : undefined;
|
|
20
|
+
if (basis === 'misrepresented') {
|
|
21
|
+
// Abstention is a valid position to hold and an empty one to misquote: with
|
|
22
|
+
// nothing on the record there is nothing the synthesis can have got wrong.
|
|
23
|
+
if (!mia) throw new Error(`${actor} submitted no position, so none can be misrepresented`);
|
|
24
|
+
// Once. Saying "that is not what I said" is a correction, and a correction
|
|
25
|
+
// that can be repeated is a filibuster with better manners.
|
|
26
|
+
if (state.floor.requests.some((r) => r.actor === actor && r.basis === 'misrepresented')) {
|
|
27
|
+
throw new Error(`${actor} already challenged how its position was represented`);
|
|
28
|
+
}
|
|
29
|
+
} else {
|
|
30
|
+
// The rebuttal budget governs DEBATE. A committee may legitimately forbid
|
|
31
|
+
// debate — `maxRebuttals: 0` is a real choice — and may not thereby forbid a
|
|
32
|
+
// member from saying the record is wrong about them, because the record is
|
|
33
|
+
// what the decision gets made from.
|
|
34
|
+
const alreadyUsed = state.floor.replies.filter((reply) => reply.actor === actor).length;
|
|
35
|
+
if (alreadyUsed >= state.brief.maxRebuttals) {
|
|
36
|
+
throw new Error(`${actor} reached this deliberation's rebuttal limit`);
|
|
37
|
+
}
|
|
21
38
|
}
|
|
22
39
|
const request: FloorRequest = {
|
|
23
40
|
id: randomId('floor'),
|
|
24
41
|
actor,
|
|
25
|
-
basis
|
|
42
|
+
basis,
|
|
26
43
|
reason: text(payload.reason, 'reason'),
|
|
27
|
-
|
|
44
|
+
// Every other basis has to bring something from the world. This one is
|
|
45
|
+
// answered by what the committee already holds — the position below — so
|
|
46
|
+
// demanding evidence on top would be asking a member to prove its own words
|
|
47
|
+
// with something other than its own words.
|
|
48
|
+
evidence: strings(payload.evidence, 'evidence', basis !== 'misrepresented'),
|
|
49
|
+
...(mia ? { position: mia } : {}),
|
|
28
50
|
status: 'pending',
|
|
29
51
|
requestedAt: isoNow(),
|
|
30
52
|
};
|
|
@@ -1,7 +1,28 @@
|
|
|
1
1
|
import { ActorRole } from '../protocol.js';
|
|
2
2
|
|
|
3
3
|
export const STANCES = ['support', 'oppose', 'conditional', 'abstain'] as const;
|
|
4
|
-
|
|
4
|
+
/** Why a member may interrupt a synthesis.
|
|
5
|
+
*
|
|
6
|
+
* The first four are about the WORLD: something is true that the chair did not
|
|
7
|
+
* have, said something the evidence contradicts, carries a risk, or depends on
|
|
8
|
+
* something. They are deliberately narrow — "I would like to add something" is
|
|
9
|
+
* how a committee becomes a chat room, and with the chat room comes the
|
|
10
|
+
* anchoring that isolated first positions exist to prevent.
|
|
11
|
+
*
|
|
12
|
+
* `misrepresented` is about the RECORD, and it is the one a member had no way
|
|
13
|
+
* to raise. A position summarised into something it did not say is neither new
|
|
14
|
+
* evidence nor a contradiction in the facts; it is the chair's account of what
|
|
15
|
+
* you said being wrong. Until this existed, the synthesis was the one place in
|
|
16
|
+
* the protocol where a single actor could bend the outcome and nothing could
|
|
17
|
+
* catch it: execution gets an independent verifier, the summary got nobody.
|
|
18
|
+
*/
|
|
19
|
+
export const FLOOR_BASES = [
|
|
20
|
+
'new_evidence',
|
|
21
|
+
'contradiction',
|
|
22
|
+
'risk',
|
|
23
|
+
'dependency',
|
|
24
|
+
'misrepresented',
|
|
25
|
+
] as const;
|
|
5
26
|
export const AUTHORITIES = ['recommend', 'decide', 'execute'] as const;
|
|
6
27
|
export const VERIFY_RESULTS = ['pass', 'fail'] as const;
|
|
7
28
|
export const TERMINAL_STATUSES = new Set(['closed', 'cancelled']);
|
|
@@ -40,6 +61,11 @@ export interface FloorRequest {
|
|
|
40
61
|
basis: (typeof FLOOR_BASES)[number];
|
|
41
62
|
reason: string;
|
|
42
63
|
evidence: string[];
|
|
64
|
+
/** On `misrepresented`, the requester's own position, attached by the bus.
|
|
65
|
+
* The point of the basis is that the chair is shown the literal text it
|
|
66
|
+
* summarised, not a member's memory of it — quoting yourself from memory is
|
|
67
|
+
* the same class of evidence as the summary being challenged. */
|
|
68
|
+
position?: Position;
|
|
43
69
|
status: 'pending' | 'granted' | 'used' | 'denied';
|
|
44
70
|
requestedAt: string;
|
|
45
71
|
decidedAt?: string;
|
|
@@ -82,7 +82,11 @@ function schema(verb: string): Record<string, unknown> {
|
|
|
82
82
|
unknowns: [],
|
|
83
83
|
},
|
|
84
84
|
synthesize: { summary: '', agreements: [], conflicts: [], unknowns: [], proceedWithout: '' },
|
|
85
|
-
'floor-request': {
|
|
85
|
+
'floor-request': {
|
|
86
|
+
basis: 'new_evidence | contradiction | risk | dependency | misrepresented',
|
|
87
|
+
reason: 'what the committee does not yet know, or what the synthesis got wrong about you',
|
|
88
|
+
evidence: [''],
|
|
89
|
+
},
|
|
86
90
|
'floor-grant': { requestId: '' },
|
|
87
91
|
'floor-deny': { requestId: '', reason: '' },
|
|
88
92
|
reply: { claim: '', evidence: [''], consequence: '' },
|
|
@@ -3981,7 +3981,13 @@ function limpia(bruto) {
|
|
|
3981
3981
|
|
|
3982
3982
|
// committee/types.ts
|
|
3983
3983
|
var STANCES = ["support", "oppose", "conditional", "abstain"];
|
|
3984
|
-
var FLOOR_BASES = [
|
|
3984
|
+
var FLOOR_BASES = [
|
|
3985
|
+
"new_evidence",
|
|
3986
|
+
"contradiction",
|
|
3987
|
+
"risk",
|
|
3988
|
+
"dependency",
|
|
3989
|
+
"misrepresented"
|
|
3990
|
+
];
|
|
3985
3991
|
var AUTHORITIES = ["recommend", "decide", "execute"];
|
|
3986
3992
|
var VERIFY_RESULTS = ["pass", "fail"];
|
|
3987
3993
|
var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["closed", "cancelled"]);
|
|
@@ -4280,20 +4286,34 @@ function requestFloor(state, payload, actor, role) {
|
|
|
4280
4286
|
requireMember(state, actor, role);
|
|
4281
4287
|
if (state.status !== "deliberating")
|
|
4282
4288
|
throw new Error("the floor opens only after the chair synthesis");
|
|
4283
|
-
const
|
|
4289
|
+
const basis = oneOf(payload.basis, FLOOR_BASES, "basis");
|
|
4284
4290
|
const alreadyPending = state.floor.requests.some(
|
|
4285
4291
|
(request2) => request2.actor === actor && ["pending", "granted"].includes(request2.status)
|
|
4286
4292
|
);
|
|
4287
4293
|
if (alreadyPending) throw new Error(`${actor} already has a pending floor request`);
|
|
4288
|
-
|
|
4289
|
-
|
|
4294
|
+
const mia = basis === "misrepresented" ? state.positions[actor] : void 0;
|
|
4295
|
+
if (basis === "misrepresented") {
|
|
4296
|
+
if (!mia) throw new Error(`${actor} submitted no position, so none can be misrepresented`);
|
|
4297
|
+
if (state.floor.requests.some((r) => r.actor === actor && r.basis === "misrepresented")) {
|
|
4298
|
+
throw new Error(`${actor} already challenged how its position was represented`);
|
|
4299
|
+
}
|
|
4300
|
+
} else {
|
|
4301
|
+
const alreadyUsed = state.floor.replies.filter((reply) => reply.actor === actor).length;
|
|
4302
|
+
if (alreadyUsed >= state.brief.maxRebuttals) {
|
|
4303
|
+
throw new Error(`${actor} reached this deliberation's rebuttal limit`);
|
|
4304
|
+
}
|
|
4290
4305
|
}
|
|
4291
4306
|
const request = {
|
|
4292
4307
|
id: randomId("floor"),
|
|
4293
4308
|
actor,
|
|
4294
|
-
basis
|
|
4309
|
+
basis,
|
|
4295
4310
|
reason: text(payload.reason, "reason"),
|
|
4296
|
-
|
|
4311
|
+
// Every other basis has to bring something from the world. This one is
|
|
4312
|
+
// answered by what the committee already holds — the position below — so
|
|
4313
|
+
// demanding evidence on top would be asking a member to prove its own words
|
|
4314
|
+
// with something other than its own words.
|
|
4315
|
+
evidence: strings(payload.evidence, "evidence", basis !== "misrepresented"),
|
|
4316
|
+
...mia ? { position: mia } : {},
|
|
4297
4317
|
status: "pending",
|
|
4298
4318
|
requestedAt: isoNow()
|
|
4299
4319
|
};
|
|
@@ -3723,6 +3723,7 @@ function promptFor(envelope, operatingRole = "blank") {
|
|
|
3723
3723
|
RULES,
|
|
3724
3724
|
role,
|
|
3725
3725
|
`Your first position is isolated: do not seek or read another member's answer. Inspect your own repo and use any matching skill installed there. Abstention is valid.`,
|
|
3726
|
+
`Look, do not touch. A position is what you have found and what you would do \u2014 reading, running read-only checks, and reporting. Nothing here asks you to change a file, land a branch or run a migration, and doing it now would decide the very thing the chair convened this committee to decide. The act of gathering an opinion must leave the repo as it found it; work follows a decision, and this is not one yet.`,
|
|
3726
3727
|
`Brief:
|
|
3727
3728
|
${pretty(envelope.payload.brief)}`,
|
|
3728
3729
|
`Submit evidence, not a chat reply. Run:`,
|
|
@@ -3737,8 +3738,9 @@ ${pretty(envelope.payload.brief)}`,
|
|
|
3737
3738
|
`${HEADER} The chair published the synthesis for ${thread}.`,
|
|
3738
3739
|
RULES,
|
|
3739
3740
|
pretty(envelope.payload.synthesis),
|
|
3740
|
-
`Stay silent unless you have new evidence, a contradiction, a material risk or a dependency. If
|
|
3741
|
+
`Stay silent unless you have new evidence, a contradiction, a material risk or a dependency. Wanting to add something is not one of those: your turn to contribute was your isolated position, and a floor taken for an opinion is how a committee becomes a chat room. If you do have one of the four, request\u2014not take\u2014the floor:`,
|
|
3741
3742
|
` agents-city committee floor-request ${thread} --input <request.json>`,
|
|
3743
|
+
`There is a fifth basis and it is about this text rather than the world: \`misrepresented\`, for when the synthesis does not say what you said. Not "I disagree with the summary" \u2014 that it puts words in your mouth, drops your condition, or reports a stance you did not take. You get one, it does not spend your rebuttals even where the chair allowed none, and your own position is attached automatically so the chair is shown what it summarised rather than your memory of it.`,
|
|
3742
3744
|
`Use: agents-city committee schema floor-request`
|
|
3743
3745
|
].join("\n\n");
|
|
3744
3746
|
}
|
|
@@ -526,6 +526,34 @@ entorno_de() { # $1 = actor ("seat" or a window name)
|
|
|
526
526
|
printf '%s%s' "$(sync_line)AGENTS_CITY_DATA=$EQUIPO AGENTS_CITY_HOME=${AGENTS_CITY_HOME:-$HOME/.agents-city} AGENTS_CITY_USER=$USUARIO CITY_ADDRESS=$ADDRESS CITY_SEAT_NAME=$SEAT_NAME " "$propio"
|
|
527
527
|
}
|
|
528
528
|
|
|
529
|
+
# A house's Claude, opened the way its card asks for.
|
|
530
|
+
#
|
|
531
|
+
# `gateway` by default, and that default is what makes a city a city: work
|
|
532
|
+
# reaches the agent without anybody sitting in its terminal, and the whole
|
|
533
|
+
# conversation shows up in the Hall instead of in a pane nobody is watching.
|
|
534
|
+
#
|
|
535
|
+
# `ui.<house>: tui` opens the person's own Claude Code instead — their plugins,
|
|
536
|
+
# their statusline, their slash commands, their transcript. The cost is stated
|
|
537
|
+
# rather than hidden: delivery falls back to a protected paste into the pane,
|
|
538
|
+
# and `city-runtime.sh fallback` warns that native delivery is unavailable.
|
|
539
|
+
#
|
|
540
|
+
# That choice already existed for the chair, and `ui_de` was written as a
|
|
541
|
+
# general function consulted for exactly one window. Which is the tell: the
|
|
542
|
+
# chair — the one actor that receives text from OTHER cities — has been running
|
|
543
|
+
# pasted delivery all along, so refusing a house the same option while granting
|
|
544
|
+
# it there was backwards on risk, not careful about it.
|
|
545
|
+
lanza_casa_claude() { # $1 = window, $2 = cwd, $3 = the full claude command, $4 = wait
|
|
546
|
+
local win="$1" ruta="$2" orden="$3" espera="${4:-}" entorno
|
|
547
|
+
entorno="$(entorno_de "$win")$BROKER_ENV$JAULA$CAGE_RUNTIME_ENV"
|
|
548
|
+
if [ "$(ui_de "$win" gateway)" = tui ]; then
|
|
549
|
+
lanza "$SESSION:$win" "$win" "$ruta" "$espera$entorno${CLAUDE_AUTH_PREFIX}$orden"
|
|
550
|
+
"$RUNTIME" fallback "$win" "$SESSION:$win" claude
|
|
551
|
+
else
|
|
552
|
+
lanza "$SESSION:$win" "$win" "$ruta" \
|
|
553
|
+
"$espera$entorno${CLAUDE_AUTH_PREFIX}$(gateway_line "$win" "$ruta" "$orden")"
|
|
554
|
+
fi
|
|
555
|
+
}
|
|
556
|
+
|
|
529
557
|
# The chair's Claude, opened the way the card asks for.
|
|
530
558
|
#
|
|
531
559
|
# Both spellings carry the SAME flags. That matters: `--settings` closes
|
|
@@ -651,8 +679,7 @@ for path in ${RUTAS[@]+"${RUTAS[@]}"}; do
|
|
|
651
679
|
turno=$((turno + 1))
|
|
652
680
|
if [ "$espera" = "0" ]; then espera=''; else espera="sleep $espera; "; fi
|
|
653
681
|
CLAUDE_REPO="$(con_motor "$win" claude "$OTRO --name $SESSION-$win")$CLAUDE_TRATO$YOLO_FLAG"
|
|
654
|
-
|
|
655
|
-
"$espera$(entorno_de "$win")$BROKER_ENV$JAULA$CAGE_RUNTIME_ENV${CLAUDE_AUTH_PREFIX}$(gateway_line "$win" "$path" "$CLAUDE_REPO")"
|
|
682
|
+
lanza_casa_claude "$win" "$path" "$CLAUDE_REPO" "$espera"
|
|
656
683
|
elif [ "$KIND" = codex ] || [ "$KIND" = opencode ] || [ "$KIND" = kimi ]; then
|
|
657
684
|
# Native servers have their own credentials and do not share Claude's OAuth race.
|
|
658
685
|
# Codex stays outside the outer cage ON macOS ONLY: its node_repl MCP
|
|
@@ -684,8 +711,7 @@ for path in ${RUTAS[@]+"${RUTAS[@]}"}; do
|
|
|
684
711
|
turno=$((turno + 1))
|
|
685
712
|
if [ "$espera" = "0" ]; then espera=''; else espera="sleep $espera; "; fi
|
|
686
713
|
CLAUDE_REPO="$(con_motor "$win" claude "claude --name $SESSION-$win")$CLAUDE_TRATO$YOLO_FLAG"
|
|
687
|
-
|
|
688
|
-
"$espera$(entorno_de "$win")$BROKER_ENV${JAULA}$CAGE_RUNTIME_ENV${CLAUDE_AUTH_PREFIX}$(gateway_line "$win" "$path" "$CLAUDE_REPO")"
|
|
714
|
+
lanza_casa_claude "$win" "$path" "$CLAUDE_REPO" "$espera"
|
|
689
715
|
fi
|
|
690
716
|
fi
|
|
691
717
|
done
|
|
@@ -103,16 +103,29 @@ Use a committee only when specialised repo evidence can change the decision:
|
|
|
103
103
|
chair or another member before the barrier opens. Abstention is valid.
|
|
104
104
|
3. Compare evidence and publish one synthesis. Do not count votes.
|
|
105
105
|
4. After synthesis, members stay silent unless they can identify new evidence, a
|
|
106
|
-
contradiction, a material risk or a dependency.
|
|
107
|
-
|
|
106
|
+
contradiction, a material risk or a dependency. Wanting to add something is
|
|
107
|
+
not one of those — the moment to contribute was the isolated position, and a
|
|
108
|
+
floor taken for an opinion is how a committee becomes a chat room. They
|
|
109
|
+
request the floor; they never take it.
|
|
110
|
+
5. There is a fifth basis, `misrepresented`, and it is about the record rather
|
|
111
|
+
than the world: the synthesis does not say what that member said. Not
|
|
112
|
+
disagreement with the summary — words put in their mouth, a condition
|
|
113
|
+
dropped, a stance reported that they did not take. Each member gets one, it
|
|
114
|
+
does not spend the rebuttal budget even where the brief allowed none, and
|
|
115
|
+
the bus attaches their stored position so the chair is shown what it
|
|
116
|
+
summarised rather than a memory of it. A member the chair proceeded without
|
|
117
|
+
has nothing on the record and so cannot raise it. This exists because
|
|
118
|
+
execution gets an independent verifier and the summary had nobody: it was
|
|
119
|
+
the one place where a single actor could bend the outcome unchecked.
|
|
120
|
+
6. Grant or deny every request explicitly. A grant allows one scoped reply. That
|
|
108
121
|
intervention is then heard by all selected members, but nobody answers it
|
|
109
122
|
directly: a material counterpoint requires a new request which the chair
|
|
110
123
|
sequences. This creates deliberation without opening a free debate.
|
|
111
|
-
|
|
124
|
+
7. Record outcome, rationale, owner, executor, selected and rejected evidence,
|
|
112
125
|
decisive contributors, dissent and observable conditions that reopen the
|
|
113
126
|
decision. Review `agents-city committee history`; repeated influence is a
|
|
114
127
|
prompt to inspect capture, not a verdict.
|
|
115
|
-
|
|
128
|
+
8. Assign verification to an agent other than the executor whenever one exists.
|
|
116
129
|
A failed verification must replan; only a pass may close the act.
|
|
117
130
|
|
|
118
131
|
The vendor-neutral terminal door is:
|