@astrosheep/square 0.3.58 → 0.3.62

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.
Files changed (91) hide show
  1. package/claude-plugin/.claude-plugin/plugin.json +1 -1
  2. package/claude-plugin/skills/square/SKILL.md +5 -7
  3. package/codex-plugin/.codex-plugin/plugin.json +1 -1
  4. package/dist/activity.d.ts +6 -2
  5. package/dist/activity.js +170 -133
  6. package/dist/artifact.d.ts +4 -0
  7. package/dist/artifact.js +43 -14
  8. package/dist/attention-presentation.d.ts +1 -0
  9. package/dist/attention-presentation.js +21 -19
  10. package/dist/automatic-session.js +16 -26
  11. package/dist/boundary-presentation.d.ts +1 -2
  12. package/dist/boundary-presentation.js +4 -19
  13. package/dist/catch-decisions.js +1 -1
  14. package/dist/cli/context.d.ts +2 -0
  15. package/dist/cli/context.js +17 -33
  16. package/dist/cli/harness-command.js +1 -1
  17. package/dist/cli/mcp-server-command.d.ts +2 -0
  18. package/dist/cli/mcp-server-command.js +13 -0
  19. package/dist/cli/observation-commands.js +132 -64
  20. package/dist/cli/program.js +36 -9
  21. package/dist/cli/registry.js +2 -0
  22. package/dist/cli/square-commands.d.ts +1 -1
  23. package/dist/cli/square-commands.js +103 -133
  24. package/dist/decisions.d.ts +4 -0
  25. package/dist/decisions.js +82 -34
  26. package/dist/delivery-operations.d.ts +1 -2
  27. package/dist/delivery-operations.js +1 -4
  28. package/dist/file-changes.d.ts +12 -0
  29. package/dist/file-changes.js +235 -0
  30. package/dist/help.js +4 -8
  31. package/dist/host-ledger-file-adapter.d.ts +15 -13
  32. package/dist/host-ledger-file-adapter.js +131 -79
  33. package/dist/host-ledger-root.d.ts +2 -0
  34. package/dist/host-ledger-root.js +9 -0
  35. package/dist/host-ledger.d.ts +4 -24
  36. package/dist/inbox.d.ts +10 -5
  37. package/dist/inbox.js +152 -67
  38. package/dist/index.d.ts +3 -1
  39. package/dist/index.js +1 -0
  40. package/dist/list.js +12 -9
  41. package/dist/mcp.d.ts +7 -0
  42. package/dist/mcp.js +122 -0
  43. package/dist/mention-parse.d.ts +8 -0
  44. package/dist/mention-parse.js +107 -0
  45. package/dist/model.d.ts +5 -0
  46. package/dist/model.js +4 -1
  47. package/dist/notifications.d.ts +1 -0
  48. package/dist/notifications.js +25 -20
  49. package/dist/participant-host.js +2 -2
  50. package/dist/ports.d.ts +2 -9
  51. package/dist/presentation.d.ts +19 -1
  52. package/dist/presentation.js +111 -73
  53. package/dist/presented.js +2 -1
  54. package/dist/registry.d.ts +8 -4
  55. package/dist/registry.js +43 -76
  56. package/dist/routes.d.ts +19 -2
  57. package/dist/routes.js +48 -5
  58. package/dist/runtime.d.ts +10 -0
  59. package/dist/runtime.js +39 -0
  60. package/dist/square-actions.d.ts +26 -13
  61. package/dist/square-actions.js +203 -87
  62. package/dist/square-application.d.ts +58 -0
  63. package/dist/square-application.js +183 -0
  64. package/dist/square-facade.d.ts +24 -9
  65. package/dist/square-file-adapter.d.ts +5 -12
  66. package/dist/square-file-adapter.js +12 -64
  67. package/dist/square-projections.d.ts +3 -2
  68. package/dist/square-projections.js +12 -3
  69. package/dist/square-storage.js +27 -34
  70. package/dist/square-wiring.d.ts +12 -7
  71. package/dist/square-wiring.js +36 -32
  72. package/dist/state-cell.d.ts +1 -1
  73. package/dist/stream.js +31 -27
  74. package/dist/tty-style.d.ts +7 -0
  75. package/dist/tty-style.js +13 -0
  76. package/dist/views.d.ts +3 -3
  77. package/dist/views.js +10 -9
  78. package/dist/wake-attempts.js +2 -1
  79. package/dist/wake-evidence.js +2 -8
  80. package/dist/wakes.d.ts +1 -0
  81. package/dist/wakes.js +5 -5
  82. package/dist/watch.d.ts +6 -1
  83. package/dist/watch.js +43 -28
  84. package/extensions/square-pi.js +107 -109
  85. package/guides/application-boundary.md +32 -0
  86. package/guides/change-waits.md +28 -0
  87. package/guides/host-ledger.md +9 -0
  88. package/guides/mcp.md +9 -0
  89. package/guides/operation-cancellation.md +17 -0
  90. package/package.json +10 -4
  91. package/skills/square/SKILL.md +5 -7
@@ -6,15 +6,20 @@ import { cmdListSquares } from '../list.js';
6
6
  import { nameKey, parseActivityId, sameName } from '../model.js';
7
7
  import { commandPrefix, participantIdentity, renderGrepActivitiesView, renderEventCli, renderAmbientEvent, renderPresenceAnchor, truncateChars, withPathOutput, quoteShell, } from '../presentation.js';
8
8
  import { actId, nowMs } from '../runtime.js';
9
+ import { style } from '../tty-style.js';
9
10
  import { cmdStream, cmdStreamNdjson } from '../stream.js';
10
11
  import { formatRelativeTime, formatTimestamp, parseTimeOrRelative } from '../time.js';
11
12
  import { cmdWatch } from '../watch.js';
12
13
  import { openSquare } from '../square-file-adapter.js';
13
14
  import { closeOpenSquare } from '../open-square.js';
14
- import { historyPresentation, participantsPresentation, statusPresentation } from '../views.js';
15
+ import { createSquareApplication } from '../square-application.js';
16
+ import { historyPresentation } from '../views.js';
17
+ import { hostLedgerForEnv } from '../registry.js';
15
18
  import { fail, parseBoundedLimit, parseDurationMs, parseNameList, parseNonNegativeInteger, readStdin, requireParticipant, requireSquarePath, requireValue, usage, } from './context.js';
16
19
  import { CATCH_DEFAULT_LIMIT, CATCH_MAX_LIMIT } from '../catch-decisions.js';
17
20
  const STATUS_PARTICIPANT_PREVIEW_LIMIT = 10;
21
+ /** A participant counts as here when their last sign of life sits inside this window. */
22
+ const STATUS_HERE_WINDOW_MS = 30 * 60_000;
18
23
  const HISTORY_DEFAULT_LIMIT = 10;
19
24
  const HISTORY_MAX_LIMIT = 100;
20
25
  const PARTICIPANTS_DEFAULT_LIMIT = 20;
@@ -94,10 +99,10 @@ export const catchCommand = {
94
99
  for (let index = 0; index < argv.length; index++) {
95
100
  if (argv[index] === '--id') {
96
101
  if (id !== undefined)
97
- fail('✕ catch accepts one --id\n» square catch --help');
102
+ fail('✕ catch accepts one --id\nsquare catch --help');
98
103
  const value = requireValue(argv, index, '--id');
99
104
  if (parseActivityId(value) === undefined)
100
- fail('✕ invalid --id: expected an activity id like act/12\n» square catch --help');
105
+ fail('✕ invalid --id: expected an activity id like act/12\nsquare catch --help');
101
106
  id = value;
102
107
  index += 1;
103
108
  }
@@ -121,11 +126,11 @@ export const catchCommand = {
121
126
  else if (argv[index] === '--now')
122
127
  now = true;
123
128
  else
124
- fail(`✕ catch does not know ${argv[index]}\n» square catch --help`);
129
+ fail(`✕ catch does not know ${argv[index]}\nsquare catch --help`);
125
130
  }
126
131
  if (id !== undefined) {
127
132
  if (argv.some((flag) => ['--idle', '--from', '--mention', '--limit', '--replace'].includes(flag))) {
128
- fail('✕ --id cannot be combined with --idle, --from, --mention, --limit or --replace\n» square catch --help');
133
+ fail('✕ --id cannot be combined with --idle, --from, --mention, --limit or --replace\nsquare catch --help');
129
134
  }
130
135
  return { id, now: true };
131
136
  }
@@ -144,7 +149,7 @@ export const catchCommand = {
144
149
  },
145
150
  async execute(intent, context) {
146
151
  const squarePath = requireSquarePath(context);
147
- const caught = await cmdWatch(squarePath, requireParticipant(context.name), intent);
152
+ const caught = await cmdWatch(squarePath, requireParticipant(context.name), intent, { cwd: context.cwd, env: context.env });
148
153
  if (caught !== false)
149
154
  await sweepPendingNotifications(squarePath);
150
155
  },
@@ -271,7 +276,7 @@ function parseHistory(argv, context) {
271
276
  else if (flag === '--json')
272
277
  json = true;
273
278
  else
274
- fail(`✕ history does not know ${flag}\n» square history --help`);
279
+ fail(`✕ history does not know ${flag}\nsquare history --help`);
275
280
  }
276
281
  if (grep !== undefined && fixed !== undefined)
277
282
  fail('--grep and --fixed cannot be combined.');
@@ -300,6 +305,30 @@ function parseHistory(argv, context) {
300
305
  continuationArgs: historyContinuationArgs(argv),
301
306
  };
302
307
  }
308
+ function applicationHistoryQuery(options) {
309
+ return {
310
+ ...(options.participants === undefined || options.participants.length === 0 ? {} : { from: options.participants }),
311
+ ...(options.mention === undefined ? {} : { mention: options.mention }),
312
+ ...(options.afterIndex === undefined ? {} : { after: actId(options.afterIndex) }),
313
+ ...(options.beforeIndex === undefined ? {} : { before: actId(options.beforeIndex) }),
314
+ ...(options.grep === undefined ? {} : { grep: options.grep }), ...(options.fixed === undefined ? {} : { fixed: options.fixed }),
315
+ order: 'asc',
316
+ };
317
+ }
318
+ function activityAsStored(activity) {
319
+ const index = parseActivityId(activity.id);
320
+ if (index === undefined)
321
+ throw new Error('Application returned a non-canonical activity id');
322
+ switch (activity.kind) {
323
+ case 'say': return { index, kind: 'say', actor: activity.actor, at: activity.at, body: activity.body ?? '', mentions: activity.mentions, ...(activity.reach === undefined ? {} : { reach: activity.reach }), ...(activity.reply === undefined ? {} : { reply: parseActivityId(activity.reply) ?? undefined }) };
324
+ case 'done': return { index, kind: 'done', actor: activity.actor, at: activity.at, body: activity.body ?? '' };
325
+ case 'hold': return { index, kind: 'hold', actor: activity.actor, at: activity.at, ...(activity.body === undefined ? {} : { body: activity.body }) };
326
+ case 'resume': return { index, kind: 'resume', actor: activity.actor, at: activity.at };
327
+ case 'join': return { index, kind: 'join', actor: activity.actor, at: activity.at };
328
+ case 'listen': return { index, kind: 'listen', actor: activity.actor, target: activity.target ?? '', at: activity.at };
329
+ case 'ignore': return { index, kind: 'ignore', actor: activity.actor, target: activity.target ?? '', at: activity.at };
330
+ }
331
+ }
303
332
  function historyContinuationCommand(options, squarePath, direction, index) {
304
333
  const args = [...(options.continuationArgs ?? []), direction, actId(index), '--limit', String(options.lastN ?? HISTORY_DEFAULT_LIMIT)];
305
334
  return `${commandPrefix(squarePath)} history ${args.map((arg) => arg.startsWith('-') || /^act\/\d+$/.test(arg) || /^\d+$/.test(arg) ? arg : quoteShell(arg)).join(' ')}`;
@@ -308,6 +337,10 @@ function boundedHistoryCommand(options, squarePath) {
308
337
  const args = [...(options.continuationArgs ?? []), '--limit', String(HISTORY_MAX_LIMIT)];
309
338
  return `${commandPrefix(squarePath)} history ${args.map((arg) => arg.startsWith('-') || /^act\/\d+$/.test(arg) || /^\d+$/.test(arg) ? arg : quoteShell(arg)).join(' ')}`;
310
339
  }
340
+ /** The archive-visible activity model: content plus the lifecycle moves that keep its story honest — a raised hand pairs with its lowering, and a departure pairs with the return. */
341
+ function isArchiveActivity(item) {
342
+ return item.kind === 'say' || item.kind === 'done' || item.kind === 'hold' || item.kind === 'resume' || item.kind === 'join';
343
+ }
311
344
  function renderFields(sayNumbers, item, fields) {
312
345
  return fields.map((field) => {
313
346
  switch (field) {
@@ -341,7 +374,7 @@ function jsonLine(sayNumbers, item) {
341
374
  });
342
375
  }
343
376
  function renderHistoryProjection(projection, visible, noTruncate, squarePath) {
344
- const shown = visible.filter((activity) => activity.kind === 'say' || activity.kind === 'done');
377
+ const shown = visible.filter(isArchiveActivity);
345
378
  const preview = noTruncate || shown.length <= 1 ? undefined : 200;
346
379
  const chunks = [];
347
380
  for (const activity of shown) {
@@ -359,7 +392,7 @@ function renderHistoryProjection(projection, visible, noTruncate, squarePath) {
359
392
  if (chunks.length === 0)
360
393
  return 'latest\n ○ no public activity in this view';
361
394
  if (preview !== undefined && shown.some((activity) => activity.kind === 'say' && activity.body.length > preview)) {
362
- chunks.push(`» ${commandPrefix(squarePath)} history --no-truncate`);
395
+ chunks.push(`${commandPrefix(squarePath)} history --no-truncate`);
363
396
  }
364
397
  return chunks.join('\n\n');
365
398
  }
@@ -367,14 +400,18 @@ export const historyCommand = {
367
400
  parse(argv, context) { return parseHistory(argv, context); },
368
401
  async execute(options, context) {
369
402
  const squarePath = requireSquarePath(context);
370
- const square = await openSquare(squarePath, { clock: nowMs });
403
+ const square = await openSquare(squarePath, { clock: nowMs, env: context.env, hostLedger: hostLedgerForEnv(context.env) });
371
404
  try {
372
405
  // Keep the projection chronological; pagination chooses a stable edge,
373
406
  // then --order only changes how the selected page is displayed.
374
407
  const projection = await historyPresentation(square, { ...options, order: 'asc' });
375
- let events = [...projection.activities];
408
+ const useApplicationHistory = options.atIndexes === undefined && options.beforeContext === undefined && options.afterContext === undefined && options.after === undefined;
409
+ const applicationHistory = useApplicationHistory
410
+ ? await createSquareApplication({ cwd: context.cwd, env: { ...context.env, SQUARE_PARTICIPANT_NAME: undefined }, squarePath, clock: nowMs }).history(applicationHistoryQuery(options))
411
+ : undefined;
412
+ let events = applicationHistory === undefined ? [...projection.activities] : applicationHistory.map(activityAsStored);
376
413
  if (options.lastN === null && events.length > HISTORY_MAX_LIMIT) {
377
- fail(`✕ history is capped at ${HISTORY_MAX_LIMIT} activities\n» ${boundedHistoryCommand(options, squarePath)}`);
414
+ fail(`✕ history is capped at ${HISTORY_MAX_LIMIT} activities\n${boundedHistoryCommand(options, squarePath)}`);
378
415
  }
379
416
  const searching = options.grep !== undefined || options.fixed !== undefined;
380
417
  const totalMatches = searching ? events.length : 0;
@@ -394,8 +431,8 @@ export const historyCommand = {
394
431
  const output = pattern === undefined || pattern === ''
395
432
  ? renderHistoryProjection(projection, events, options.noTruncate === true, squarePath)
396
433
  : renderGrepActivitiesView(events, totalMatches, options.noTruncate, squarePath, pattern, options.fixed !== undefined, () => 'full');
397
- const publicEvents = events.filter((item) => item.kind === 'say' || item.kind === 'done');
398
- const allPublic = projection.activities.filter((item) => item.kind === 'say' || item.kind === 'done');
434
+ const publicEvents = events.filter(isArchiveActivity);
435
+ const allPublic = projection.activities.filter(isArchiveActivity);
399
436
  const pageMin = publicEvents.length === 0 ? undefined : Math.min(...publicEvents.map((item) => item.index));
400
437
  const pageMax = publicEvents.length === 0 ? undefined : Math.max(...publicEvents.map((item) => item.index));
401
438
  const hasMore = options.lastN != null && publicEvents.length > 0 && (options.afterIndex !== undefined
@@ -403,7 +440,7 @@ export const historyCommand = {
403
440
  : allPublic.some((item) => item.index < (pageMin ?? Infinity)));
404
441
  const cursorDirection = options.afterIndex !== undefined ? '--after' : '--before';
405
442
  const cursorIndex = cursorDirection === '--after' ? Math.max(...publicEvents.map((item) => item.index)) : Math.min(...publicEvents.map((item) => item.index));
406
- const continuation = hasMore ? `\n\n» ${historyContinuationCommand(options, squarePath, cursorDirection, cursorIndex)}` : '';
443
+ const continuation = hasMore ? `\n\n${historyContinuationCommand(options, squarePath, cursorDirection, cursorIndex)}` : '';
407
444
  return withPathOutput(squarePath, output + continuation, { participantCount: projection.participantCount });
408
445
  }
409
446
  finally {
@@ -429,32 +466,30 @@ export const participantsCommand = {
429
466
  parse(argv, context) { return parseParticipants(argv, context); },
430
467
  async execute(intent, context) {
431
468
  const squarePath = requireSquarePath(context);
432
- const square = await openSquare(squarePath, { clock: nowMs });
433
- try {
469
+ const application = createSquareApplication({ cwd: context.cwd, env: context.env, squarePath, clock: nowMs });
470
+ {
434
471
  const now = nowMs();
435
- const participants = await participantsPresentation(square);
472
+ const participants = await application.participants();
436
473
  const lines = participants.slice(0, intent.limit).map((participant) => {
437
- const glyph = participant.state === 'done' ? '○' : participant.presence === 'watching' ? '◎' : participant.activityCount > 0 ? '●' : '○';
474
+ const recent = participant.lastActiveAt !== undefined && now - participant.lastActiveAt <= STATUS_HERE_WINDOW_MS;
475
+ const glyph = participant.state === 'done' ? '○' : participant.presence === 'watching' ? '◎' : participant.activityCount > 0 && recent ? '●' : '○';
438
476
  const state = participant.state === 'done' ? 'done' : participant.presence === 'watching' ? 'catching' : participant.state;
439
477
  const last = participant.lastActiveAt === undefined ? '—' : formatRelativeTime(participant.lastActiveAt, now);
440
- return ` ${glyph} ${participant.name} · ${state} · ${participant.activityCount} ${participant.activityCount === 1 ? 'activity' : 'activities'} · ${last}`;
478
+ return ` ${glyph} ${participant.name}${style('dim', ` · ${state} · ${participant.activityCount} ${participant.activityCount === 1 ? 'activity' : 'activities'} · ${last}`)}`;
441
479
  });
442
480
  const participantCount = participants.filter((participant) => participant.state === 'active').length;
443
481
  const tail = participants.length <= intent.limit
444
482
  ? []
445
483
  : [
446
- ` ○ ${lines.length} of ${participants.length} participants shown`,
484
+ style('dim', ` ○ ${lines.length} of ${participants.length} participants shown`),
447
485
  ...(participants.length <= PARTICIPANTS_MAX_LIMIT ? [
448
- `» ${participantsLimitCommand(squarePath, participants.length)}`,
486
+ `${participantsLimitCommand(squarePath, participants.length)}`,
449
487
  ] : []),
450
488
  ];
451
489
  return withPathOutput(squarePath, ['participants', ...lines, ...tail].join('\n'), {
452
490
  participantCount,
453
491
  });
454
492
  }
455
- finally {
456
- await closeOpenSquare(square);
457
- }
458
493
  },
459
494
  present: (result) => process.stdout.write(result),
460
495
  };
@@ -463,41 +498,60 @@ export const statusCommand = {
463
498
  usage(context.command); return undefined; },
464
499
  async execute(_intent, context) {
465
500
  const squarePath = requireSquarePath(context);
466
- const square = await openSquare(squarePath, { clock: nowMs });
467
- try {
468
- const presentation = await statusPresentation(square);
501
+ const application = createSquareApplication({ cwd: context.cwd, env: context.env, squarePath, clock: nowMs });
502
+ {
503
+ const presentation = await application.status();
469
504
  const result = presentation.status;
470
505
  const active = result.participants.filter((participant) => participant.state === 'active');
471
- const people = active.length === 0 ? [' ○ nobody in the square'] : active.slice(0, STATUS_PARTICIPANT_PREVIEW_LIMIT).map((participant) => {
472
- const glyph = participant.presence === 'watching'
473
- ? '◎'
474
- : participant.activityCount > 0 ? '●' : '○';
475
- const summary = participant.activityCount > 0
476
- ? `${participant.activityCount} ${participant.activityCount === 1 ? 'activity' : 'activities'} · ${participant.lastActiveAt === undefined
477
- ? 'just now'
478
- : formatRelativeTime(participant.lastActiveAt, result.now)}`
479
- : `quiet · ${participant.lastActiveAt === undefined ? '—' : formatRelativeTime(participant.lastActiveAt, result.now)}`;
480
- const showAttention = context.name === undefined || sameName(participant.name, context.name);
481
- const attention = !showAttention
482
- ? ''
483
- : participant.pendingMentionCount > 0
484
- ? `${participant.pendingMentionCount} attention${participant.pendingMentionCount === 1 ? '' : 's'} waiting`
485
- : participant.unreadActivityCount > 0
486
- ? `${participant.unreadActivityCount} change${participant.unreadActivityCount === 1 ? '' : 's'} waiting`
487
- : 'caught up';
488
- return ` ${glyph} ${participantIdentity(participant.name)} · ${summary}${attention === '' ? '' : ` · ${attention}`}`;
489
- });
490
- if (active.length > STATUS_PARTICIPANT_PREVIEW_LIMIT) {
491
- people.push(` ○ … ${active.length - STATUS_PARTICIPANT_PREVIEW_LIMIT} more participants`);
492
- people.push(`» ${result.participants.length <= PARTICIPANTS_MAX_LIMIT
493
- ? participantsLimitCommand(squarePath, result.participants.length)
494
- : `${commandPrefix(squarePath)} participants`}`);
506
+ const here = active.filter((participant) => participant.presence === 'watching'
507
+ || (participant.lastActiveAt !== undefined && result.now - participant.lastActiveAt <= STATUS_HERE_WINDOW_MS));
508
+ const lingering = active.length - here.length;
509
+ const orderedHere = [
510
+ ...here.filter((participant) => participant.presence === 'watching'),
511
+ ...here.filter((participant) => participant.presence !== 'watching'),
512
+ ];
513
+ const people = [];
514
+ if (active.length === 0) {
515
+ people.push(' ○ nobody in the square');
516
+ }
517
+ else {
518
+ if (orderedHere.length === 0)
519
+ people.push(' ○ nobody here right now');
520
+ people.push(...orderedHere.slice(0, STATUS_PARTICIPANT_PREVIEW_LIMIT).map((participant) => {
521
+ const glyph = participant.presence === 'watching'
522
+ ? '◎'
523
+ : participant.activityCount > 0 ? '●' : '○';
524
+ const summary = participant.presence === 'watching'
525
+ ? 'catching'
526
+ : participant.activityCount > 0
527
+ ? formatRelativeTime(participant.lastActiveAt ?? result.now, result.now)
528
+ : `quiet · ${participant.lastActiveAt === undefined ? 'just now' : formatRelativeTime(participant.lastActiveAt, result.now)}`;
529
+ const showAttention = context.name === undefined || sameName(participant.name, context.name);
530
+ const attention = !showAttention
531
+ ? ''
532
+ : participant.pendingMentionCount > 0
533
+ ? `${participant.pendingMentionCount} ${participant.pendingMentionCount === 1 ? 'mention' : 'mentions'} waiting`
534
+ : participant.unreadActivityCount > 0
535
+ ? `${participant.unreadActivityCount} change${participant.unreadActivityCount === 1 ? '' : 's'} waiting`
536
+ : 'caught up';
537
+ return ` ${glyph} ${participantIdentity(participant.name)}${style('dim', ` · ${summary}${attention === '' ? '' : ` · ${attention}`}`)}`;
538
+ }));
539
+ const unshown = Math.max(0, orderedHere.length - STATUS_PARTICIPANT_PREVIEW_LIMIT) + lingering;
540
+ if (orderedHere.length > STATUS_PARTICIPANT_PREVIEW_LIMIT) {
541
+ people.push(` ○ … ${orderedHere.length - STATUS_PARTICIPANT_PREVIEW_LIMIT} more here`);
542
+ }
543
+ if (lingering > 0)
544
+ people.push(` ○ … ${lingering} lingering`);
545
+ if (unshown > 0) {
546
+ people.push(`${result.participants.length <= PARTICIPANTS_MAX_LIMIT
547
+ ? participantsLimitCommand(squarePath, result.participants.length)
548
+ : `${commandPrefix(squarePath)} participants`}`);
549
+ }
495
550
  }
496
- const cap = result.hardCap === null ? 'unlimited' : String(result.hardCap);
551
+ const cap = result.hardCap === null ? 'unlimited' : `${result.hardCap} each`;
552
+ const holdTime = result.holdAt === undefined ? 'just now' : formatRelativeTime(result.holdAt, result.now);
497
553
  const hold = result.holdActive
498
- ? `· ${result.holdActor === undefined ? 'someone' : participantIdentity(result.holdActor)} raised a hand${result.holdReason ? ` — ${result.holdReason}` : ''} · ${result.holdAt === undefined
499
- ? 'just now'
500
- : formatRelativeTime(result.holdAt, result.now)}`
554
+ ? `· ${result.holdActor === undefined ? 'someone' : participantIdentity(result.holdActor)} raised a hand${result.holdReason ? ` — ${result.holdReason}` : ''}${style('dim', ` · ${holdTime}`)}`
501
555
  : undefined;
502
556
  const visible = result.latestAct === undefined
503
557
  ? ''
@@ -510,20 +564,34 @@ export const statusCommand = {
510
564
  const latest = visible === ''
511
565
  ? [result.latestAct === undefined
512
566
  ? ' ○ no public activity yet'
513
- : ' · latest activity is private to another participant']
567
+ : ' · the latest words were meant for other ears']
514
568
  : [` ${visible.replace(/\n/g, '\n ')}`];
569
+ // Presence events after the latest public act: otherwise the roster can say someone is
570
+ // here while the latest line shows them leaving, with nothing to reconcile the two.
571
+ const latestIndex = result.latestAct?.index ?? -1;
572
+ const stepsIn = presentation.state.acts.filter((act) => act.kind === 'join' && act.index > latestIndex);
573
+ for (const act of stepsIn.slice(-3)) {
574
+ const rejoined = presentation.state.acts.some((other) => other.kind === 'done' && other.actor === act.actor && other.index < act.index);
575
+ latest.push(style('dim', ` → ${participantIdentity(act.actor)} ${rejoined ? 'stepped back in' : 'stepped in'} · ${formatRelativeTime(act.at, result.now)}`));
576
+ }
577
+ if (stepsIn.length > 3)
578
+ latest.push(style('dim', ` → … ${stepsIn.length - 3} more stepped in`));
515
579
  if (visible.includes('more chars') && result.latestAct !== undefined) {
516
- latest.push(`» ${commandPrefix(squarePath)} history --at ${actId(result.latestAct)} -C 2 --no-truncate`);
580
+ latest.push(`${commandPrefix(squarePath)} history --at ${actId(result.latestAct)} -C 2 --no-truncate`);
517
581
  }
582
+ const counts = [
583
+ `${here.length} here`,
584
+ ...(lingering > 0 ? [`${lingering} lingering`] : []),
585
+ `${result.doneCount} done`,
586
+ `cap ${cap}`,
587
+ `throttle ${result.throttlePerMinute === undefined ? 'none' : `${result.throttlePerMinute}/min`}`,
588
+ ].join(' · ');
518
589
  const output = [
519
- `${result.activeCount} active · ${result.doneCount} done · cap ${cap} · throttle ${result.throttlePerMinute === undefined ? 'none' : `${result.throttlePerMinute}/min`}`,
520
- ...(hold === undefined ? [] : ['', hold]), '', 'around the square', ...people, '', 'latest', ...latest,
590
+ style('dim', counts),
591
+ ...(hold === undefined ? [] : ['', hold]), '', style('dim', 'around the square'), ...people, '', style('dim', 'latest'), ...latest,
521
592
  ].join('\n');
522
593
  return withPathOutput(squarePath, output, { participantCount: result.activeCount, held: result.holdActive });
523
594
  }
524
- finally {
525
- await closeOpenSquare(square);
526
- }
527
595
  },
528
596
  present: (result) => process.stdout.write(result),
529
597
  };
@@ -558,7 +626,7 @@ export const inboxCommand = {
558
626
  fail('inbox requires --for-session <session-id>.');
559
627
  const retry = inboxLimitCommand(sessionId, json);
560
628
  if (duplicateLimit)
561
- fail(`✕ inbox accepts one --limit\n» ${retry}`);
629
+ fail(`✕ inbox accepts one --limit\n${retry}`);
562
630
  const limit = hasLimit
563
631
  ? parseBoundedLimit(limitValue, '--limit', INBOX_MAX_LIMIT, retry)
564
632
  : INBOX_DEFAULT_LIMIT;
@@ -1,20 +1,47 @@
1
1
  import { helpRequest } from '../help.js';
2
2
  import { isSquareError } from '../model.js';
3
- import { joinRecoveryCommand, participantsRecoveryCommand } from '../presentation.js';
3
+ import { formatRefusal, joinRecoveryCommand, participantsRecoveryCommand, takeoverRecoveryLines } from '../presentation.js';
4
+ import { inSquareCount, nowMs } from '../runtime.js';
5
+ import { openSquare } from '../square-file-adapter.js';
6
+ import { closeOpenSquare } from '../open-square.js';
4
7
  import { defaultContext, parseGlobalArgs } from './context.js';
5
8
  import { executeRegisteredCommand, findCommand } from './registry.js';
6
- function handleSquareError(error, squarePath, name) {
9
+ async function participantCountFor(squarePath) {
10
+ try {
11
+ const square = await openSquare(squarePath, { clock: nowMs });
12
+ try {
13
+ return inSquareCount((await square.artifact.read()).state);
14
+ }
15
+ finally {
16
+ await closeOpenSquare(square);
17
+ }
18
+ }
19
+ catch {
20
+ return undefined;
21
+ }
22
+ }
23
+ async function handleSquareError(error, squarePath, name) {
7
24
  if (isSquareError(error)) {
8
- process.stderr.write(`${error.message}\n`);
9
- if (squarePath !== undefined && /^Unknown participant/.test(error.message)) {
25
+ const existing = [error.message];
26
+ if (squarePath !== undefined && name !== undefined && error.code === 'already_joined') {
27
+ existing.push(...takeoverRecoveryLines(squarePath, name));
28
+ }
29
+ else if (squarePath !== undefined && /^✕ .* has never stepped into/.test(error.message)) {
10
30
  // The caller's name has never joined this square; only a join admits it.
11
31
  if (name !== undefined)
12
- process.stderr.write(`${joinRecoveryCommand(squarePath, name)}\n`);
32
+ existing.push(joinRecoveryCommand(squarePath, name));
13
33
  else
14
- process.stderr.write(`${participantsRecoveryCommand(squarePath)}\n`);
34
+ existing.push(participantsRecoveryCommand(squarePath));
35
+ }
36
+ else if (squarePath !== undefined && /^✕ .* is not standing in this square/.test(error.message)) {
37
+ existing.push(participantsRecoveryCommand(squarePath));
38
+ }
39
+ if (squarePath === undefined) {
40
+ process.stderr.write(`${existing.join('\n')}\n`);
15
41
  }
16
- else if (squarePath !== undefined && /^Unknown mention target/.test(error.message)) {
17
- process.stderr.write(`${participantsRecoveryCommand(squarePath)}\n`);
42
+ else {
43
+ const participantCount = await participantCountFor(squarePath);
44
+ process.stderr.write(formatRefusal(squarePath, existing, participantCount === undefined ? {} : { participantCount }));
18
45
  }
19
46
  process.exit(error.code === 'not_found' ? 1 : 2);
20
47
  }
@@ -45,6 +72,6 @@ export async function runCli(rawArgs = process.argv.slice(2)) {
45
72
  await executeRegisteredCommand(command, parsed.args.slice(1), defaultContext(command, parsed.squarePath, parsed.name));
46
73
  }
47
74
  catch (error) {
48
- handleSquareError(error, squarePath, requestedName);
75
+ await handleSquareError(error, squarePath, requestedName);
49
76
  }
50
77
  }
@@ -2,6 +2,7 @@ import { buildCommand, doneCommand, expressCommand, holdCommand, ignoreCommand,
2
2
  import { doctorCommand } from './maintenance-commands.js';
3
3
  import { harnessCommand, installCommand, uninstallCommand } from './harness-command.js';
4
4
  import { helpCommand, versionCommand } from './meta-commands.js';
5
+ import { mcpServerCommand } from './mcp-server-command.js';
5
6
  import { catchCommand, claudeHookCommand, codexHookCommand, historyCommand, inboxCommand, listCommand, participantsCommand, statusCommand, streamCommand, } from './observation-commands.js';
6
7
  /** Every public command is an executable adapter, including aliases and utility commands. */
7
8
  export const commandRegistry = [
@@ -29,6 +30,7 @@ export const commandRegistry = [
29
30
  { names: ['participants'], spec: participantsCommand },
30
31
  { names: ['help'], spec: helpCommand },
31
32
  { names: ['version', '--version', '-v'], spec: versionCommand },
33
+ { names: ['mcp-server'], spec: mcpServerCommand },
32
34
  ];
33
35
  export function findCommand(name) {
34
36
  return commandRegistry.find((command) => command.names.includes(name));
@@ -19,7 +19,7 @@ interface ActivityIntent {
19
19
  noMention: boolean;
20
20
  mentions: string[];
21
21
  reach?: Reach;
22
- reply?: number;
22
+ reply?: string;
23
23
  }
24
24
  interface BodyIntent {
25
25
  name: string;