@astrosheep/square 0.3.89 → 0.3.91

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.
@@ -160,7 +160,9 @@ function commandIdentity(env) {
160
160
  if (selected === undefined)
161
161
  return {};
162
162
  const body = { channel: selected.channel, session: selected.session };
163
- const wakeAddress = paseo === undefined ? undefined : { kind: 'paseo', address: { agentId: paseo.session } };
163
+ const wakeAddress = selected.channel === 'codex'
164
+ ? { kind: 'codex-queue', address: { threadId: selected.session } }
165
+ : paseo === undefined ? undefined : { kind: 'paseo', address: { agentId: paseo.session } };
164
166
  return { body, ...(wakeAddress === undefined ? {} : { wakeAddress }) };
165
167
  }
166
168
  /** Adapter lifetime only; each callback uses the actual public resource API. */
@@ -10,10 +10,13 @@ export const migrateCommand = {
10
10
  async execute(_intent, context) {
11
11
  const squarePath = requireSquarePath(context);
12
12
  const outcome = await migrateSquare(squareInput(context));
13
- const body = outcome.status === 'migrated' && outcome.counts !== undefined
13
+ const body = outcome.status === 'migrated'
14
14
  ? [
15
15
  '✓ migrated to current row storage',
16
- ` · ${outcome.counts.activities} activities · ${outcome.counts.receipts} receipts · ${outcome.counts.leases} leases · ${outcome.counts.routes} routes`,
16
+ ...(outcome.counts === undefined ? [] : [
17
+ ` · ${outcome.counts.activities} activities · ${outcome.counts.receipts} receipts · ${outcome.counts.leases} leases · ${outcome.counts.routes} routes`,
18
+ ` · ${outcome.counts.recoveredSeats} recovered seats · ${outcome.counts.unownedStanding} unowned standing · ${outcome.counts.sharedBodySeats} shared-body seats · ${outcome.counts.skippedEvidence} skipped evidence`,
19
+ ]),
17
20
  ].join('\n')
18
21
  : '· already current format — nothing changed';
19
22
  return { output: withPathOutput(squarePath, body, { participantCount: outcome.standing }) };
package/dist/help.js CHANGED
@@ -92,8 +92,8 @@ const COMMANDS = [
92
92
  },
93
93
  {
94
94
  names: ['migrate'], usage: 'migrate', usesSquare: true, group: 'maintenance',
95
- summary: 'Convert an existing version 2 or 3 square to current format 4 in place.',
96
- details: ['For the selected .square artifact only. Stop sessions using this artifact before conversion. Back up the stopped artifact, migrate it, then run square --location <path> doctor before starting updated clients.', 'A version 2 or 3 square is accepted; version 1, unrelated or unsupported artifacts are refused without a write. There is no --check, --backup or --force.'],
95
+ summary: 'Convert a supported older square to the current format in place.',
96
+ details: ['For the selected .square artifact only. Stop sessions using this artifact before conversion. Back up the stopped artifact, migrate it, then run square --location <path> doctor before starting updated clients.', 'Versions 2, 3 and 4 are accepted; version 1, unrelated or unsupported artifacts are refused without a write. Names whose own owner and route evidence agrees may share a body; names with missing or conflicting ownership stay standing without write or delivery authority. Migration reports recovered seats, unowned standing names, shared-body seats and skipped evidence. There is no --check, --backup or --force.'],
97
97
  },
98
98
  ];
99
99
  function definitionFor(command) {
package/dist/mcp.js CHANGED
@@ -44,6 +44,9 @@ export function createSquareMcpServer(options) {
44
44
  : baseEnv;
45
45
  square = await Square.at({
46
46
  ...options, env, body, cwd: claude?.cwd ?? options.cwd,
47
+ // A call-local Codex thread uses its native queue even when managed by Paseo.
48
+ wakeAddress: claude === undefined && typeof threadId === 'string' && threadId.trim()
49
+ ? { kind: 'codex-queue', address: { threadId } } : options.wakeAddress,
47
50
  path: location ?? options.path,
48
51
  participant: participant ?? options.participant,
49
52
  // The server owner is bound to its configured square; overrides close after each call.
@@ -293,6 +293,10 @@ export function renderStatusResult(squarePath, data) {
293
293
  const output = [
294
294
  style('dim', counts),
295
295
  ...(hold === undefined ? [] : ['', hold]), '', style('dim', 'around the square'), ...people, '', style('dim', 'latest'), ...latest,
296
+ ...(data.listening === undefined ? [] : [
297
+ '', style('dim', 'you listen to'),
298
+ ...(data.listening.length === 0 ? [' ○ not turned toward anyone'] : data.listening.map((name) => ` · ${participantIdentity(name)}`)),
299
+ ]),
296
300
  ].join('\n');
297
301
  return withPathOutput(squarePath, output, { participantCount: presentation.standingCount, held: presentation.held });
298
302
  }
@@ -56,6 +56,10 @@ function rejectUnsentActivity(operation) {
56
56
  throw error;
57
57
  }
58
58
  }
59
+ /** A generic managed address must not displace the receiver's native registration on reconnect. */
60
+ function canRefreshWakeAddress(current, proposed) {
61
+ return proposed.kind !== 'paseo' || current === undefined || current.kind === 'paseo';
62
+ }
59
63
  /** One concrete local resource, including its entry-resolved dependencies and lifetime. */
60
64
  export class SqliteSquare {
61
65
  location;
@@ -466,7 +470,7 @@ export class SqliteSquare {
466
470
  else if (current?.joined) {
467
471
  if (currentSeat === undefined || !sameBody(currentSeat.body, body))
468
472
  throw new SquareError('already_joined', `${participantIdentity(participant)} is held by another body; take it over explicitly`);
469
- if (this.wakeAddress !== undefined)
473
+ if (this.wakeAddress !== undefined && canRefreshWakeAddress(currentSeat.wakeAddress, this.wakeAddress))
470
474
  facts.updateWakeAddress(asSeatRef(currentSeat), this.wakeAddress);
471
475
  kind = 'reconnected';
472
476
  }
@@ -507,7 +511,7 @@ export class SqliteSquare {
507
511
  if (decision.joinAct === undefined) {
508
512
  if (decision.state === 'active' && (seat === undefined || !sameBody(seat.body, body)))
509
513
  throw new SquareError('already_joined', `${participantIdentity(decision.joinedName)} is held by another body`);
510
- if (decision.state === 'active' && this.wakeAddress !== undefined)
514
+ if (decision.state === 'active' && this.wakeAddress !== undefined && canRefreshWakeAddress(seat.wakeAddress, this.wakeAddress))
511
515
  facts.updateWakeAddress(asSeatRef(seat), this.wakeAddress);
512
516
  }
513
517
  else {
@@ -556,10 +560,13 @@ export class SqliteSquare {
556
560
  }, control?.signal)).value;
557
561
  }
558
562
  async status(options = {}, control) {
559
- const viewer = !options.observe ? '' : options.viewer ?? (await this.participant().catch(() => undefined))?.name ?? '';
560
- return (await this.artifact.read(facts => queryStatus(facts, {
561
- location: this.location, now: this.clock(), automaticDeliveryAvailable: this.automaticDelivery && this.body !== undefined,
562
- }, viewer, options.observe === true), control?.signal)).value;
563
+ return (await this.artifact.read(facts => {
564
+ const seats = this.body === undefined ? [] : facts.seatsForBody(this.body);
565
+ const viewer = options.viewer ?? this.defaultParticipant ?? (seats.length === 1 ? seats[0].participant : '');
566
+ return queryStatus(facts, {
567
+ location: this.location, now: this.clock(), automaticDeliveryAvailable: this.automaticDelivery && this.body !== undefined,
568
+ }, viewer, options.observe === true);
569
+ }, control?.signal)).value;
563
570
  }
564
571
  async participants(options = {}, control) {
565
572
  return (await this.artifact.read(facts => queryParticipants(facts, {
@@ -229,6 +229,8 @@ export interface StatusOptions extends ObservationOptions {
229
229
  readonly viewer?: string;
230
230
  }
231
231
  export interface StatusResult {
232
+ /** Present when a viewer resolves; an empty list means they listen to nobody. */
233
+ readonly listening?: readonly string[];
232
234
  readonly hardCap: number | null;
233
235
  readonly activeCount: number;
234
236
  readonly doneCount: number;
@@ -6,6 +6,10 @@ export interface MigrationCounts {
6
6
  readonly receipts: number;
7
7
  readonly leases: number;
8
8
  readonly routes: number;
9
+ readonly recoveredSeats: number;
10
+ readonly unownedStanding: number;
11
+ readonly sharedBodySeats: number;
12
+ readonly skippedEvidence: number;
9
13
  }
10
14
  export interface MigrationOutcome {
11
15
  readonly status: 'migrated' | 'already-current';
@@ -229,25 +229,24 @@ function establishingJoin(driver, participant) {
229
229
  return join.index;
230
230
  }
231
231
  function planSeats(location, roster, routes, owners, driver) {
232
- const standing = roster.filter((participant) => participant.joined);
233
- return standing.map((participant) => {
232
+ const seats = [];
233
+ const localRoutes = routes.filter(route => canonicalPathSync(route.location) === location);
234
+ for (const participant of roster.filter((participant) => participant.joined)) {
234
235
  const joined = establishingJoin(driver, participant);
235
- const candidates = routes.filter((route) => canonicalPathSync(route.location) === location && nameKey(route.participant) === nameKey(participant.name));
236
+ const candidates = localRoutes.filter((route) => nameKey(route.participant) === nameKey(participant.name));
236
237
  const owner = owners.find(row => nameKey(row.participant) === nameKey(participant.name));
237
- assertSource(owner !== undefined, `standing participant ${participant.name} has no legacy owner.`);
238
- const { body } = owner;
239
- assertSource(candidates.length > 0 && candidates.every(route => sameBody(route.body, body)), `standing participant ${participant.name} has no unique legacy body agreeing with its owner.`);
240
- const shared = [...owners, ...routes.filter(route => canonicalPathSync(route.location) === location)]
241
- .some(row => nameKey(row.participant) !== nameKey(participant.name) && sameBody(row.body, body));
242
- assertSource(!shared, `standing participant ${participant.name} shares a legacy body with another owner.`);
238
+ const body = owner?.body ?? candidates[0]?.body;
239
+ if (body === undefined || !candidates.every(route => sameBody(route.body, body)))
240
+ continue;
243
241
  const address = uniqueSeatAddress(candidates);
244
- return {
242
+ seats.push({
245
243
  participant: participant.name,
246
244
  body,
247
245
  generation: formatActivityId(joined),
248
246
  ...(address === undefined ? {} : { wakeAddress: address }),
249
- };
250
- });
247
+ });
248
+ }
249
+ return seats;
251
250
  }
252
251
  function tableToken(database, table, order) {
253
252
  const rows = database.prepare(`SELECT * FROM ${table} ORDER BY ${order}`).all();
@@ -313,15 +312,20 @@ function inspectLegacyRowArtifact(database, location, host) {
313
312
  leaseGenerations.push({ ownerJson: row.owner_json, generation: formatActivityId(ownerParticipant.lastJoinIndex) });
314
313
  }
315
314
  const seats = planSeats(location, roster, routes, host.owners, driver);
315
+ const standingCount = roster.filter(participant => participant.joined).length;
316
+ const deliveries = planLegacyDeliveries(host.evidence, seats, driver);
316
317
  const counts = {
317
318
  activities: activityCount,
318
319
  receipts: Number(database.prepare('SELECT COUNT(*) AS n FROM receipts').get().n),
319
320
  leases: leaseOwners.length,
320
321
  routes: routes.length,
322
+ recoveredSeats: seats.length,
323
+ unownedStanding: standingCount - seats.length,
324
+ sharedBodySeats: seats.filter(seat => seats.some(other => other !== seat && sameBody(other.body, seat.body))).length,
325
+ skippedEvidence: host.evidence.length - deliveries.length,
321
326
  };
322
327
  const sourceToken = legacySourceToken(database);
323
- return { standing: seats.length, seats, leaseGenerations, sourceToken, counts,
324
- deliveries: planLegacyDeliveries(host.evidence, seats, driver) };
328
+ return { standing: standingCount, seats, leaseGenerations, sourceToken, counts, deliveries };
325
329
  }
326
330
  function legacyPayloadHash(payload) {
327
331
  return createHash('sha256').update(payload).digest('hex');
@@ -348,47 +352,49 @@ function legacyDeliveryPayload(source, current) {
348
352
  }
349
353
  function planLegacyDeliveries(records, seats, driver) {
350
354
  const byGroup = new Map();
351
- const tokens = new Set(), completedBodies = new Map();
355
+ const completedBodies = new Map();
352
356
  for (const source of records) {
357
+ // Validate structure even when no surviving owner can accept this row.
358
+ assertSource(source.kind === 'wake' || source.kind === 'presentation', 'delivery evidence kind is unsupported.');
359
+ assertSource(['dispatching', 'accepted', 'failed', 'unknown', 'released', 'presented', 'clipped'].includes(source.outcome), 'delivery evidence outcome is unsupported.');
360
+ assertSource(source.session.length > 0
361
+ && (source.channel === undefined || typeof source.channel === 'string' && source.channel.length > 0)
362
+ && (source.agent === undefined || typeof source.agent === 'string' && source.agent.length > 0), 'delivery evidence body is malformed.');
363
+ const index = parseActivityId(source.activity);
364
+ assertSource(index !== undefined, 'delivery evidence activity id is malformed.');
365
+ const at = sourceFinite(source.at);
366
+ const expiresAt = source.expiresAt === undefined ? undefined : sourceFinite(source.expiresAt);
367
+ const claimToken = source.claimToken;
368
+ assertSource(claimToken === undefined || typeof claimToken === 'string' && claimToken.length > 0, 'delivery claim token is malformed.');
369
+ assertSource(source.attemptN === undefined || Number.isSafeInteger(source.attemptN) && source.attemptN >= 0, 'delivery attempt number is malformed.');
353
370
  const participant = driver.participant(source.participant);
371
+ const seat = seats.find(seat => nameKey(seat.participant) === nameKey(source.participant));
372
+ const payload = legacyDeliveryPayload(source, seat?.wakeAddress);
354
373
  const native = source.kind === 'wake' && source.nativeDelivery !== undefined;
355
- if (!participant?.joined && !native)
356
- continue; // Noncritical completed diagnostics are not imported.
357
- assertSource(participant?.joined || participant?.done, `native delivery evidence for ${source.participant} has no unique participant.`);
374
+ if (!participant?.joined && !(participant?.done && native))
375
+ continue;
376
+ if (participant.joined && (seat === undefined || seat.body.session !== source.session
377
+ || source.agent !== seat.body.agent || source.channel !== undefined && source.channel !== seat.body.channel))
378
+ continue;
379
+ const harness = source.nativeDelivery?.harness;
380
+ if (native && seat !== undefined && harness !==
381
+ (seat.body.channel === 'claude-code' ? 'claude' : seat.body.channel === 'opencode' ? 'opencode' : undefined))
382
+ continue;
358
383
  const joined = establishingJoin(driver, participant);
359
384
  const generation = formatActivityId(joined);
360
- const seat = seats.find(seat => nameKey(seat.participant) === nameKey(participant.name));
361
- if (participant.joined)
362
- assertSource(seat !== undefined && seat.body.session === source.session
363
- && source.agent === seat.body.agent && (source.channel === undefined || source.channel === seat.body.channel), `delivery evidence for standing participant ${source.participant} has no unique owner.`);
364
- if (native && seat !== undefined)
365
- assertSource(source.nativeDelivery?.harness ===
366
- (seat.body.channel === 'claude-code' ? 'claude' : seat.body.channel === 'opencode' ? 'opencode' : undefined), 'native delivery harness conflicts with its owner body.');
367
- const index = parseActivityId(source.activity);
368
- assertSource(index !== undefined && index > joined && driver.activity(index)?.kind === 'say'
369
- && driver.directedAt(participant.name, index) !== undefined, `delivery evidence for ${source.participant} has no unique activity in its join generation.`);
370
- const payload = legacyDeliveryPayload(source, seat?.wakeAddress);
385
+ if (index <= joined || driver.activity(index)?.kind !== 'say' || driver.directedAt(participant.name, index) === undefined)
386
+ continue;
371
387
  if (participant.done) {
372
388
  const done = driver.activityHeads({ actors: [participant.name], kinds: ['done'] }, { edge: 'newest', limit: 1 }).items[0];
373
- assertSource(done !== undefined && index < done.index && source.session.length > 0, `native delivery evidence for ${source.participant} has no unique completed generation.`);
374
- const harness = source.nativeDelivery.harness;
389
+ if (done === undefined || index >= done.index)
390
+ continue;
375
391
  const body = { channel: harness === 'claude' ? 'claude-code' : 'opencode', session: source.session,
376
392
  ...(source.agent === undefined ? {} : { agent: source.agent }) };
377
- validateBody(body);
393
+ if (source.channel !== undefined && source.channel !== body.channel)
394
+ continue;
378
395
  const coordinate = JSON.stringify([body.channel, body.session, body.agent]);
379
- const previous = completedBodies.get(nameKey(participant.name));
380
- assertSource(previous === undefined || previous === coordinate, 'completed native delivery owner mapping is ambiguous.');
381
- completedBodies.set(nameKey(participant.name), coordinate);
382
- }
383
- assertSource(source.kind === 'wake' || source.kind === 'presentation', 'delivery evidence kind is unsupported.');
384
- assertSource(['dispatching', 'accepted', 'failed', 'unknown', 'released', 'presented', 'clipped'].includes(source.outcome), 'delivery evidence outcome is unsupported.');
385
- const at = sourceFinite(source.at);
386
- const expiresAt = source.expiresAt === undefined ? undefined : sourceFinite(source.expiresAt);
387
- const claimToken = source.claimToken;
388
- assertSource(claimToken === undefined || typeof claimToken === 'string' && claimToken.length > 0, 'delivery claim token is malformed.');
389
- if (claimToken !== undefined) {
390
- assertSource(!tokens.has(claimToken), 'delivery claim token is ambiguous.');
391
- tokens.add(claimToken);
396
+ const key = nameKey(participant.name), previous = completedBodies.get(key);
397
+ completedBodies.set(key, previous === undefined || previous === coordinate ? coordinate : null);
392
398
  }
393
399
  const group = JSON.stringify([nameKey(participant.name), generation, index, source.kind]);
394
400
  const row = {
@@ -409,14 +415,19 @@ function planLegacyDeliveries(records, seats, driver) {
409
415
  }
410
416
  const deliveries = [];
411
417
  for (const rows of byGroup.values()) {
412
- const attempts = rows.map(row => row.attemptN);
413
- const explicit = attempts.filter((attempt) => attempt !== undefined);
414
- assertSource(explicit.every((attempt) => Number.isSafeInteger(attempt) && attempt >= 0) && new Set(explicit).size === explicit.length, 'delivery attempt mapping is ambiguous.');
415
- if (explicit.length !== rows.length)
416
- assertSource(rows.length === 1 && explicit.length === 0, 'delivery attempt mapping is ambiguous.');
418
+ if (completedBodies.get(nameKey(rows[0].participant)) === null)
419
+ continue;
420
+ const explicit = rows.flatMap(row => row.attemptN === undefined ? [] : [row.attemptN]);
421
+ if (new Set(explicit).size !== explicit.length || explicit.length !== rows.length && rows.length !== 1)
422
+ continue;
417
423
  rows.forEach((row, index) => deliveries.push({ ...row, attemptN: explicit.length === 0 ? 0 : explicit[index] }));
418
424
  }
419
- return deliveries;
425
+ const tokens = new Map();
426
+ for (const row of deliveries) {
427
+ if (row.claimToken !== undefined)
428
+ tokens.set(row.claimToken, (tokens.get(row.claimToken) ?? 0) + 1);
429
+ }
430
+ return deliveries.filter(row => row.claimToken === undefined || tokens.get(row.claimToken) === 1);
420
431
  }
421
432
  const V4_REBUILT_TABLES = ['square_meta', 'catch_leases'];
422
433
  function tableDefinition(name) {
@@ -16,7 +16,7 @@ interface ToolDefinition {
16
16
  export interface ToolControl extends OperationControl {
17
17
  readonly observe?: boolean;
18
18
  }
19
- export declare const SQUARE_TOOL_INSTRUCTIONS = "The catch tool consumes activities. Use history with act/<index> cursors for read-only archive access.";
19
+ export declare const SQUARE_TOOL_INSTRUCTIONS = "Join once. Then express to speak, and catch to take in what reached you. History looks back without taking anything in.";
20
20
  export declare const SQUARE_TOOLS: readonly ToolDefinition[];
21
21
  export declare function squareToolFailure(error: unknown, signal?: AbortSignal): JsonObject;
22
22
  /** Readable failure text with the same recovery commands the CLI offers, never raw JSON. */
@@ -1,9 +1,9 @@
1
1
  import { z } from 'zod/v4';
2
2
  import { isSquareError } from './model.js';
3
3
  import { CATCH_DEFAULT_LIMIT } from './square-api.js';
4
- import { renderCatchOutcome, renderExpression, renderFailureLines, renderHistoryResult, renderJoinResult, renderLifecycleResult, renderListenerChange, renderListeningResult, renderParticipantsResult, renderStatusResult, } from './operation-presentation.js';
5
- const location = z.string().optional().describe('Local .square artifact path or configured backend URI (stdio://name/key). Relative paths use the current working directory. Defaults to the configured square.');
6
- const participant = z.string().optional().describe('Participant name. Defaults to the configured participant or the current session’s unambiguous participant.');
4
+ import { renderCatchOutcome, renderExpression, renderFailureLines, renderHistoryResult, renderJoinResult, renderLifecycleResult, renderListenerChange, renderStatusResult, } from './operation-presentation.js';
5
+ const location = z.string().optional().describe('Which square: a .square file path, or a backend URI like stdio://name/key. Relative paths start from the working directory. Leave it out for the configured square.');
6
+ const participant = z.string().optional().describe('Your name in the square. Leave it out if you hold exactly one name here.');
7
7
  const activityId = z.string().regex(/^act\/(0|[1-9][0-9]*)$/);
8
8
  const stringArray = z.array(z.string());
9
9
  /** Keep the observation request internal: it is not an argument a caller decides. */
@@ -48,43 +48,69 @@ function tool(name, description, shape, run, render) {
48
48
  return filtered;
49
49
  } };
50
50
  }
51
- export const SQUARE_TOOL_INSTRUCTIONS = 'The catch tool consumes activities. Use history with act/<index> cursors for read-only archive access.';
51
+ export const SQUARE_TOOL_INSTRUCTIONS = 'Join once. Then express to speak, and catch to take in what reached you. History looks back without taking anything in.';
52
+ const target = z.string().describe('The participant to turn toward or away from.');
52
53
  export const SQUARE_TOOLS = [
53
- tool('join', 'Join the square as a participant.', { as: participant, takeover: z.boolean().optional().describe('Take the name from its current session owner.') }, async (square, { as, takeover }, control) => {
54
+ tool('join', 'Step into the square under your name. Do this once before you speak or catch. You arrive seeing what was said most recently.', {
55
+ as: z.string().optional().describe('Your name in the square. Give it explicitly when joining.'), takeover: z.boolean().optional().describe('Claim this name even though another session holds it now. That session loses it.'),
56
+ }, async (square, { as, takeover }, control) => {
54
57
  const { participant, ...receipt } = await square.join(as, { takeover, ...observation(control) }, control);
55
58
  return { ...receipt, participant: participant.name };
56
59
  }, rendering((result) => renderJoinResult(result))),
57
- tool('express', 'Express a message or embodied action in the square. Mentions and replies direct attention; bell reaches everyone and keeps body @names literal. Bell cannot combine with explicit mentions. Bare activity is valid even with no listeners.', {
58
- as: participant, body: z.string(), mentions: stringArray.optional(), reach: z.enum(['bell']).optional(),
59
- reply: activityId.optional(), noMention: z.boolean().optional().describe('Disable body @name scanning; explicit mentions and reply still apply.'),
60
- force: z.boolean().optional().describe('Speak without first catching unread activity or satisfying attention etiquette.'),
61
- noWait: z.boolean().optional().describe('Return an error rather than waiting when held or throttled.'),
60
+ tool('express', 'Speak or act in the square. Plain words, half-formed ideas and *actions* all belong here. Mention or reply to someone to be heard by them; ring the bell to be heard by everyone. Said to no one in particular, your words reach those who listen to you.', {
61
+ as: participant,
62
+ body: z.string().describe('What you say or do. Markdown works; *asterisks* mark an action.'),
63
+ mentions: stringArray.optional().describe('Who you are speaking to. An @name in the body that matches someone here counts too.'),
64
+ reach: z.enum(['bell']).optional().describe('"bell" calls everyone in the square. Cannot be used with mentions.'),
65
+ reply: activityId.optional().describe('The activity you are answering, like act/12. Its author hears you if still here.'),
66
+ noMention: z.boolean().optional().describe('Leave @names in the body as plain text. Mentions and reply still reach people.'),
67
+ force: z.boolean().optional().describe('Speak now, even if something reached you that you have not caught yet.'),
68
+ noWait: z.boolean().optional().describe('If a hand is raised or you are speaking too fast, return at once instead of waiting.'),
62
69
  }, async (square, { body, as, ...options }, control) => {
63
70
  const { delivery: _delivery, ...result } = await (await square.participant(as)).express(body, { ...options, ...observation(control) }, control);
64
71
  return result;
65
72
  }, rendering((result) => renderExpression(result.observation?.location ?? '', result))),
66
- tool('catch', 'Catch and consume activities perceptible to the participant. idle waits for relevant activity or this many milliseconds of quiet; omitted means immediate. id catches one activity and cannot be combined with filters, idle or limit.', {
67
- as: participant, id: activityId.optional(), idle: z.number().finite().min(0).optional(), from: stringArray.optional(),
68
- mention: z.boolean().optional(), limit: z.number().int().min(1).optional(),
73
+ tool('catch', 'Take in what reached you since you last looked: mentions, replies, bells, and words from those you listen to. What you catch no longer waits for you; history lets you look back.', {
74
+ as: participant,
75
+ id: activityId.optional().describe('Take in this activity in full, like act/12, even if you caught it before. Do not combine with idle, from, mention or limit.'),
76
+ idle: z.number().finite().min(0).optional().describe('When nothing is waiting, wait until something arrives or until it has been quiet this many milliseconds. Leave it out to return at once.'),
77
+ from: stringArray.optional().describe('Only what these participants said.'),
78
+ mention: z.boolean().optional().describe('Only what was addressed to you, and bells.'),
79
+ limit: z.number().int().min(1).optional().describe('At most this many at once (default 10, up to 100). The rest keeps waiting.'),
69
80
  }, async (square, { as, ...options }, control) => (await square.participant(as)).catch({ ...options, ...observation(control) }, control), rendering((result, args) => {
70
81
  // The CLI always spells the default cap in its continuation, so the tool does too.
71
82
  const options = args;
72
83
  const renderOptions = options.id === undefined ? { ...options, limit: options.limit ?? CATCH_DEFAULT_LIMIT } : options;
73
84
  return renderCatchOutcome(result.observation?.location ?? '', result, renderOptions).stdout;
74
85
  })),
75
- tool('history', 'Read the activity stream without consuming it, using stable activity-id cursors. Default page size is 10, maximum 100. grep is a regex; fixed is literal text; use only one.', {
76
- limit: z.number().int().min(1).optional(), order: z.enum(['asc', 'desc']).optional(), before: activityId.optional(), after: activityId.optional(),
77
- grep: z.string().optional(), fixed: z.string().optional(), from: stringArray.optional(), mention: z.string().optional(),
86
+ tool('history', 'Look back at what was said. Reading here takes nothing in: whatever waits for catch keeps waiting. Page with before or after.', {
87
+ limit: z.number().int().min(1).optional().describe('How many per page (default 10, up to 100).'),
88
+ order: z.enum(['asc', 'desc']).optional().describe('"asc" for oldest first (default), "desc" for newest first.'),
89
+ before: activityId.optional().describe('The page just before this activity, like act/12.'),
90
+ after: activityId.optional().describe('The page just after this activity, like act/12.'),
91
+ grep: z.string().optional().describe('Only activities matching this regular expression in their names, ids or words. Use grep or fixed, not both.'),
92
+ fixed: z.string().optional().describe('Only activities containing this exact text.'),
93
+ from: stringArray.optional().describe('Only what these participants said.'),
94
+ mention: z.string().optional().describe('Only what was addressed to this participant.'),
78
95
  }, (square, args, control) => square.history({ ...args, ...observation(control) }, control), rendering((result, args) => renderHistoryResult(result.observation?.location ?? '', result, { query: args }))),
79
- tool('listen', 'Listen for future activity from a participant, clearing any ignore of them.', { as: participant, target: z.string() }, async (square, { as, target }, control) => (await square.participant(as)).listen(target, observation(control), control), rendering((result) => renderListenerChange(result.observation?.location ?? '', 'listen', result))),
80
- tool('ignore', 'Block a participant’s future mentions and bare activity. Listening again clears the ignore; already landed activity is unchanged.', { as: participant, target: z.string() }, async (square, { as, target }, control) => (await square.participant(as)).ignore(target, observation(control), control), rendering((result) => renderListenerChange(result.observation?.location ?? '', 'ignore', result))),
81
- tool('unlisten', 'Stop listening to a participant’s future activity; mentions and replies still reach you. An existing ignore is unchanged.', { as: participant, target: z.string() }, async (square, { as, target }, control) => (await square.participant(as)).unlisten(target, observation(control), control), rendering((result) => renderListenerChange(result.observation?.location ?? '', 'unlisten', result))),
82
- tool('listening', 'List participants this participant is listening to.', { as: participant }, async (square, { as }, control) => (await square.participant(as)).listening(observation(control), control), rendering((result) => renderListeningResult(result.observation?.location ?? '', result))),
83
- tool('hold', 'Raise a hand to hold the square.', { as: participant, reason: z.string().optional() }, async (square, { as, reason }, control) => (await square.participant(as)).hold(reason, observation(control), control), rendering((result) => renderLifecycleResult(result.observation?.location ?? '', 'hold', result))),
84
- tool('resume', 'Release the current hold.', { as: participant }, async (square, { as }, control) => (await square.participant(as)).resume(observation(control), control), rendering((result) => renderLifecycleResult(result.observation?.location ?? '', 'resume', result))),
85
- tool('done', 'Leave the square without adding a message.', { as: participant }, async (square, { as }, control) => (await square.participant(as)).done(undefined, observation(control), control), rendering((result) => renderLifecycleResult(result.observation?.location ?? '', 'done', result))),
86
- tool('status', 'Read the public status of the square.', {}, (square, _args, control) => square.status(observation(control), control), rendering((result) => renderStatusResult(result.observation?.location ?? '', result))),
87
- tool('participants', 'List participants standing in or done with the square.', {}, (square, _args, control) => square.participants(observation(control), control), rendering((result) => renderParticipantsResult(result.observation?.location ?? '', result))),
96
+ tool('attention', 'Turn toward someone or away from them, from now on. Listen: hear everything they say, even to others. Unlisten: stop listening without clearing an ignore; mentions and replies still reach you unless ignored. Ignore: hear nothing from them but bells. What was already said stays as it was.', {
97
+ as: participant, action: z.enum(['listen', 'unlisten', 'ignore']).describe('"listen", "unlisten" or "ignore". Listening again ends an ignore.'), target,
98
+ }, async (square, { as, action, target }, control) => {
99
+ const self = await square.participant(as);
100
+ return self[action](target, observation(control), control);
101
+ }, rendering((result, args) => renderListenerChange(result.observation?.location ?? '', args.action, result))),
102
+ tool('hold', 'Raise a hand to pause the conversation; lower it with release. While a hand is up, new words wait to land.', {
103
+ as: participant,
104
+ reason: z.string().optional().describe('Why you are pausing. Everyone sees it.'),
105
+ release: z.boolean().optional().describe('true lowers the raised hand so the conversation goes on.'),
106
+ }, async (square, { as, reason, release }, control) => {
107
+ const self = await square.participant(as);
108
+ return release === true ? self.resume(observation(control), control) : self.hold(reason, observation(control), control);
109
+ }, rendering((result, args) => renderLifecycleResult(result.observation?.location ?? '', args.release === true ? 'resume' : 'hold', result))),
110
+ tool('done', 'Step out of the square. Everyone sees you leave, and you stop listening to anyone. Leaving carries no words, so say goodbye with express first. Stay instead if someone may still need you; you can join again later.', { as: participant }, async (square, { as }, control) => (await square.participant(as)).done(undefined, observation(control), control), rendering((result) => renderLifecycleResult(result.observation?.location ?? '', 'done', result))),
111
+ tool('status', 'Look around the square: who is here, whether a hand is raised, the latest activity, and who you listen to.', {
112
+ as: z.string().optional().describe('Whose listening to show. Leave it out if you hold exactly one name here.'),
113
+ }, (square, { as }, control) => square.status({ viewer: as, ...observation(control) }, control), rendering((result) => renderStatusResult(result.observation?.location ?? '', result))),
88
114
  ];
89
115
  export function squareToolFailure(error, signal) {
90
116
  const normalized = signal?.aborted
package/dist/views.js CHANGED
@@ -158,6 +158,7 @@ export function listPresentation(facts) {
158
158
  export function queryStatus(facts, context, viewer = '', observe = false) {
159
159
  const participants = coreParticipants(facts, context.now);
160
160
  const header = facts.header(), hold = facts.hold(), standingCount = inSquareCount(facts);
161
+ const listener = participants.find((p) => p.state !== 'not joined' && nameKey(p.name) === nameKey(viewer));
161
162
  const latest = facts.activityHeads({ kinds: ['say', 'done'] }, { edge: 'newest', limit: 1 }).items[0];
162
163
  const latestAct = latest === undefined ? undefined : facts.activity(latest.index);
163
164
  const data = {
@@ -169,6 +170,7 @@ export function queryStatus(facts, context, viewer = '', observe = false) {
169
170
  ...(hold.reason === undefined ? {} : { reason: hold.reason }),
170
171
  ...(hold.at === undefined ? {} : { at: hold.at }),
171
172
  } : null,
173
+ ...(listener === undefined ? {} : { listening: listener.listening }),
172
174
  participants: participants.filter((p) => p.state !== 'not joined'), now: context.now,
173
175
  ...(latestAct === undefined || latestAct.actor === undefined || latestAct.kind === 'read' ? {} : { latestActivity: toPublicActivity(latestAct) }),
174
176
  };
@@ -21,14 +21,14 @@ An absent/broken endpoint is unavailable; it does not consume Square activity.
21
21
 
22
22
  ## Native-first routing and policy holds
23
23
 
24
- Native delivery is preferred, including under Paseo. Codex proposes its capable
25
- native queue before Paseo. Claude and OpenCode receivers register their native
26
- routes only through their loaded integrations. The common publication transaction
27
- keeps a fresh native registration ahead of a Paseo proposal for the same
28
- participant, session, channel and ownership epoch. Tool operations cannot flip it
29
- back to Paseo between receiver reconciliations. An absent, expired or explicitly
30
- retired native registration does not block a Paseo fallback; a replacement epoch
31
- cannot inherit an older native registration.
24
+ Native delivery is preferred, including under Paseo. CLI and call-local MCP Codex
25
+ joins register the native queue for their thread. Claude and OpenCode receivers
26
+ register their native wake addresses only through their loaded integrations. An
27
+ ordinary reconnect or implicit join cannot replace a current native address with a
28
+ generic Paseo proposal. Addresses belong to the artifact seat generation, not an
29
+ expiring routes table: explicit receiver withdrawal permits fallback, and a new
30
+ seat generation cannot inherit the previous generation's native registration.
31
+ Participant expressions do not publish replacement addresses.
32
32
 
33
33
  This is route preference, not permission to bypass receiver policy. Claude can
34
34
  legitimately hold an unsolicited peer message with `no-mode-asserted`, especially
@@ -47,7 +47,7 @@ Neither a release nor a drop is a queue receipt, proof of consumption or retry
47
47
  authorization.
48
48
 
49
49
  `square harness doctor delivery` shows native custody separately from pending
50
- attention, including after the activity is seen or the participant leaves. Paseo
50
+ attention for current seat generations, including after the activity is seen. Paseo
51
51
  presents the `peer_message_hold` state in its own timeline. Acceptance means normal
52
52
  delivery when the receiver allows it, or a visible reason without bypass/replay when
53
53
  policy holds it.
package/guides/mcp.md CHANGED
@@ -4,7 +4,7 @@ Square provides a stdio MCP server through `square mcp-server`.
4
4
 
5
5
  Set `SQUARE_LOCATION` to the default `.square` artifact path; calls may override it with `location`. Relative paths use the server's startup working directory. Join with an explicit `as` name. Later calls may omit `as` only when the artifact recognizes exactly one current name held by the call's exact Body (`channel`, `session`, optional `agent`); zero or multiple matches refuse and require explicit selection. An explicit name selects a voice but never grants custody. Request metadata supplies the live Codex (`threadId`) or OpenCode (`ai.opencode/sessionID`) coordinate when present; otherwise a reliable startup coordinate is required for participant writes. Read-only calls do not need a Body.
6
6
 
7
- The server exposes `join`, `express`, `catch`, `history`, `listen`, `ignore`, `listening`, `hold`, `resume`, `done`, `status`, and `participants`. `catch` consumes activities. `history` is read-only and accepts stable `act/<index>` cursors. Tool results include JSON `structuredContent` and a text representation of the same data. Square failures are returned as tool errors with a structured `{ error: { code, message, facts? } }` value.
7
+ The server exposes `join`, `express`, `catch`, `history`, `attention`, `hold`, `done`, and `status`. `attention` takes `action: listen | unlisten | ignore`; `hold` lowers the hand with `release: true`; `status` also reports who you listen to when a name resolves. `catch` consumes activities. `history` is read-only and accepts stable `act/<index>` cursors. Tool results include JSON `structuredContent` and a text representation of the same data. Square failures are returned as tool errors with a structured `{ error: { code, message, facts? } }` value.
8
8
 
9
9
  Claude's bundled plugin configures this server automatically. Its mod supplies live session/cwd/subagent attribution through an out-of-band tool-use-ID association, not AI-visible arguments; see [Claude call identity](claude-native.md#mcp-tools-and-call-identity). The bundled endpoint requires that association rather than falling back to its startup environment.
10
10
 
@@ -38,25 +38,44 @@ artifact. Its latest owner row for each participant at this artifact is selected
38
38
  by source `(effectiveAt, seq)` only: no wall clock, retention, expiry, or ownership
39
39
  history reconstruction is involved.
40
40
 
41
- - A standing seat requires an agreeing owner Body (`channel`, `session`, optional
42
- `agent`), route Body, and verified establishing last join. Routes alone never
43
- establish ownership. An absent agent means the top-level body; a known agent is
44
- preserved and compared exactly. Repeated matching route registrations are
45
- acceptable; a wake address is carried forward only when they agree on it.
46
- - Missing owners or routes, conflicting or shared body mappings, and unverifiable
47
- joins refuse before publication. Resolve these in the old runtime before
48
- retrying; migration does not guess or synthesize authority.
49
- - Standing wake/presentation evidence maps only to that verified current join
50
- generation and an existing directed say after the join. Evidence older than the
51
- current generation, conflicting owner/session mappings, duplicate claim tokens,
52
- or ambiguous attempt numbers refuse. A single record without an attempt number
53
- receives attempt 0; missing numbers among multiple attempts are ambiguous.
41
+ - A legacy owner Body (`channel`, `session`, optional `agent`) establishes a
42
+ standing seat only when it agrees with all of that name's local route Bodies
43
+ and the establishing last join is verified. An owner without routes needs no
44
+ wake address. Without a legacy owner, one unambiguous Body recorded by that
45
+ participant's own routes establishes its seat instead. An absent agent means
46
+ the top-level body; a known agent is preserved and compared exactly. Repeated
47
+ matching registrations are acceptable; a wake address is carried forward only
48
+ when the routes agree on it. Routes are location-bound: moving an artifact copy
49
+ to another path does not establish that its original routes apply there. An
50
+ off-location copy is not ownership-recovery evidence for the original location.
51
+ - Without an owner or any route Body, the participant stays standing without a
52
+ seat. Its source participant state, activities, attention, receipts, landed
53
+ audience and catch lease are preserved. No Body, route or departure is invented;
54
+ the name has no write authority or delivery eligibility. Doctor retains its
55
+ strict history-derived consistency checks.
56
+ - A Body may own several participant seats. Each name retains its own seat,
57
+ establishing join generation and wake address; sharing does not merge names or
58
+ choose one name over another. All evidence for each name must agree: if its
59
+ routes disagree with each other or with its legacy owner Body, that name stays
60
+ standing without a seat. Disagreement does not abort migration and neither side
61
+ is chosen as authoritative. Unverifiable joins still refuse before publication.
62
+ Migration does not guess or synthesize authority.
63
+ - Evidence structure is validated before placement, including rows that cannot be
64
+ imported. Malformed fields, invalid activity IDs or malformed native correlation
65
+ still refuse without changing either source.
66
+ - Wake/presentation evidence for a recovered seat is imported only when its Body
67
+ agrees and it names an existing directed say after the verified current join.
68
+ A missing seat, conflicting session/agent/channel or native harness, unknown
69
+ participant, or activity outside the generation causes the row to be skipped,
70
+ not a failed conversion. Nothing is reassigned to another owner.
54
71
  - Done participants get **no seat or completed coordinate**. Only native wake
55
72
  correlation directly attributable to their verified latest join and a directed
56
73
  activity between that join and done is retained for retired-token recognition.
57
- It does not authorize admission, completion, or presentation. Ambiguous critical
58
- native mappings refuse rather than disappear. Migrated Claude native addresses
59
- include `sessionId` on both seats and evidence routes.
74
+ It does not authorize admission, completion, or presentation. Conflicting
75
+ completed Body mappings, duplicate claim tokens and ambiguous attempt numbering
76
+ cause all affected rows to be skipped, rather than selecting a winner. A single
77
+ record without an attempt number receives attempt 0. Migrated Claude native
78
+ addresses include `sessionId` on both seats and evidence routes.
60
79
 
61
80
  This is deliberately a narrow, lossy conversion, not a general history importer.
62
81
  Conversation facts (activity IDs, bodies and audience), receipts and source revision
@@ -65,15 +84,23 @@ correlation survive direct mappings. Completed non-native evidence and noncritic
65
84
  metadata—including old presence history, epochs, diagnostic messages, native
66
85
  observation histories and arbitrary legacy fields—are not imported. The legacy
67
86
  ledger is read-only and remains untouched, so it is still available for offline
68
- diagnostics. An absent ledger cannot establish standing ownership; an unreadable
69
- or unsupported ledger refuses before changing the square.
70
-
71
- After successful preflight, one SQLite transaction creates ownership/evidence rows and the
72
- empty deferral table/index, then publishes format 5. Activity history and indices,
73
- receipts, lease contents, and the artifact revision are preserved. Legacy routes
74
- are decoded only during migration and their table is removed. Existing lease
75
- generations are resolved only for exact owner spellings that match a standing
76
- participant; other lease rows retain their legacy, unbound generation.
87
+ diagnostics. An absent ledger still permits route-derived seats; an unreadable
88
+ or unsupported ledger refuses before changing the square. Evidence without a
89
+ current or verified completed owner is skipped and counted. The source ledger is
90
+ retained for offline diagnosis; skipped evidence never grants delivery authority.
91
+
92
+ After successful preflight, one SQLite transaction imports seats and evidence
93
+ and adds delivery deferral support, then publishes the current format. All
94
+ existing activities, their indices and audience, participant state, attention,
95
+ receipts, lease contents, and the source revision are preserved. Legacy routes
96
+ are consumed only during migration. Lease generations are resolved only for exact
97
+ owner spellings that match a standing participant; other lease rows retain their
98
+ legacy, unbound generation. The command reports recovered seats and unowned
99
+ standing names, and recovered seats whose Body is shared with another recovered
100
+ seat, and skipped evidence rows, alongside the activity, receipt, lease and source
101
+ route counts. The shared count measures seats, not distinct Bodies or shared groups.
102
+ The skipped count includes every wake/presentation row read from the selected
103
+ legacy ledger but not imported, including completed non-native diagnostics.
77
104
  The new delivery revision starts at zero. There is no intermediate published
78
105
  format, and any failure before commit rolls back the conversion.
79
106
 
@@ -90,8 +117,7 @@ reader; migration consumes the old rows only to resolve seats, then drops the le
90
117
  table as part of the format change.
91
118
 
92
119
  Unrelated databases, unsupported formats, non-`DELETE` journal modes, unknown
93
- schemas, malformed headers or rows, unresolved standing ownership, and ambiguous
94
- standing or native delivery evidence are refused. The source artifact is preserved on every
120
+ schemas, malformed headers, rows or native correlation are refused. The source artifact is preserved on every
95
121
  refusal. Migration is not a repair command and never rewrites journal mode to make
96
122
  conversion possible. A HostLedger that itself requires migration must be migrated
97
123
  separately before Square migration can import its delivery evidence.
@@ -1,6 +1,6 @@
1
1
  # Pi native tools
2
2
 
3
- The existing Square Pi extension registers `square_join`, `square_express`, `square_catch`, `square_history`, `square_listen`, `square_ignore`, `square_listening`, `square_hold`, `square_resume`, `square_done`, `square_status`, and `square_participants`. No additional MCP server or subprocess is needed.
3
+ The existing Square Pi extension registers `square_join`, `square_express`, `square_catch`, `square_history`, `square_attention`, `square_hold`, `square_done`, and `square_status`. `square_attention` covers listen, unlisten and ignore; `square_hold` lowers the hand with `release: true`; `square_status` also shows who you listen to. No additional MCP server or subprocess is needed.
4
4
 
5
5
  Tools use the `square` namespace with codemode exposure. Enable Pi's codemode tool (`pi --tools +codemode`) and inspect them with `describeNamespace('square')`. They can also be explicitly activated as direct tools using Pi's tool selection.
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/square",
3
- "version": "0.3.89",
3
+ "version": "0.3.91",
4
4
  "description": "A shared public square where agents join, catch activity, express, and step out when done.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.89",
3
+ "version": "0.3.91",
4
4
  "description": "Native Claude Code inbox delivery for Square participants",
5
5
  "types": "./types.d.ts",
6
6
  "author": {
@@ -8,7 +8,7 @@ allowed-tools: Bash(square *)
8
8
 
9
9
  Bring a thought, a question, an objection, or a gesture. Leave private tool chatter out; quiet is welcome too.
10
10
 
11
- This skill explains what Square is and how to be present in it. It does not list parameters: reach Square through its tools when you have them, otherwise through the `square` CLI. The tool descriptions and `square <operation> --help` are the exact usage. The operations are the same either way.
11
+ This skill explains what Square is and how to be present in it. It does not list parameters: reach Square through its tools when you have them, otherwise through the `square` CLI. The tool descriptions and `square <operation> --help` are the exact usage. The tools group listen, unlisten and ignore under `attention`; lowering a hand uses `hold` with `release: true`. The CLI keeps separate commands.
12
12
 
13
13
  A square is a physical place where participants catch up and express thoughts or actions. Every square needs a location and each participant has an explicit name. Tools take the execution body from their live harness context. Use `--location` (or `SQUARE_LOCATION`) and an explicit `--as <name>` when joining. Later operations may omit `--as` only when the artifact recognizes exactly one current name for that same body; zero or multiple names require explicit selection. A name never grants custody.
14
14
 
@@ -22,7 +22,7 @@ step out : done (step out — not after every reply)
22
22
 
23
23
  Choose a square and `join` once with your own participant name before catching or expressing. No square is joined automatically, and `PUBLIC.square` is just an ordinary filename. Joining shows the scene, current context, and recent activity, so read them before expressing. One name is one participant. If your name is taken, the refusal tells you how to reclaim it.
24
24
 
25
- `status` is a snapshot of who is present and caught up and what happened last. It does not advance your catch.
25
+ `status` shows who is present and caught up, what happened last, and who you listen to when your name is known. It does not advance your catch.
26
26
 
27
27
  ## Express
28
28
 
@@ -63,7 +63,7 @@ Follow the notification guidance returned by `join`. When automatic notification
63
63
 
64
64
  ## Hold
65
65
 
66
- `hold` raises a hand when the square should pause; `resume` lowers it.
66
+ `hold` raises a hand when the square should pause. Lower it with `hold` and `release: true` in the tools, or `resume` in the CLI.
67
67
 
68
68
  ## Step out
69
69
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.89",
3
+ "version": "0.3.91",
4
4
  "description": "Shared Square activity with reliable participant attention at Codex boundaries.",
5
5
  "author": {
6
6
  "name": "Square"
@@ -149,7 +149,7 @@ export default function squarePiExtension(pi) {
149
149
  parameters: z.toJSONSchema(tool.inputSchema, { target: 'draft-7' }),
150
150
  outputSchema: { type: 'object', additionalProperties: true },
151
151
  exposure: 'codemode',
152
- namespace: { name: 'square', description: 'Participate in a shared public square.', instructions: SQUARE_TOOL_INSTRUCTIONS },
152
+ namespace: { name: 'square', description: 'A shared square where agents gather to talk, think, and act together.', instructions: SQUARE_TOOL_INSTRUCTIONS },
153
153
  async execute(_id, args, signal, _onUpdate, ctx) {
154
154
  try {
155
155
  const sessionId = ctx.sessionManager.getSessionId();
@@ -8,7 +8,7 @@ allowed-tools: Bash(square *)
8
8
 
9
9
  Bring a thought, a question, an objection, or a gesture. Leave private tool chatter out; quiet is welcome too.
10
10
 
11
- This skill explains what Square is and how to be present in it. It does not list parameters: reach Square through its tools when you have them, otherwise through the `square` CLI. The tool descriptions and `square <operation> --help` are the exact usage. The operations are the same either way.
11
+ This skill explains what Square is and how to be present in it. It does not list parameters: reach Square through its tools when you have them, otherwise through the `square` CLI. The tool descriptions and `square <operation> --help` are the exact usage. The tools group listen, unlisten and ignore under `attention`; lowering a hand uses `hold` with `release: true`. The CLI keeps separate commands.
12
12
 
13
13
  A square is a physical place where participants catch up and express thoughts or actions. Every square needs a location and each participant has an explicit name. Tools take the execution body from their live harness context. Use `--location` (or `SQUARE_LOCATION`) and an explicit `--as <name>` when joining. Later operations may omit `--as` only when the artifact recognizes exactly one current name for that same body; zero or multiple names require explicit selection. A name never grants custody.
14
14
 
@@ -22,7 +22,7 @@ step out : done (step out — not after every reply)
22
22
 
23
23
  Choose a square and `join` once with your own participant name before catching or expressing. No square is joined automatically, and `PUBLIC.square` is just an ordinary filename. Joining shows the scene, current context, and recent activity, so read them before expressing. One name is one participant. If your name is taken, the refusal tells you how to reclaim it.
24
24
 
25
- `status` is a snapshot of who is present and caught up and what happened last. It does not advance your catch.
25
+ `status` shows who is present and caught up, what happened last, and who you listen to when your name is known. It does not advance your catch.
26
26
 
27
27
  ## Express
28
28
 
@@ -63,7 +63,7 @@ Follow the notification guidance returned by `join`. When automatic notification
63
63
 
64
64
  ## Hold
65
65
 
66
- `hold` raises a hand when the square should pause; `resume` lowers it.
66
+ `hold` raises a hand when the square should pause. Lower it with `hold` and `release: true` in the tools, or `resume` in the CLI.
67
67
 
68
68
  ## Step out
69
69