@astrosheep/square 0.3.63 → 0.3.64

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 (166) hide show
  1. package/claude-plugin/.claude-plugin/plugin.json +3 -2
  2. package/claude-plugin/hooks/hooks.json +1 -22
  3. package/claude-plugin/hooks/register.js +119 -0
  4. package/claude-plugin/types.d.ts +7 -0
  5. package/codex-plugin/.codex-plugin/plugin.json +1 -1
  6. package/dist/activity-feed.d.ts +1 -10
  7. package/dist/activity-feed.js +1 -27
  8. package/dist/activity.js +57 -25
  9. package/dist/artifact.js +12 -24
  10. package/dist/attention-presentation.js +1 -0
  11. package/dist/automatic-session.d.ts +3 -1
  12. package/dist/automatic-session.js +21 -34
  13. package/dist/boundary-presentation.js +4 -13
  14. package/dist/canonical-path.d.ts +11 -0
  15. package/dist/canonical-path.js +47 -0
  16. package/dist/catch-decisions.js +14 -10
  17. package/dist/catch-selection.d.ts +6 -0
  18. package/dist/catch-selection.js +14 -0
  19. package/dist/claude-delivery.d.ts +11 -0
  20. package/dist/claude-delivery.js +145 -0
  21. package/dist/claude-mod.d.ts +8 -0
  22. package/dist/claude-mod.js +164 -0
  23. package/dist/cli/context.d.ts +0 -1
  24. package/dist/cli/context.js +9 -21
  25. package/dist/cli/harness-command.d.ts +0 -1
  26. package/dist/cli/harness-command.js +0 -6
  27. package/dist/cli/observation-commands.d.ts +1 -1
  28. package/dist/cli/observation-commands.js +31 -58
  29. package/dist/cli/program.js +20 -18
  30. package/dist/cli/registry.js +2 -2
  31. package/dist/cli/square-commands.js +10 -6
  32. package/dist/codex-hook.js +9 -12
  33. package/dist/codex-queue.d.ts +2 -2
  34. package/dist/codex-queue.js +7 -5
  35. package/dist/decisions.d.ts +2 -6
  36. package/dist/decisions.js +16 -17
  37. package/dist/delivery-health.d.ts +1 -2
  38. package/dist/delivery-health.js +4 -5
  39. package/dist/delivery-operations.d.ts +2 -5
  40. package/dist/delivery-operations.js +77 -214
  41. package/dist/delivery.d.ts +25 -47
  42. package/dist/delivery.js +14 -46
  43. package/dist/diagnostic-redaction.js +7 -9
  44. package/dist/file-changes.d.ts +0 -1
  45. package/dist/file-changes.js +2 -19
  46. package/dist/harness-claude.js +8 -6
  47. package/dist/harness-codex.d.ts +0 -2
  48. package/dist/harness-codex.js +0 -2
  49. package/dist/harness-links.js +13 -17
  50. package/dist/harness.js +3 -7
  51. package/dist/help.js +4 -3
  52. package/dist/host-ledger-file-adapter.d.ts +17 -8
  53. package/dist/host-ledger-file-adapter.js +164 -80
  54. package/dist/host-ledger.d.ts +55 -50
  55. package/dist/inbox.d.ts +0 -3
  56. package/dist/inbox.js +13 -30
  57. package/dist/index.d.ts +1 -1
  58. package/dist/index.js +1 -1
  59. package/dist/list.js +1 -4
  60. package/dist/mcp.js +18 -40
  61. package/dist/mention-parse.js +3 -9
  62. package/dist/model.d.ts +11 -2
  63. package/dist/model.js +1 -1
  64. package/dist/notifications.d.ts +3 -16
  65. package/dist/notifications.js +41 -122
  66. package/dist/opencode-delivery.d.ts +14 -0
  67. package/dist/opencode-delivery.js +171 -0
  68. package/dist/opencode.d.ts +39 -18
  69. package/dist/opencode.js +145 -40
  70. package/dist/packages/agent-delivery/src/claude-auth.d.ts +13 -0
  71. package/dist/packages/agent-delivery/src/claude-auth.js +70 -0
  72. package/dist/packages/agent-delivery/src/claude-native.d.ts +28 -0
  73. package/dist/packages/agent-delivery/src/claude-native.js +65 -0
  74. package/dist/packages/agent-delivery/src/claude.d.ts +14 -0
  75. package/dist/packages/agent-delivery/src/claude.js +66 -0
  76. package/dist/packages/agent-delivery/src/deadline.d.ts +11 -0
  77. package/dist/packages/agent-delivery/src/deadline.js +41 -0
  78. package/dist/packages/agent-delivery/src/errors.d.ts +16 -0
  79. package/dist/packages/agent-delivery/src/errors.js +41 -0
  80. package/dist/packages/agent-delivery/src/guards.d.ts +6 -0
  81. package/dist/packages/agent-delivery/src/guards.js +6 -0
  82. package/dist/packages/agent-delivery/src/index.d.ts +83 -0
  83. package/dist/packages/agent-delivery/src/index.js +110 -0
  84. package/dist/packages/agent-delivery/src/local-endpoint.d.ts +26 -0
  85. package/dist/packages/agent-delivery/src/local-endpoint.js +114 -0
  86. package/dist/packages/agent-delivery/src/opencode-native.d.ts +33 -0
  87. package/dist/packages/agent-delivery/src/opencode-native.js +75 -0
  88. package/dist/packages/agent-delivery/src/opencode.d.ts +14 -0
  89. package/dist/packages/agent-delivery/src/opencode.js +127 -0
  90. package/dist/packages/agent-delivery/src/paseo-native.d.ts +58 -0
  91. package/dist/packages/agent-delivery/src/paseo-native.js +122 -0
  92. package/dist/packages/agent-delivery/src/paseo.d.ts +19 -0
  93. package/dist/packages/agent-delivery/src/paseo.js +106 -0
  94. package/dist/packages/agent-delivery/src/pi-client.d.ts +10 -0
  95. package/dist/packages/agent-delivery/src/pi-client.js +106 -0
  96. package/dist/packages/agent-delivery/src/pi-protocol.d.ts +15 -0
  97. package/dist/packages/agent-delivery/src/pi-protocol.js +50 -0
  98. package/dist/packages/agent-delivery/src/pi-send.d.ts +23 -0
  99. package/dist/packages/agent-delivery/src/pi-send.js +4 -0
  100. package/dist/packages/agent-delivery/src/pi.d.ts +25 -0
  101. package/dist/packages/agent-delivery/src/pi.js +209 -0
  102. package/dist/packages/agent-delivery/src/types.d.ts +120 -0
  103. package/dist/participant-host.js +12 -13
  104. package/dist/participant-identity.d.ts +21 -0
  105. package/dist/participant-identity.js +32 -11
  106. package/dist/paseo-delivery.d.ts +2 -2
  107. package/dist/paseo-delivery.js +35 -27
  108. package/dist/paseo-timeline.js +25 -21
  109. package/dist/paseo.d.ts +2 -2
  110. package/dist/paseo.js +1 -2
  111. package/dist/ports.d.ts +7 -20
  112. package/dist/presentation-operations.js +16 -4
  113. package/dist/presentation.d.ts +19 -10
  114. package/dist/presentation.js +99 -177
  115. package/dist/registry.d.ts +15 -66
  116. package/dist/registry.js +45 -123
  117. package/dist/routes.d.ts +1 -28
  118. package/dist/routes.js +22 -104
  119. package/dist/runtime.d.ts +3 -9
  120. package/dist/runtime.js +14 -67
  121. package/dist/search.d.ts +0 -2
  122. package/dist/search.js +3 -10
  123. package/dist/square-actions.js +48 -72
  124. package/dist/square-application.js +25 -23
  125. package/dist/square-core.d.ts +3 -0
  126. package/dist/square-core.js +22 -5
  127. package/dist/square-facade.d.ts +2 -0
  128. package/dist/square-file-adapter.js +4 -7
  129. package/dist/square-projections.d.ts +40 -45
  130. package/dist/square-projections.js +43 -73
  131. package/dist/square-state.js +1 -4
  132. package/dist/square-storage.d.ts +5 -8
  133. package/dist/square-storage.js +13 -36
  134. package/dist/square-wiring.d.ts +0 -5
  135. package/dist/square-wiring.js +3 -21
  136. package/dist/stream.js +4 -6
  137. package/dist/time.js +2 -7
  138. package/dist/views.d.ts +4 -16
  139. package/dist/views.js +12 -19
  140. package/dist/wake-attempts.d.ts +2 -56
  141. package/dist/wake-attempts.js +15 -110
  142. package/dist/wake-eligibility.d.ts +43 -0
  143. package/dist/wake-eligibility.js +106 -0
  144. package/dist/wake-evidence.d.ts +32 -9
  145. package/dist/wake-evidence.js +29 -24
  146. package/dist/wake-sink.d.ts +13 -9
  147. package/dist/wake-sink.js +35 -54
  148. package/dist/watch.js +13 -21
  149. package/extensions/square-pi.js +10 -5
  150. package/guides/claude-native.md +101 -0
  151. package/guides/host-ledger.md +3 -1
  152. package/guides/participant.md +14 -7
  153. package/guides/pi-native-delivery.md +43 -0
  154. package/package.json +4 -3
  155. package/dist/claude-hook.d.ts +0 -10
  156. package/dist/claude-hook.js +0 -60
  157. package/dist/paseo-connection.d.ts +0 -15
  158. package/dist/paseo-connection.js +0 -141
  159. package/dist/presence.d.ts +0 -2
  160. package/dist/presence.js +0 -24
  161. package/dist/presented.d.ts +0 -2
  162. package/dist/presented.js +0 -10
  163. package/dist/state-cell.d.ts +0 -14
  164. package/dist/wake-port.d.ts +0 -23
  165. package/dist/wake-port.js +0 -27
  166. /package/dist/{state-cell.js → packages/agent-delivery/src/types.js} +0 -0
@@ -3,19 +3,13 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { participantIdentity } from './participant-identity.js';
5
5
  export { participantIdentity } from './participant-identity.js';
6
- import { audienceIncludes, audienceOf, MAX_IDENTITY_SET_SIZE, replayLandedAudiences } from './square-core.js';
6
+ import { audienceIncludes, audienceOf, MAX_IDENTITY_SET_SIZE } from './square-core.js';
7
7
  import { perceiveActivity } from './delivery.js';
8
- import { actId, presenceAnchor, publicActs, rosterNames, sayNumberFor, HELD_WAIT_BUDGET_MS } from './runtime.js';
8
+ import { actId, sayNumberFor, HELD_WAIT_BUDGET_MS } from './runtime.js';
9
9
  import { formatDuration, formatRelativeTime, formatTimestamp } from './time.js';
10
10
  import { compareParticipantActivity } from './decisions.js';
11
11
  import { grepSnippet } from './search.js';
12
12
  import { style } from './tty-style.js';
13
- function headerLine(squarePath, opts = {}) {
14
- const count = opts.participantCount ?? 0;
15
- const heldSuffix = opts.held ? ' — a hand is raised' : '';
16
- const line = `· ${displayPath(squarePath)} — ${count} in the square${heldSuffix}`;
17
- return opts.plain === true ? line : style('dim', line);
18
- }
19
13
  export function displayPath(squarePath, cwd = process.cwd()) {
20
14
  if (!path.isAbsolute(squarePath))
21
15
  return squarePath.split(path.sep).join('/');
@@ -32,7 +26,10 @@ export function displayPath(squarePath, cwd = process.cwd()) {
32
26
  return displayed.split(path.sep).join('/');
33
27
  }
34
28
  export function withPathOutput(squarePath, body = '', opts = {}) {
35
- return [headerLine(squarePath, opts), ...(body === '' ? [] : ['', body])].join('\n') + '\n';
29
+ const count = opts.participantCount ?? 0;
30
+ const heldSuffix = opts.held ? ' — a hand is raised' : '';
31
+ const line = `· ${displayPath(squarePath)} — ${count} in the square${heldSuffix}`;
32
+ return [opts.plain === true ? line : style('dim', line), ...(body === '' ? [] : ['', body])].join('\n') + '\n';
36
33
  }
37
34
  /** The same header/blank/body layout as withPathOutput, for refusal bodies written to stderr. */
38
35
  export function formatRefusal(squarePath, bodyLines, opts = {}) {
@@ -72,35 +69,7 @@ function formatAge(ms) {
72
69
  return `${Math.max(0, ms)}ms`;
73
70
  return `${Math.floor(ms / 1000)}s`;
74
71
  }
75
- function pluralize(count, singular, plural = `${singular}s`) {
76
- return count === 1 ? singular : plural;
77
- }
78
72
  const PRESENCE_WINDOW_MS = 8 * 60 * 60 * 1000;
79
- function presenceGlyph(participant) {
80
- if (participant.state === 'done')
81
- return '○';
82
- if (participant.presence === 'watching')
83
- return '◎';
84
- if (participant.lastActiveAt !== undefined)
85
- return '●';
86
- return '○';
87
- }
88
- function styledPresenceGlyph(participant) {
89
- const glyph = presenceGlyph(participant);
90
- return glyph === '○' ? style('dim', glyph) : glyph;
91
- }
92
- function presenceText(participant, now) {
93
- if (participant.state === 'done') {
94
- return participant.lastActiveAt === undefined ? 'stepped out of the square' : `stepped out of the square · ${formatRelativeTime(participant.lastActiveAt, now)}`;
95
- }
96
- if (participant.presence === 'watching') {
97
- const at = participant.lastActiveAt;
98
- return at === undefined ? 'catching' : `catching · ${formatRelativeTime(at, now)}`;
99
- }
100
- if (participant.lastActiveAt === undefined)
101
- return 'quiet';
102
- return participant.activityCount > 0 ? `${formatRelativeTime(participant.lastActiveAt, now)}` : `quiet · ${formatRelativeTime(participant.lastActiveAt, now)}`;
103
- }
104
73
  export function renderPresenceLines(participants, now, limit = 5) {
105
74
  const recent = participants
106
75
  .filter((p) => p.state === 'done' || p.presence === 'watching' || (p.lastActiveAt !== undefined && now - p.lastActiveAt <= PRESENCE_WINDOW_MS))
@@ -108,16 +77,25 @@ export function renderPresenceLines(participants, now, limit = 5) {
108
77
  const shown = recent.slice(0, limit);
109
78
  if (shown.length === 0)
110
79
  return [style('dim', ' ○ nobody nearby')];
111
- const lines = shown.map((p) => ` ${styledPresenceGlyph(p)} ${participantIdentity(p.name)}${style('dim', ` · ${presenceText(p, now)}`)}`);
80
+ const lines = shown.map((p) => {
81
+ const glyph = p.state === 'done' ? '○' : p.presence === 'watching' ? '◎' : p.lastActiveAt !== undefined ? '●' : '○';
82
+ const text = p.state === 'done'
83
+ ? (p.lastActiveAt === undefined ? 'stepped out of the square' : `stepped out of the square · ${formatRelativeTime(p.lastActiveAt, now)}`)
84
+ : p.presence === 'watching'
85
+ ? (p.lastActiveAt === undefined ? 'is nearby · catching' : `is nearby · catching · ${formatRelativeTime(p.lastActiveAt, now)}`)
86
+ : p.lastActiveAt === undefined ? 'is nearby · quiet'
87
+ : p.activityCount > 0 ? `is nearby · ${formatRelativeTime(p.lastActiveAt, now)}` : `is nearby · quiet · ${formatRelativeTime(p.lastActiveAt, now)}`;
88
+ return ` ${glyph === '○' ? style('dim', glyph) : glyph} ${participantIdentity(p.name)}${style('dim', ` ${text}`)}`;
89
+ });
112
90
  const remaining = recent.length - shown.length;
113
91
  if (remaining > 0)
114
92
  lines.push(style('dim', ` ○ … ${remaining} more nearby`));
115
93
  return lines;
116
94
  }
117
95
  const EXPRESS_HINTS = [
118
- '*asterisks* are your body — *slams table*, *sketches in the air*, *shrugs*',
119
- "you're standing in a square — words and gestures both land",
120
- 'the square runs on conversation — say things that move it, not nods that fill it',
96
+ '*asterisks* are your body — slam a table, shrug, sketch in the air',
97
+ "answer someone's actual words — they're standing right there",
98
+ "say the half-shaped thing — that's what the square is for",
121
99
  ];
122
100
  export function expressHintLine(ownActivityCount) {
123
101
  if (ownActivityCount !== 1 && ownActivityCount % 5 !== 0)
@@ -138,18 +116,14 @@ export function truncateExternalDiagnostic(diagnostic) {
138
116
  return diagnostic;
139
117
  return `${truncateChars(diagnostic, EXTERNAL_DIAGNOSTIC_MAX_CHARS - 1).text}…`;
140
118
  }
141
- function previewBody(body, maxLen = BODY_PREVIEW_LENGTH) {
119
+ function previewBodyFacts(body, maxLen = BODY_PREVIEW_LENGTH) {
142
120
  const preview = truncateChars(body, maxLen);
143
- return preview.remaining === 0 ? preview.text : `${preview.text}\n${style('dim', `… ${preview.remaining} more chars`)}`;
121
+ return {
122
+ text: preview.remaining === 0 ? preview.text : `${preview.text}\n${style('dim', `… ${preview.remaining} more chars`)}`,
123
+ clipped: preview.remaining > 0,
124
+ };
144
125
  }
145
126
  const UNREAD_PREVIEW_CHARS = 120;
146
- export function previewActivityBody(body) {
147
- const compact = body.replace(/\s+/g, ' ').trim();
148
- if (compact === '')
149
- return '(empty)';
150
- const preview = truncateChars(compact, UNREAD_PREVIEW_CHARS);
151
- return preview.remaining === 0 ? preview.text : `${preview.text}${style('dim', `… (+${preview.remaining} chars)`)}`;
152
- }
153
127
  export function renderRoomChangeText(event) {
154
128
  const actor = event.actor ?? 'someone';
155
129
  const identity = actor === 'someone' ? actor : participantIdentity(actor);
@@ -163,34 +137,34 @@ export function renderRoomChangeText(event) {
163
137
  case 'resume':
164
138
  return `${identity} lowered the hand`;
165
139
  case 'listen':
166
- return `${identity} turns an ear toward ${participantIdentity(event.target)}`;
140
+ return `${identity} turned an ear toward ${participantIdentity(event.target)}`;
167
141
  case 'ignore':
168
- return `${identity} turns away from ${participantIdentity(event.target)}`;
142
+ return `${identity} turned away from ${participantIdentity(event.target)}`;
169
143
  }
170
144
  }
171
145
  function renderedBody(body, maxChars) {
172
146
  if (!body)
173
- return '';
174
- return maxChars === undefined ? body : previewBody(body, maxChars);
147
+ return { text: '', clipped: false };
148
+ return maxChars === undefined ? { text: body, clipped: false } : previewBodyFacts(body, maxChars);
175
149
  }
176
150
  function bodySuffix(body) {
177
151
  if (body === '')
178
152
  return '';
179
153
  return `\n${body.split('\n').map((line) => ` ${line}`).join('\n')}`;
180
154
  }
181
- export function renderEventCli(event, opts = {}) {
155
+ function renderEventCliFacts(event, opts = {}) {
182
156
  const now = opts.now;
183
157
  const maxBody = opts.preview;
184
158
  switch (event.kind) {
185
159
  case 'join':
186
- return `· ${renderRoomChangeText(event)}`;
160
+ return { text: `· ${renderRoomChangeText(event)}`, bodyClipped: false };
187
161
  case 'hold':
188
- return `· ${renderRoomChangeText(event)}`;
162
+ return { text: `· ${renderRoomChangeText(event)}`, bodyClipped: false };
189
163
  case 'resume':
190
- return `${style('release', '✓')} ${renderRoomChangeText(event)}`;
164
+ return { text: `${style('release', '✓')} ${renderRoomChangeText(event)}`, bodyClipped: false };
191
165
  case 'listen':
192
166
  case 'ignore':
193
- return `· ${renderRoomChangeText(event)}`;
167
+ return { text: `· ${renderRoomChangeText(event)}`, bodyClipped: false };
194
168
  case 'say': {
195
169
  const body = renderedBody(event.body, maxBody);
196
170
  const mention = opts.mention;
@@ -199,16 +173,22 @@ export function renderEventCli(event, opts = {}) {
199
173
  : '';
200
174
  const replySuffix = event.reply === undefined ? '' : ` · replies to ${actId(event.reply)}`;
201
175
  const meta = ` #${opts.actNumber ?? 1} · ${actId(event)} · ${formatRelativeTime(event.at, now)}${mentionSuffix}${replySuffix}`;
202
- return `● ${participantIdentity(event.actor)}${style('dim', meta)}${bodySuffix(body)}`;
176
+ return { text: `● ${participantIdentity(event.actor)}${style('dim', meta)}${bodySuffix(body.text)}`, bodyClipped: body.clipped };
203
177
  }
204
178
  case 'done': {
205
179
  const body = renderedBody(event.body, maxBody);
206
- return `${style('dim', '○')} ${participantIdentity(event.actor)} stepped out of the square — done${style('dim', ` · ${actId(event)} · ${formatRelativeTime(event.at, now)}`)}${bodySuffix(body)}`;
180
+ return {
181
+ text: `${style('dim', '○')} ${participantIdentity(event.actor)} stepped out of the square — done${style('dim', ` · ${actId(event)} · ${formatRelativeTime(event.at, now)}`)}${bodySuffix(body.text)}`,
182
+ bodyClipped: body.clipped,
183
+ };
207
184
  }
208
185
  case 'read':
209
- return '';
186
+ return { text: '', bodyClipped: false };
210
187
  }
211
188
  }
189
+ export function renderEventCli(event, opts = {}) {
190
+ return renderEventCliFacts(event, opts).text;
191
+ }
212
192
  function renderPresenceOnlySay(event, opts = {}) {
213
193
  const audience = audienceOf(event);
214
194
  const targets = audience.kind === 'bell' ? [] : audience.names;
@@ -224,19 +204,22 @@ function renderPresenceOnlySay(event, opts = {}) {
224
204
  ].join(' and ')}`;
225
205
  return `● ${participantIdentity(event.actor)}${meta}\n${style('dim', ` talked to${dest}`)}`;
226
206
  }
227
- export function renderAmbientEvent(event, viewer, opts = {}) {
207
+ export function renderAmbientEventFacts(event, viewer, opts = {}) {
228
208
  if (event.kind !== 'say')
229
- return renderEventCli(event, opts);
209
+ return renderEventCliFacts(event, opts);
230
210
  if (opts.perception === undefined && opts.squareState === undefined) {
231
211
  throw new Error('Ambient say rendering requires a settled perception or SquareState');
232
212
  }
233
213
  const seen = opts.perception ?? perceiveActivity(opts.squareState, event, viewer);
234
214
  if (seen === 'presence') {
235
215
  const presence = renderPresenceOnlySay(event, opts);
236
- const body = opts.presencePreview === undefined ? '' : renderedBody(event.body, opts.presencePreview);
237
- return `${presence}${bodySuffix(body)}`;
216
+ const body = opts.presencePreview === undefined ? { text: '', clipped: false } : renderedBody(event.body, opts.presencePreview);
217
+ return { text: `${presence}${bodySuffix(body.text)}`, bodyClipped: body.clipped };
238
218
  }
239
- return renderEventCli(event, opts);
219
+ return renderEventCliFacts(event, opts);
220
+ }
221
+ export function renderAmbientEvent(event, viewer, opts = {}) {
222
+ return renderAmbientEventFacts(event, viewer, opts).text;
240
223
  }
241
224
  function draftSavedLines(draftPath) {
242
225
  return draftPath === undefined ? [] : [style('dim', `· draft kept: ${draftPath}`)];
@@ -256,14 +239,16 @@ function renderUnreadSummary(opts) {
256
239
  if (preview.perception === 'presence') {
257
240
  return style('dim', ` · ${participantIdentity(item.name)} spoke — ${formatAge(item.latestActivityAgeMs)} ago · ${rendered.replace(/\n/g, ' ')}`);
258
241
  }
259
- return style('dim', ` · ${participantIdentity(item.name)} spoke — ${formatAge(item.latestActivityAgeMs)} ago · "${previewActivityBody(preview.act.body)}"`);
242
+ const compact = preview.act.body.replace(/\s+/g, ' ').trim();
243
+ const bodyPreview = compact === '' ? { text: '(empty)', remaining: 0 } : truncateChars(compact, UNREAD_PREVIEW_CHARS);
244
+ const bodyText = bodyPreview.remaining === 0 ? bodyPreview.text : `${bodyPreview.text}${style('dim', `… (+${bodyPreview.remaining} chars)`)}`;
245
+ return style('dim', ` · ${participantIdentity(item.name)} spoke — ${formatAge(item.latestActivityAgeMs)} ago · "${bodyText}"`);
260
246
  }),
261
247
  ]),
262
248
  ...(remainingSummaries === 0 ? [] : [style('dim', ` · ${remainingSummaries} more participants have unread activity`)]),
263
- ...opts.roomChanges.map((act) => style('dim', ` · ${renderRoomChangeText(act)}`)),
264
249
  ];
265
250
  }
266
- export function renderPendingFeed(history, publicItems, roomChanges, viewer = '', squareState) {
251
+ export function renderPendingFeed(history, publicItems, viewer = '', squareState) {
267
252
  const lines = [];
268
253
  for (const act of publicItems) {
269
254
  const rendered = renderAmbientEvent(act, viewer, {
@@ -273,19 +258,13 @@ export function renderPendingFeed(history, publicItems, roomChanges, viewer = ''
273
258
  if (rendered !== '')
274
259
  lines.push(rendered);
275
260
  }
276
- const publicIndexes = new Set(publicItems.map((item) => item.index));
277
- for (const act of roomChanges) {
278
- if (publicIndexes.has(act.index))
279
- continue;
280
- lines.push(`· ${renderRoomChangeText(act)}`);
281
- }
282
261
  return lines.join('\n\n');
283
262
  }
284
263
  export function renderActivityBlocked(opts) {
285
264
  const readNowCommand = `${participantCommandPrefix(opts.squarePath, opts.name)} catch --now`;
286
265
  return withPathOutput(opts.squarePath, [
287
266
  `${style('blocked', '✕')} your activity doesn't land — the square moved behind your back`,
288
- ...renderUnreadSummary({ activitySummaries: opts.activitySummaries, roomChanges: opts.unreadRoomChanges, viewer: opts.name }),
267
+ ...renderUnreadSummary({ activitySummaries: opts.activitySummaries, viewer: opts.name }),
289
268
  ...draftSavedLines(opts.draftPath),
290
269
  `${readNowCommand}`,
291
270
  ' · take it in, then retry:',
@@ -306,18 +285,27 @@ export function renderActivityUncertain(opts) {
306
285
  `${commandPrefix(opts.squarePath)} history${opts.landedId === undefined ? ' --limit 10' : ` --at ${opts.landedId}`} --no-truncate`,
307
286
  ], opts);
308
287
  }
288
+ /** A blank or whitespace hold reason is absent, never an empty sensory line. */
289
+ function holdReasonLines(reason) {
290
+ const text = reason?.trim() ?? '';
291
+ return text === '' ? [] : [style('dim', ` · ${text}`)];
292
+ }
293
+ function heldMainLine(holder, styled) {
294
+ const glyph = styled ? style('blocked', '✕') : '✕';
295
+ const who = holder === undefined ? 'a hand is raised' : `${participantIdentity(holder)} has a hand raised`;
296
+ return `${glyph} ${who} — voices drop, yours too`;
297
+ }
309
298
  export function renderExpressWaiting(opts) {
310
299
  if (opts.reason === 'throttled') {
311
300
  return [
312
- '✕ the square is packed',
313
- ` · your activity is waiting · next opening in ${formatDuration(opts.delayMs)}`,
301
+ '✕ the square is packed — shoulder to shoulder',
302
+ ` · a lull opens in ${formatDuration(opts.delayMs)}`,
314
303
  ' · --no-wait saves a draft and returns now',
315
304
  ].join('\n');
316
305
  }
317
- const holder = opts.holder === undefined ? 'a hand is raised' : `${participantIdentity(opts.holder)} raised a hand`;
318
306
  return [
319
- `✕ your activity doesn't land — ${holder}`,
320
- ...(opts.holdReason === undefined ? [] : [` · ${opts.holdReason}`]),
307
+ heldMainLine(opts.holder, false),
308
+ ...holdReasonLines(opts.holdReason),
321
309
  ` · your activity is waiting — after ${formatDuration(HELD_WAIT_BUDGET_MS)} it saves a draft and stops`,
322
310
  ' · --no-wait saves a draft and returns now',
323
311
  ].join('\n');
@@ -326,31 +314,19 @@ export function renderExpressNoWait(opts) {
326
314
  const retryCommand = opts.retryCommand;
327
315
  const lines = opts.reason === 'throttled'
328
316
  ? [
329
- `${style('blocked', '✕')} the square is packed`,
330
- ` · next opening in ${formatDuration(opts.delayMs)}`,
317
+ `${style('blocked', '✕')} the square is packed — shoulder to shoulder`,
318
+ ` · a lull opens in ${formatDuration(opts.delayMs)}`,
331
319
  ...draftSavedLines(opts.draftPath),
332
320
  `${withDraftInput(retryCommand, opts.draftPath)}`,
333
321
  ]
334
322
  : [
335
- `${style('blocked', '✕')} your activity doesn't land — a hand is raised`,
336
- style('dim', ` · ${opts.holdReason ?? 'the square holds its breath'}`),
323
+ heldMainLine(opts.holder, true),
324
+ ...holdReasonLines(opts.holdReason),
337
325
  ...draftSavedLines(opts.draftPath),
338
326
  `${withDraftInput(retryCommand, opts.draftPath)}`,
339
327
  ];
340
328
  return withPathOutput(opts.squarePath, lines.join('\n'), { participantCount: opts.participantCount, held: opts.held });
341
329
  }
342
- export function renderPublicTail(squareState, events, lastN, now, viewer = '') {
343
- const publicItems = publicActs(events);
344
- const selected = lastN == null ? publicItems : publicItems.slice(-lastN);
345
- const preview = lastN == null ? undefined : BODY_PREVIEW_LENGTH;
346
- return selected
347
- .map((event) => renderAmbientEvent(event, viewer, { now, preview, actNumber: event.kind === 'say' ? sayNumberFor(events, event) : undefined, squareState }))
348
- .filter(Boolean)
349
- .join('\n\n');
350
- }
351
- function lastPresenceAnchor(squareState, name, landed) {
352
- return presenceAnchor(squareState, name, landed);
353
- }
354
330
  export function renderPresenceAnchor(names) {
355
331
  const visibleNames = names.slice(0, MAX_IDENTITY_SET_SIZE);
356
332
  const participants = visibleNames.map((name) => participantIdentity(name)).join(', ');
@@ -358,45 +334,7 @@ export function renderPresenceAnchor(names) {
358
334
  const suffix = remaining === 0 ? '' : ` and ${remaining} more`;
359
335
  return style('dim', names.length === 1 ? `→ ${participants} was here` : `→ ${participants}${suffix} were here`);
360
336
  }
361
- export function renderActivitiesView(squareState, visible, lastN, noTruncate, squarePath, viewer = '', mode = 'ambient') {
362
- const publicVisible = visible.filter((act) => act.kind === 'say' || act.kind === 'done');
363
- const shown = lastN == null ? publicVisible : publicVisible.slice(-lastN);
364
- const previewLen = noTruncate ? undefined : BODY_PREVIEW_LENGTH;
365
- const markers = new Map();
366
- const landed = replayLandedAudiences(squareState.acts);
367
- for (const participant of rosterNames(squareState)) {
368
- const anchor = lastPresenceAnchor(squareState, participant, landed);
369
- if (anchor >= 0)
370
- markers.set(anchor, [...(markers.get(anchor) ?? []), participant]);
371
- }
372
- const chunks = [];
373
- for (const act of shown) {
374
- const opts = {
375
- preview: previewLen,
376
- actNumber: act.kind === 'say' ? sayNumberFor(squareState.acts, act) : undefined,
377
- };
378
- const rendered = mode === 'archive'
379
- ? renderEventCli(act, opts)
380
- : renderAmbientEvent(act, viewer, { ...opts, squareState });
381
- if (rendered !== '')
382
- chunks.push(rendered);
383
- const participants = markers.get(act.index);
384
- if (participants !== undefined)
385
- chunks.push(renderPresenceAnchor(participants));
386
- }
387
- if (chunks.length === 0)
388
- return 'latest\n ○ no public activity in this view';
389
- if (previewLen !== undefined) {
390
- const truncated = shown.some((act) => act.kind === 'say' && act.body.length > previewLen && (mode === 'archive' || perceiveActivity(squareState, act, viewer) === 'full'));
391
- if (truncated)
392
- chunks.push(`${commandPrefix(squarePath)} history --no-truncate`);
393
- }
394
- return chunks.join('\n\n');
395
- }
396
337
  const GREP_PREVIEW_CHARS = 160;
397
- function highlightGrepMatch(text) {
398
- return style('match', text);
399
- }
400
338
  export function renderGrepActivitiesView(visible, totalMatches, noTruncate, squarePath, pattern, fixed = false, perception) {
401
339
  const publicVisible = visible.filter((act) => act.kind === 'say' || act.kind === 'done');
402
340
  if (totalMatches === 0)
@@ -418,14 +356,14 @@ export function renderGrepActivitiesView(visible, totalMatches, noTruncate, squa
418
356
  }
419
357
  const snippet = grepSnippet(rawBody, pattern, GREP_PREVIEW_CHARS, fixed);
420
358
  if (snippet === undefined) {
421
- const preview = previewBody(rawBody, GREP_PREVIEW_CHARS);
359
+ const preview = previewBodyFacts(rawBody, GREP_PREVIEW_CHARS).text;
422
360
  chunks.push(`${header}${preview === '' ? '' : `\n ${preview}`}`);
423
361
  continue;
424
362
  }
425
363
  const clippedBefore = snippet.beforeOmitted > 0;
426
364
  const clippedAfter = snippet.afterOmitted > 0;
427
365
  truncated ||= clippedBefore || clippedAfter;
428
- const text = `${clippedBefore ? '… ' : ''}${snippet.before}${highlightGrepMatch(snippet.match)}${snippet.after}${clippedAfter ? ' …' : ''}`;
366
+ const text = `${clippedBefore ? '… ' : ''}${snippet.before}${style('match', snippet.match)}${snippet.after}${clippedAfter ? ' …' : ''}`;
429
367
  const omitted = clippedBefore || clippedAfter
430
368
  ? `\n${style('dim', ` · ${snippet.beforeOmitted} chars before · ${snippet.afterOmitted} chars after`)}`
431
369
  : '';
@@ -439,18 +377,16 @@ export function renderGrepActivitiesView(visible, totalMatches, noTruncate, squa
439
377
  }
440
378
  return chunks.join('\n\n');
441
379
  }
442
- function renderActivityLimitBody(opts) {
443
- const countText = opts.count !== undefined && opts.hardCap !== undefined ? ` (${opts.count}/${opts.hardCap})` : '';
380
+ export function renderActivityLimit(opts) {
381
+ // The cap wording lives inside the count: the speaker is simply spent.
382
+ const spoken = opts.count !== undefined && opts.hardCap !== undefined ? ` — ${opts.count}/${opts.hardCap} spoken` : '';
444
383
  const doneCommand = `${participantCommandPrefix(opts.squarePath, opts.name)} done`;
445
- return [
446
- `${style('blocked', '✕')} your activity doesn't land — the cap is reached${countText}`,
384
+ return withPathOutput(opts.squarePath, [
385
+ `${style('blocked', '✕')} nothing left in you${spoken}`,
447
386
  ...draftSavedLines(opts.draftPath),
448
387
  ' · your draft stays unsent; done only steps out',
449
388
  doneCommand,
450
- ].join('\n');
451
- }
452
- export function renderActivityLimit(opts) {
453
- return withPathOutput(opts.squarePath, renderActivityLimitBody(opts), { participantCount: opts.participantCount, held: opts.held });
389
+ ].join('\n'), { participantCount: opts.participantCount, held: opts.held });
454
390
  }
455
391
  export function renderWatchAlreadyActive(opts) {
456
392
  return [
@@ -480,53 +416,39 @@ export function renderWatchStatus(opts) {
480
416
  case 'stale':
481
417
  case 'empty-now': {
482
418
  const prefix = participantCommandPrefix(opts.squarePath, opts.name);
483
- const quiet = opts.status === 'stale' && opts.idleMs !== undefined
484
- ? style('dim', `○ ${formatDuration(opts.idleMs)} of quiet — nothing new for you`)
485
- : style('dim', '○ only footsteps in the square — nothing new for you');
419
+ // Quiet graduates with the idle length; an empty catch has no idle budget to report.
420
+ const idleMs = opts.idleMs;
421
+ const quiet = idleMs !== undefined && idleMs >= 60 * 60 * 1000
422
+ ? style('dim', `○ dust lies thick — ${formatDuration(idleMs)} of quiet`)
423
+ : idleMs !== undefined && idleMs >= 60 * 1000
424
+ ? style('dim', `○ dust settles on the flagstones — ${formatDuration(idleMs)} of quiet`)
425
+ : style('dim', '○ only footsteps in the square — nothing new for you');
426
+ // The quiet report never repeats the catch just run.
427
+ const hints = [`${prefix} catch --idle 30m`];
486
428
  return [
487
429
  quiet,
488
- ...(opts.showCatchHint === false
489
- ? []
490
- : [`${prefix} catch --idle 30m`, ` glance: ${prefix} catch --now`]),
430
+ ...(opts.showCatchHint === false ? [] : hints),
491
431
  ...presenceLines,
492
432
  ].join('\n');
493
433
  }
494
434
  case 'quorum':
495
435
  return [`${style('release', '✓')} everyone else has left — the square is yours alone`, `${participantCommandPrefix(opts.squarePath, opts.name)} done`].join('\n');
496
- case 'capped':
497
- return [`${style('blocked', '✕')} nothing left in you — the cap is reached`, `${participantCommandPrefix(opts.squarePath, opts.name)} done`].join('\n');
436
+ case 'capped': {
437
+ const spoken = opts.hardCap === undefined || opts.hardCap === null
438
+ ? ''
439
+ : ` — ${opts.ownActivityCount ?? 0}/${opts.hardCap} spoken`;
440
+ return [`${style('blocked', '✕')} nothing left in you${spoken}`, `${participantCommandPrefix(opts.squarePath, opts.name)} done`].join('\n');
441
+ }
498
442
  }
499
443
  }
500
- function renderRoomChanges(changes) {
501
- if (changes.length === 0)
502
- return '';
503
- return [`${style('changed', '▲')} while your back was turned`, ...changes.map((act) => ` · ${renderRoomChangeText(act)}`)].join('\n');
504
- }
505
444
  export function renderDoctorClean() {
506
445
  return '✓ no problems found';
507
446
  }
508
447
  export function renderDoctorUnfixable(reason) {
509
448
  return ['✕ unreadable artifact', ` · ${truncateExternalDiagnostic(reason)}`].join('\n');
510
449
  }
511
- export function renderWatchOutput(history, publicItems, roomChanges, opts) {
450
+ export function renderWatchOutput(history, publicItems, opts) {
512
451
  const sections = [];
513
- if (opts.stalePartial) {
514
- const prefix = participantCommandPrefix(opts.squarePath, opts.viewer);
515
- const quiet = opts.idleMs === undefined
516
- ? '○ only footsteps in the square — nothing new for you'
517
- : `○ ${formatDuration(opts.idleMs)} of quiet — nothing else for you`;
518
- sections.push([
519
- quiet,
520
- ...(opts.showCatchHint === false
521
- ? []
522
- : [`${prefix} catch --idle 30m`, ` glance: ${prefix} catch --now`]),
523
- ].join('\n'));
524
- }
525
- const publicIndexes = new Set(publicItems.map((item) => item.index));
526
- const presenceChanges = roomChanges.filter(({ index }) => !publicIndexes.has(index));
527
- const room = renderRoomChanges(presenceChanges);
528
- if (room !== '')
529
- sections.push(room);
530
452
  if (publicItems.length > 0) {
531
453
  const rendered = publicItems
532
454
  .map((act) => renderAmbientEvent(act, opts.viewer, {
@@ -1,51 +1,12 @@
1
- /** Machine-local participant discovery cache. */
2
- import { type StoredAct } from './model.js';
3
- import type { HostLedgerPort, PresenceRecord } from './host-ledger.js';
4
- export type SessionChannel = 'claude-code' | 'codex' | 'opencode' | 'pi' | 'paseo' | 'unknown';
5
- export interface RegistryBinding {
6
- sessionId: string;
7
- name: string;
8
- squarePath: string;
9
- channel: SessionChannel;
10
- child: boolean;
11
- route?: PresenceRecord['route'];
12
- updatedAt: number;
13
- epoch: number;
14
- }
15
- export interface RegistryWriteOptions {
16
- channel?: SessionChannel;
17
- child?: boolean;
18
- route?: PresenceRecord['route'];
19
- at?: number;
20
- env?: NodeJS.ProcessEnv;
21
- }
22
- type PresenceWithEpoch = PresenceRecord & {
23
- readonly epoch?: number;
24
- };
25
- export declare function registryPath(env?: NodeJS.ProcessEnv): string;
26
- export declare function canonicalSquarePath(squarePath: string): Promise<string>;
27
- export declare function presenceEpoch(record: PresenceWithEpoch | undefined): number;
28
- /** Runs `fn` inside the ownership claim critical section, under the same lock as claims and finalize. */
29
- export declare function withOwnershipClaimLock<T>(env: NodeJS.ProcessEnv, fn: () => Promise<T>, signal?: AbortSignal): Promise<T>;
30
- export declare function recordJoin(sessionId: string, name: string, squarePath: string, options?: RegistryWriteOptions): Promise<void>;
31
- export declare function recordDone(sessionId: string, name: string, squarePath: string, options?: RegistryWriteOptions): Promise<void>;
32
- export declare function readActiveBindings(now?: number, env?: NodeJS.ProcessEnv): Promise<RegistryBinding[]>;
33
- export declare function lookupSessionBindings(sessionId: string, now?: number, env?: NodeJS.ProcessEnv): Promise<RegistryBinding[]>;
34
- export declare function lookupSession(sessionId: string, now?: number, env?: NodeJS.ProcessEnv): Promise<Array<{
35
- name: string;
36
- squarePath: string;
37
- }>>;
38
- export declare function lookupParticipant(squarePath: string, name: string, now?: number, env?: NodeJS.ProcessEnv): Promise<RegistryBinding[]>;
39
- export declare function localParticipantOwner(squarePath: string, name: string, env?: NodeJS.ProcessEnv, now?: number): Promise<string | undefined>;
40
- export declare function localParticipantName(squarePath: string, env?: NodeJS.ProcessEnv): Promise<string | undefined>;
1
+ /** Participant ownership and local session identity on the host ledger. */
2
+ import { type FileHostLedgerPort } from './host-ledger-file-adapter.js';
3
+ import type { HostLedgerPort, PresenceChannel, PresenceRecord } from './host-ledger.js';
4
+ export declare function presenceEpoch(record: PresenceRecord | undefined): number;
5
+ export declare function localParticipantName(squarePath: string, hostLedger: HostLedgerPort, env?: NodeJS.ProcessEnv): Promise<string | undefined>;
41
6
  export declare function squareAssignedParticipantName(env?: NodeJS.ProcessEnv): string | undefined;
42
- export type CurrentParticipantBinding = Readonly<{
43
- created: boolean;
44
- sessionId: string;
45
- }>;
46
7
  export type OwnershipClaim = {
47
8
  readonly status: 'acquired' | 'owned';
48
- readonly record: PresenceWithEpoch;
9
+ readonly record: PresenceRecord;
49
10
  readonly sessionId: string;
50
11
  readonly epoch: number;
51
12
  } | undefined;
@@ -62,43 +23,31 @@ export type TakeoverRunResult<T> = {
62
23
  readonly status: 'busy';
63
24
  readonly epoch: number;
64
25
  };
65
- export declare function readParticipantOwner(squarePath: string, name: string, env?: NodeJS.ProcessEnv): Promise<RegistryBinding | undefined>;
66
- export declare function sessionOwnsParticipant(squarePath: string, name: string, sessionId: string, env?: NodeJS.ProcessEnv, expectedEpoch?: number): Promise<boolean>;
67
- export declare function releaseSessionParticipantClaim(squarePath: string, name: string, env: NodeJS.ProcessEnv, claim: OwnershipClaim): Promise<void>;
68
- export declare function claimSessionParticipant(squarePath: string, name: string, env?: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<OwnershipClaim>;
26
+ export declare function readParticipantOwner(squarePath: string, name: string, hostLedger: HostLedgerPort): Promise<PresenceRecord | undefined>;
27
+ export declare function sessionOwnsParticipant(squarePath: string, name: string, sessionId: string, hostLedger: HostLedgerPort, expectedEpoch?: number): Promise<boolean>;
28
+ export declare function releaseSessionParticipantClaim(hostLedger: HostLedgerPort, claim: OwnershipClaim): Promise<void>;
29
+ export declare function claimSessionParticipant(squarePath: string, name: string, hostLedger: HostLedgerPort, env?: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<OwnershipClaim>;
69
30
  /**
70
31
  * Fenced takeover: the ownership claim and the lifecycle run as one critical section under the
71
- * presence-claim lock, so no second takeover can interleave between claim and commit. The claim
32
+ * claim lock, so no second takeover can interleave between claim and commit. The claim
72
33
  * persists nothing until the lifecycle commits: standing owner rows stay authoritative through
73
34
  * the lifecycle (a self-takeover can therefore never clobber its own standing row). On success
74
35
  * the standing rows are replaced by the new owner's row at the claim epoch; on refusal, rows
75
36
  * carrying the claim token (session + channel + claim epoch) are withdrawn and the captured
76
37
  * standing rows are restored exactly.
77
38
  */
78
- export declare function claimSessionTakeover<T>(squarePath: string, name: string, env: NodeJS.ProcessEnv | undefined, opts: {
39
+ export declare function claimSessionTakeover<T>(squarePath: string, name: string, hostLedger: HostLedgerPort, env: NodeJS.ProcessEnv | undefined, opts: {
79
40
  readonly expectedEpoch?: number;
80
41
  readonly expectedSession?: string;
81
42
  } | undefined, lifecycle: (claim: TakeoverClaimToken) => Promise<T>, signal?: AbortSignal): Promise<TakeoverRunResult<T>>;
82
- export declare function releaseSessionParticipant(squarePath: string, name: string, env?: NodeJS.ProcessEnv): Promise<void>;
83
- export declare function bindCurrentParticipant(squarePath: string, name: string, env?: NodeJS.ProcessEnv): Promise<CurrentParticipantBinding>;
84
- export declare function unbindCurrentParticipant(squarePath: string, name: string, env?: NodeJS.ProcessEnv): Promise<boolean>;
85
- export interface RegistryPruneResult {
86
- removed: number;
87
- kept: number;
88
- }
89
- export declare function pruneRegistry(readActs: (squarePath: string) => StoredAct[] | undefined | Promise<StoredAct[] | undefined>, now?: number): Promise<RegistryPruneResult>;
43
+ export declare function releaseSessionParticipant(squarePath: string, name: string, hostLedger: HostLedgerPort, env?: NodeJS.ProcessEnv): Promise<void>;
90
44
  export interface LocalSessionIdentity {
91
45
  sessionId: string;
92
- channel: SessionChannel;
46
+ channel: PresenceChannel;
93
47
  child: boolean;
94
48
  paseoAgentId?: string;
95
49
  }
96
50
  export declare function localSessionIdentities(env?: NodeJS.ProcessEnv): LocalSessionIdentity[];
97
51
  export declare function hasAutomaticDeliveryIdentity(env?: NodeJS.ProcessEnv): boolean;
98
- export declare function recordLocalJoin(name: string, squarePath: string, env?: NodeJS.ProcessEnv): Promise<void>;
99
- export declare function recordLocalDone(name: string, squarePath: string, env?: NodeJS.ProcessEnv): Promise<void>;
100
- export declare function recordSessionJoin(sessionId: string, name: string, squarePath: string, channel: SessionChannel, env?: NodeJS.ProcessEnv): Promise<string>;
101
- export declare function recordSessionDone(sessionId: string, name: string, squarePath: string, channel: SessionChannel, env?: NodeJS.ProcessEnv): Promise<boolean>;
102
52
  /** Bind the host ledger to the caller's captured environment. */
103
- export declare function hostLedgerForEnv(env?: NodeJS.ProcessEnv): HostLedgerPort;
104
- export {};
53
+ export declare function hostLedgerForEnv(env?: NodeJS.ProcessEnv): FileHostLedgerPort;