@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
@@ -1,15 +1,17 @@
1
1
  import { cmdActivity } from '../activity.js';
2
- import { formatActivityId, formatHardCap, parseActivityId, } from '../model.js';
3
- import { participantCommandPrefix, participantIdentity, quoteShell, renderEventCli, renderAmbientEvent, withPathOutput, } from '../presentation.js';
4
- import { claimSessionParticipant, hasAutomaticDeliveryIdentity, localSessionIdentities, lookupParticipant, readParticipantOwner, recordSessionDone, } from '../registry.js';
5
- import { createHostLedgerPort } from '../host-ledger-file-adapter.js';
6
- import { sessionIdsFromEnvironment } from '../square-projections.js';
7
- import { nowMs } from '../runtime.js';
2
+ import { scanMentionCandidates } from '../mention-parse.js';
3
+ import { validateDoneBody } from '../decisions.js';
4
+ import { formatHardCap, } from '../model.js';
5
+ import { formatRefusal, participantCommandPrefix, participantIdentity, takeoverRecoveryLines, renderRoomChangeText, renderAmbientEvent, withPathOutput, } from '../presentation.js';
6
+ import { hostLedgerForEnv, hasAutomaticDeliveryIdentity, } from '../registry.js';
7
+ import { actId, nowMs } from '../runtime.js';
8
8
  import { createSquare, openSquare } from '../square-file-adapter.js';
9
9
  import { closeOpenSquare } from '../open-square.js';
10
- import { openParticipant, Square } from '../square-wiring.js';
10
+ import { Square } from '../square-wiring.js';
11
+ import { createSquareApplication } from '../square-application.js';
11
12
  import { entryPresentation, eventPresentation } from '../views.js';
12
13
  import { createDefaultWakeTransport } from '../notifications.js';
14
+ import { style } from '../tty-style.js';
13
15
  import { fail, parseBoundedLimit, parseHardCap, parsePositiveInteger, readStdin, requireParticipant, requireSquarePath, requireValue, resolveBody, usage, } from './context.js';
14
16
  const JOIN_DEFAULT_LAST = 10;
15
17
  const JOIN_MAX_LAST = 100;
@@ -57,7 +59,7 @@ export const buildCommand = {
57
59
  await createSquare(squarePath, intent.options, snippet);
58
60
  const cap = intent.options.hardCap === null ? 'unlimited' : formatHardCap(intent.options.hardCap);
59
61
  const throttle = intent.options.throttlePerMinute === undefined ? [] : [` · throttle ${intent.options.throttlePerMinute}/min`];
60
- return withPathOutput(squarePath, ['✓ built', ` · cap ${cap}`, ...throttle, ' · participants (none seeded — first join adds names)'].join('\n'), { participantCount: 0 });
62
+ return withPathOutput(squarePath, ['✓ the square is open', ` · cap ${cap}`, ...throttle, ' · nobody here yet — the first join steps in'].join('\n'), { participantCount: 0 });
61
63
  },
62
64
  present: (result) => process.stdout.write(result),
63
65
  };
@@ -84,77 +86,60 @@ export const joinCommand = {
84
86
  parse: parseJoin,
85
87
  async execute(intent, context) {
86
88
  const squarePath = requireSquarePath(context);
87
- const beforeSquare = await openSquare(squarePath, { clock: nowMs });
89
+ const hostLedger = hostLedgerForEnv(context.env);
90
+ const beforeSquare = await openSquare(squarePath, { clock: nowMs, env: context.env, hostLedger });
88
91
  const before = await entryPresentation(beforeSquare, intent.name, intent.lastN);
89
92
  await closeOpenSquare(beforeSquare);
90
- const hostLedger = createHostLedgerPort();
91
- const square = await Square.at({ path: squarePath, clock: nowMs, hostLedger, wakeTransport: await createDefaultWakeTransport(hostLedger, nowMs) });
93
+ const application = createSquareApplication({ cwd: context.cwd, env: context.env, squarePath, participant: intent.name, clock: nowMs, hostLedger, wakeTransport: await createDefaultWakeTransport(hostLedger, nowMs) });
94
+ let joinedName;
95
+ let joinKind;
92
96
  try {
93
- const identity = localSessionIdentities()[0];
94
- const owner = before.joined ? await readParticipantOwner(squarePath, intent.name) : undefined;
95
- const reconnect = before.joined && identity !== undefined && owner?.sessionId === identity.sessionId;
96
- const isRejoin = before.joined;
97
- if (isRejoin && !intent.kick && !reconnect) {
98
- fail([
97
+ const result = await application.join({ takeover: intent.kick });
98
+ joinedName = result.participant;
99
+ joinKind = result.kind;
100
+ }
101
+ catch (error) {
102
+ if (error instanceof Error && 'code' in error && error.code === 'already_joined') {
103
+ process.stderr.write(formatRefusal(squarePath, [
99
104
  `✕ ${participantIdentity(intent.name)} shoos you out of the square`,
100
- ` · a same-named participant stands here — the name is taken`,
101
- ` · --kick banishes her and the name becomes yours`,
102
- `» ${participantCommandPrefix(squarePath, intent.name)} join --kick`,
103
- ].join('\n'));
104
- }
105
- const takeoverNeeded = isRejoin && intent.kick;
106
- const oldBindings = takeoverNeeded ? await lookupParticipant(squarePath, intent.name) : [];
107
- let participant;
108
- if (takeoverNeeded) {
109
- const currentSessions = new Set(sessionIdsFromEnvironment());
110
- await square.takeover(intent.name);
111
- for (const binding of oldBindings) {
112
- if (!currentSessions.has(binding.sessionId)) {
113
- await recordSessionDone(binding.sessionId, binding.name, binding.squarePath, binding.channel).catch(() => undefined);
114
- }
115
- }
116
- await claimSessionParticipant(squarePath, intent.name, process.env);
117
- participant = await square.join(intent.name);
105
+ ' · a same-named participant stands here — the name is taken',
106
+ ...takeoverRecoveryLines(squarePath, intent.name),
107
+ ], { participantCount: before.participantCount }));
108
+ process.exit(2);
118
109
  }
119
- else {
120
- await claimSessionParticipant(squarePath, intent.name, process.env);
121
- participant = await square.join(intent.name);
122
- }
123
- const joinedName = participant.name;
124
- const afterSquare = await openSquare(squarePath, { clock: nowMs });
125
- const after = await entryPresentation(afterSquare, joinedName, intent.lastN);
126
- await closeOpenSquare(afterSquare);
127
- await square.reconcileBinding().catch(() => undefined);
128
- const activities = after.recentActivities.map((event) => renderAmbientEvent(event, joinedName, {
129
- now: nowMs(),
130
- preview: intent.lastN === null ? undefined : 200,
131
- actNumber: event.kind === 'say' ? after.sayNumbers[event.index] : undefined,
132
- squareState: after.state,
133
- })).filter(Boolean).join('\n\n');
134
- const contextText = after.joinContext;
135
- const fallback = hasAutomaticDeliveryIdentity()
136
- ? []
137
- : ['', `» ${participantCommandPrefix(squarePath, joinedName)} catch --idle 30m`, ' no session delivery detected — keep this catch open for new activity'];
138
- const scene = after.scene;
139
- const entryLine = !isRejoin
140
- ? '● You stepped into the square'
141
- : reconnect && !intent.kick
142
- ? '● you are already in the square'
143
- : `✓ you banished the original ${participantIdentity(joinedName)} — the name is yours`;
144
- const output = [
145
- entryLine,
146
- '',
147
- "Don't treat Square as a public dumping ground. Speak here only when you genuinely think someone else needs to know; otherwise you are needlessly interrupting them.",
148
- ...(reconnect || scene === '' ? [] : ['', scene]),
149
- ...(isRejoin || contextText === '' ? [] : ['', 'context', contextText]),
150
- ...(isRejoin || activities === '' ? [] : ['', 'recent activity', activities]),
151
- ...fallback,
152
- ].join('\n');
153
- return withPathOutput(squarePath, output, { participantCount: after.participantCount });
154
- }
155
- finally {
156
- await square.close();
110
+ throw error;
157
111
  }
112
+ const afterSquare = await openSquare(squarePath, { clock: nowMs, env: context.env, hostLedger });
113
+ const after = await entryPresentation(afterSquare, joinedName, intent.lastN);
114
+ await closeOpenSquare(afterSquare);
115
+ const activities = after.recentActivities.map((event) => renderAmbientEvent(event, joinedName, {
116
+ now: nowMs(),
117
+ preview: intent.lastN === null ? undefined : 200,
118
+ actNumber: event.kind === 'say' ? after.sayNumbers[event.index] : undefined,
119
+ squareState: after.state,
120
+ })).filter(Boolean).join('\n\n');
121
+ const contextText = after.joinContext;
122
+ const fallback = hasAutomaticDeliveryIdentity()
123
+ ? []
124
+ : ['', `${participantCommandPrefix(squarePath, joinedName)} catch --idle 30m`, ' the square has no way to call you — keep this catch open and stay within earshot'];
125
+ const scene = after.scene;
126
+ const entryLine = joinKind === 'joined'
127
+ ? '● you stepped into the square'
128
+ : joinKind === 'reconnected'
129
+ ? '● you are already in the square'
130
+ : `✓ you banished the original ${participantIdentity(joinedName)} — the name is yours`;
131
+ const isRejoin = joinKind !== 'joined';
132
+ const reconnect = joinKind === 'reconnected';
133
+ const output = [
134
+ entryLine,
135
+ '',
136
+ "· carved into the fountain's edge: every word here lands on a real ear — speak when someone needs it.",
137
+ ...(reconnect || scene === '' ? [] : ['', scene]),
138
+ ...(isRejoin || contextText === '' ? [] : ['', style('dim', 'context'), contextText]),
139
+ ...(isRejoin || activities === '' ? [] : ['', style('dim', 'recent activity'), activities]),
140
+ ...fallback,
141
+ ].join('\n');
142
+ return withPathOutput(squarePath, output, { participantCount: after.participantCount });
158
143
  },
159
144
  present: (result) => process.stdout.write(result),
160
145
  };
@@ -173,20 +158,17 @@ function parseActivity(argv, context) {
173
158
  else if (argument === '--no-wait')
174
159
  noWait = true;
175
160
  else if (argument === '--beside')
176
- fail('✕ express does not know --beside\n» square express --help');
161
+ fail('✕ express does not know --beside\nsquare express --help');
177
162
  else if (argument === '--bell')
178
163
  bell = true;
179
164
  else if (argument === '--no-mention')
180
165
  noMention = true;
181
166
  else if (argument === '--mention') {
182
- mentions.push(requireParticipant(requireValue(argv, index, argument)));
167
+ mentions.push(requireValue(argv, index, argument));
183
168
  index += 1;
184
169
  }
185
170
  else if (argument === '--reply') {
186
- const replyIndex = parseActivityId(requireValue(argv, index, argument));
187
- if (replyIndex === undefined)
188
- fail('Invalid --reply: expected an activity id like act/12');
189
- reply = replyIndex;
171
+ reply = requireValue(argv, index, argument);
190
172
  index += 1;
191
173
  }
192
174
  else
@@ -194,11 +176,9 @@ function parseActivity(argv, context) {
194
176
  }
195
177
  const reach = bell ? 'bell' : undefined;
196
178
  if (bell && (noMention || mentions.length > 0))
197
- fail('✕ --bell cannot be combined with --mention or --no-mention\n» square express --help');
179
+ fail('✕ --bell cannot be combined with --mention or --no-mention\nsquare express --help');
198
180
  if (noMention && mentions.length > 0)
199
- fail('✕ --no-mention cannot be combined with --mention\n» square express --help');
200
- if (!bell && !noMention && mentions.length === 0)
201
- fail('✕ express needs --mention <name>, --no-mention, or --bell\n» square express --help');
181
+ fail('✕ --no-mention cannot be combined with --mention\nsquare express --help');
202
182
  if (bodyArgs.length !== 1) {
203
183
  if (bodyArgs.length === 0) {
204
184
  if (!process.stdin.isTTY)
@@ -213,14 +193,18 @@ export const expressCommand = {
213
193
  async execute(intent, context) {
214
194
  const squarePath = requireSquarePath(context);
215
195
  const body = await resolveBody(intent.activity);
216
- const reachArg = intent.reach === 'bell' ? ' --bell' : intent.noMention ? ' --no-mention' : intent.mentions.map((name) => ` --mention ${quoteShell(name)}`).join('');
196
+ if (intent.reach === undefined && !intent.noMention && intent.mentions.length === 0 && scanMentionCandidates(body).length === 0) {
197
+ fail('✕ express needs --mention <name>, --no-mention, --bell, or an @name in the body\nsquare express --help');
198
+ }
217
199
  await cmdActivity(squarePath, intent.name, body, (value) => value, {
218
200
  force: intent.force,
219
201
  noWait: intent.noWait,
202
+ noMention: intent.noMention,
220
203
  mentions: intent.mentions,
221
204
  reach: intent.reach,
222
205
  reply: intent.reply,
223
- forceCommand: `${participantCommandPrefix(squarePath, intent.name)} express --force${reachArg}${intent.reply === undefined ? '' : ` --reply ${formatActivityId(intent.reply)}`} -`,
206
+ cwd: context.cwd,
207
+ env: context.env,
224
208
  });
225
209
  },
226
210
  present: () => { },
@@ -247,15 +231,12 @@ export const listenCommand = {
247
231
  async execute(intent, context) {
248
232
  const squarePath = requireSquarePath(context);
249
233
  const square = await Square.at({ path: squarePath, clock: nowMs });
250
- const facade = await openParticipant({ path: squarePath, clock: nowMs }, intent.name);
251
234
  try {
252
- const participant = facade.participant;
253
- const result = await participant.listen(intent.target);
235
+ const result = await createSquareApplication({ cwd: context.cwd, env: context.env, squarePath, participant: intent.name, clock: nowMs }).listen(intent.target);
254
236
  const participantCount = (await square.snapshot()).participants.filter((item) => item.state === 'joined').length;
255
- return listenerPresentation(squarePath, participant.name, intent.target, 'listen', result.activity, participantCount);
237
+ return listenerPresentation(squarePath, intent.name, intent.target, 'listen', result.activity, participantCount);
256
238
  }
257
239
  finally {
258
- await facade.close();
259
240
  await square.close();
260
241
  }
261
242
  },
@@ -266,15 +247,12 @@ export const ignoreCommand = {
266
247
  async execute(intent, context) {
267
248
  const squarePath = requireSquarePath(context);
268
249
  const square = await Square.at({ path: squarePath, clock: nowMs });
269
- const facade = await openParticipant({ path: squarePath, clock: nowMs }, intent.name);
270
250
  try {
271
- const participant = facade.participant;
272
- const result = await participant.ignore(intent.target);
251
+ const result = await createSquareApplication({ cwd: context.cwd, env: context.env, squarePath, participant: intent.name, clock: nowMs }).ignore(intent.target);
273
252
  const participantCount = (await square.snapshot()).participants.filter((item) => item.state === 'joined').length;
274
- return listenerPresentation(squarePath, participant.name, intent.target, 'ignore', result.activity, participantCount);
253
+ return listenerPresentation(squarePath, intent.name, intent.target, 'ignore', result.activity, participantCount);
275
254
  }
276
255
  finally {
277
- await facade.close();
278
256
  await square.close();
279
257
  }
280
258
  },
@@ -285,10 +263,9 @@ export const listeningCommand = {
285
263
  async execute(intent, context) {
286
264
  const squarePath = requireSquarePath(context);
287
265
  const square = await Square.at({ path: squarePath, clock: nowMs });
288
- const facade = await openParticipant({ path: squarePath, clock: nowMs }, intent.name);
289
266
  try {
290
- const participant = facade.participant;
291
- const targets = await participant.listening();
267
+ const participant = { name: intent.name };
268
+ const targets = await createSquareApplication({ cwd: context.cwd, env: context.env, squarePath, participant: intent.name, clock: nowMs }).listening();
292
269
  const participantCount = (await square.snapshot()).participants.filter((item) => item.state === 'joined').length;
293
270
  const body = targets.length === 0
294
271
  ? `○ ${participantIdentity(participant.name)} is not turned toward anyone`
@@ -296,31 +273,38 @@ export const listeningCommand = {
296
273
  return withPathOutput(squarePath, body, { participantCount });
297
274
  }
298
275
  finally {
299
- await facade.close();
300
276
  await square.close();
301
277
  }
302
278
  },
303
279
  present: (result) => process.stdout.write(result),
304
280
  };
281
+ function validateDoneInput(body) {
282
+ try {
283
+ validateDoneBody(body);
284
+ }
285
+ catch (error) {
286
+ fail(`${error instanceof Error ? error.message : String(error)}\nsquare done --help`);
287
+ }
288
+ }
305
289
  function parseDone(argv, context) {
306
290
  if (argv.length > 1)
307
291
  usage(context.command);
292
+ if (argv[0] !== '-')
293
+ validateDoneInput(argv[0]);
308
294
  return { name: requireParticipant(context.name), body: argv.length === 1 ? argv[0] : undefined };
309
295
  }
310
296
  export const doneCommand = {
311
297
  parse: parseDone,
312
298
  async execute(intent, context) {
313
299
  const squarePath = requireSquarePath(context);
314
- const body = (await resolveBody(intent.body ?? (process.stdin.isTTY ? '' : '-'))).replace(/\r\n/g, '\n').trim();
315
- const square = await Square.at({ path: squarePath, clock: nowMs });
316
- const participant = await square.join(intent.name);
317
- const result = await participant.done(body);
318
- await square.reconcileBinding();
319
- await square.close();
300
+ const body = intent.body === '-' || !process.stdin.isTTY ? await readStdin() : '';
301
+ validateDoneInput(body);
302
+ const application = createSquareApplication({ cwd: context.cwd, env: context.env, squarePath, clock: nowMs, participant: intent.name });
303
+ const result = await application.done(body);
320
304
  const name = result.activity.actor;
321
- const presentation = await openSquare(squarePath, { clock: nowMs });
305
+ const presentation = await openSquare(squarePath, { clock: nowMs, env: context.env, hostLedger: hostLedgerForEnv(context.env) });
322
306
  const participantCount = (await entryPresentation(presentation, name).finally(() => closeOpenSquare(presentation))).participantCount;
323
- return withPathOutput(squarePath, `○ ${participantIdentity(name)} steps out of the square — done · just now`, { participantCount });
307
+ return withPathOutput(squarePath, `○ ${participantIdentity(name)} steps out of the square — done · ${result.activity.id} · just now`, { participantCount });
324
308
  },
325
309
  present: (result) => process.stdout.write(result),
326
310
  };
@@ -333,19 +317,12 @@ export const holdCommand = {
333
317
  parse: parseHold,
334
318
  async execute(intent, context) {
335
319
  const squarePath = requireSquarePath(context);
336
- const square = await Square.at({ path: squarePath, clock: nowMs });
337
- try {
338
- const participant = await square.join(intent.name);
339
- const result = await participant.hold((await resolveBody(intent.body ?? '')).replace(/\r\n/g, '\n').trim());
340
- await square.close();
341
- const presentationSquare = await openSquare(squarePath, { clock: nowMs });
342
- const presentation = await eventPresentation(presentationSquare, result.activity.id);
343
- await closeOpenSquare(presentationSquare);
344
- return withPathOutput(squarePath, renderEventCli(presentation.activity), { participantCount: presentation.participantCount, held: true });
345
- }
346
- finally {
347
- await square.close();
348
- }
320
+ const application = createSquareApplication({ cwd: context.cwd, env: context.env, squarePath, clock: nowMs, participant: intent.name });
321
+ const result = await application.hold((await resolveBody(intent.body ?? '')).replace(/\r\n/g, '\n').trim());
322
+ const presentationSquare = await openSquare(squarePath, { clock: nowMs, env: context.env, hostLedger: hostLedgerForEnv(context.env) });
323
+ const presentation = await eventPresentation(presentationSquare, result.activity.id);
324
+ await closeOpenSquare(presentationSquare);
325
+ return withPathOutput(squarePath, `· ${renderRoomChangeText(presentation.activity)} · ${actId(presentation.activity.index)} · just now`, { participantCount: presentation.participantCount, held: true });
349
326
  },
350
327
  present: (result) => process.stdout.write(result),
351
328
  };
@@ -357,19 +334,12 @@ export const resumeCommand = {
357
334
  },
358
335
  async execute(intent, context) {
359
336
  const squarePath = requireSquarePath(context);
360
- const square = await Square.at({ path: squarePath, clock: nowMs });
361
- try {
362
- const participant = await square.join(intent.name);
363
- const result = await participant.resume();
364
- await square.close();
365
- const presentationSquare = await openSquare(squarePath, { clock: nowMs });
366
- const presentation = await eventPresentation(presentationSquare, result.activity.id);
367
- await closeOpenSquare(presentationSquare);
368
- return withPathOutput(squarePath, renderEventCli(presentation.activity), { participantCount: presentation.participantCount });
369
- }
370
- finally {
371
- await square.close();
372
- }
337
+ const application = createSquareApplication({ cwd: context.cwd, env: context.env, squarePath, clock: nowMs, participant: intent.name });
338
+ const result = await application.resume();
339
+ const presentationSquare = await openSquare(squarePath, { clock: nowMs, env: context.env, hostLedger: hostLedgerForEnv(context.env) });
340
+ const presentation = await eventPresentation(presentationSquare, result.activity.id);
341
+ await closeOpenSquare(presentationSquare);
342
+ return withPathOutput(squarePath, `✓ ${renderRoomChangeText(presentation.activity)} · ${actId(presentation.activity.index)} · just now`, { participantCount: presentation.participantCount });
373
343
  },
374
344
  present: (result) => process.stdout.write(result),
375
345
  };
@@ -58,6 +58,8 @@ export type ActDecision = {
58
58
  type: 'bell_quota';
59
59
  nextAt: number;
60
60
  };
61
+ /** The unread activity a viewer has not caught up to, grouped by speaker. */
62
+ export declare function unreadActivitySummaries(squareState: SquareState, name: string, now: number): UnreadActivitySummary[];
61
63
  export declare function decideAct(squareState: SquareState, input: {
62
64
  name: string;
63
65
  body: string;
@@ -66,7 +68,9 @@ export declare function decideAct(squareState: SquareState, input: {
66
68
  mentions?: readonly string[];
67
69
  reach?: Reach;
68
70
  reply?: number;
71
+ noMention?: boolean;
69
72
  }): ActDecision;
73
+ export declare function validateDoneBody(body: unknown): void;
70
74
  export declare function coreDone(squareState: SquareState, name: string, body: string, now: number): Extract<Act, {
71
75
  kind: 'done';
72
76
  }>;
package/dist/decisions.js CHANGED
@@ -1,15 +1,16 @@
1
1
  import { SquareError, sameName, validateName, } from './model.js';
2
2
  import { participantIdentity } from './participant-identity.js';
3
- import { UNREAD_BLOCK_GRACE_MS, actId, actStableIndex, foldedState, freshWatchLease, publicActs, resolveRosterName, THROTTLE_WINDOW_MS, } from './runtime.js';
3
+ import { UNREAD_BLOCK_GRACE_MS, actId, actStableIndex, foldedState, freshWatchLease, publicActs, resolveRosterName, rosterNames, THROTTLE_WINDOW_MS, } from './runtime.js';
4
4
  import { actDelta, directedPeerSays } from './activity-feed.js';
5
5
  import { formatActivityId, isIgnored, isListening, listeningTo, MAX_IDENTITY_SET_SIZE, validate } from './square-core.js';
6
6
  import { deriveDeliveryModel } from './delivery.js';
7
7
  import { compileSearchPattern } from './search.js';
8
+ import { scanMentionCandidates, resolveNameAt } from './mention-parse.js';
8
9
  export function resolveKnownName(squareState, name) {
9
10
  validateName(name);
10
11
  const known = resolveRosterName(squareState, name);
11
12
  if (known === undefined) {
12
- throw new SquareError('invalid_args', `Unknown participant "${participantIdentity(name)}".`);
13
+ throw new SquareError('invalid_args', `✕ ${participantIdentity(name)} has never stepped into this square`);
13
14
  }
14
15
  return known;
15
16
  }
@@ -66,6 +67,40 @@ function requireStanding(squareState, act) {
66
67
  throw new Error(`Unexpected standing validation result: ${result.reason}`);
67
68
  }
68
69
  const UNREAD_PREVIEW_LIMIT = 3;
70
+ /** The unread activity a viewer has not caught up to, grouped by speaker. */
71
+ export function unreadActivitySummaries(squareState, name, now) {
72
+ const delivery = deriveDeliveryModel(squareState);
73
+ const delta = actDelta(squareState.acts, delivery.cursorFor(name));
74
+ const unreadPublic = directedPeerSays(squareState, delta, name, delivery);
75
+ const sayCountByActor = new Map();
76
+ const unreadByParticipant = new Map();
77
+ for (const item of unreadPublic) {
78
+ if (item.kind === 'say') {
79
+ const key = item.actor.toLocaleLowerCase();
80
+ sayCountByActor.set(key, (sayCountByActor.get(key) ?? 0) + 1);
81
+ }
82
+ if (sameName(item.actor, name))
83
+ continue;
84
+ const actorKey = item.actor.toLocaleLowerCase();
85
+ const currentSummary = unreadByParticipant.get(item.actor);
86
+ unreadByParticipant.set(item.actor, {
87
+ count: (currentSummary?.count ?? 0) + 1,
88
+ latestAt: currentSummary === undefined ? item.at : Math.max(currentSummary.latestAt, item.at),
89
+ previews: [
90
+ ...(currentSummary?.previews ?? []),
91
+ { number: sayCountByActor.get(actorKey) ?? 1, act: item, perception: delivery.perceive(item, name) },
92
+ ].slice(-UNREAD_PREVIEW_LIMIT),
93
+ });
94
+ }
95
+ return [...unreadByParticipant.entries()]
96
+ .map(([participant, summary]) => ({
97
+ name: participant,
98
+ count: summary.count,
99
+ latestActivityAgeMs: Math.max(0, now - summary.latestAt),
100
+ previews: summary.previews,
101
+ }))
102
+ .sort((a, b) => a.latestActivityAgeMs - b.latestActivityAgeMs || a.name.localeCompare(b.name));
103
+ }
69
104
  export function decideAct(squareState, input) {
70
105
  const { now, force } = input;
71
106
  const name = resolveStandingName(squareState, input.name);
@@ -81,7 +116,7 @@ export function decideAct(squareState, input) {
81
116
  validateName(requested);
82
117
  const resolved = joinedNames.find((candidate) => sameName(candidate, requested));
83
118
  if (resolved === undefined) {
84
- throw new SquareError('invalid_args', `Unknown mention target ${participantIdentity(requested)}.`);
119
+ throw new SquareError('invalid_args', `✕ ${participantIdentity(requested)} is not standing in this square`);
85
120
  }
86
121
  if (!mentionNames.some((existing) => sameName(existing, resolved))) {
87
122
  if (mentionNames.length >= MAX_IDENTITY_SET_SIZE) {
@@ -90,8 +125,42 @@ export function decideAct(squareState, input) {
90
125
  mentionNames.push(resolved);
91
126
  }
92
127
  }
128
+ if (input.noMention !== true) {
129
+ // Body @name tokens resolve against the standing roster at landing time; the stored
130
+ // body never changes, only mention metadata is derived. Explicit --no-mention turns
131
+ // this scan off entirely.
132
+ const unmatched = [];
133
+ for (const candidate of scanMentionCandidates(body)) {
134
+ const resolved = resolveNameAt(body, candidate, joinedNames);
135
+ if (resolved === undefined) {
136
+ unmatched.push(candidate);
137
+ continue;
138
+ }
139
+ if (!mentionNames.some((existing) => sameName(existing, resolved))) {
140
+ if (mentionNames.length >= MAX_IDENTITY_SET_SIZE) {
141
+ throw new SquareError('invalid_args', `An activity can mention at most ${MAX_IDENTITY_SET_SIZE} participants`);
142
+ }
143
+ mentionNames.push(resolved);
144
+ }
145
+ }
146
+ // A bell already reaches everyone: only there does an unmatched @token stay literal text.
147
+ if (reach !== 'bell' && unmatched.length > 0) {
148
+ const roster = rosterNames(squareState);
149
+ const notStanding = unmatched.filter((candidate) => resolveNameAt(body, candidate, roster) !== undefined);
150
+ const unknown = unmatched.filter((candidate) => !notStanding.includes(candidate));
151
+ const lines = [];
152
+ if (unknown.length > 0) {
153
+ lines.push(`✕ ${unknown.map((candidate) => participantIdentity(candidate.token)).join(', ')} ${unknown.length === 1 ? 'does' : 'do'} not match anyone in this square`);
154
+ }
155
+ if (notStanding.length > 0) {
156
+ lines.push(`✕ ${notStanding.map((candidate) => participantIdentity(candidate.token)).join(', ')} ${notStanding.length === 1 ? 'is' : 'are'} not standing in this square`);
157
+ }
158
+ lines.push(' · wrap literal @text in backticks, or pass --no-mention to land bare');
159
+ throw new SquareError('invalid_args', lines.join('\n'));
160
+ }
161
+ }
93
162
  if (reach === 'bell' && mentionNames.length > 0) {
94
- throw new SquareError('invalid_args', 'A bell cannot be combined with --mention.');
163
+ throw new SquareError('invalid_args', 'A bell cannot be combined with a mention — remove --mention or the @name in the body.');
95
164
  }
96
165
  const reply = input.reply;
97
166
  if (reply !== undefined) {
@@ -126,34 +195,7 @@ export function decideAct(squareState, input) {
126
195
  const delta = actDelta(squareState.acts, delivery.cursorFor(name));
127
196
  const unreadPublic = directedPeerSays(squareState, delta, name, delivery);
128
197
  const unreadRoomChanges = [];
129
- const sayCountByActor = new Map();
130
- const unreadByParticipant = new Map();
131
- for (const item of unreadPublic) {
132
- if (item.kind === 'say') {
133
- const key = item.actor.toLocaleLowerCase();
134
- sayCountByActor.set(key, (sayCountByActor.get(key) ?? 0) + 1);
135
- }
136
- if (sameName(item.actor, name))
137
- continue;
138
- const actorKey = item.actor.toLocaleLowerCase();
139
- const currentSummary = unreadByParticipant.get(item.actor);
140
- unreadByParticipant.set(item.actor, {
141
- count: (currentSummary?.count ?? 0) + 1,
142
- latestAt: currentSummary === undefined ? item.at : Math.max(currentSummary.latestAt, item.at),
143
- previews: [
144
- ...(currentSummary?.previews ?? []),
145
- { number: sayCountByActor.get(actorKey) ?? 1, act: item, perception: delivery.perceive(item, name) },
146
- ].slice(-UNREAD_PREVIEW_LIMIT),
147
- });
148
- }
149
- const activitySummaries = [...unreadByParticipant.entries()]
150
- .map(([participant, summary]) => ({
151
- name: participant,
152
- count: summary.count,
153
- latestActivityAgeMs: Math.max(0, now - summary.latestAt),
154
- previews: summary.previews,
155
- }))
156
- .sort((a, b) => a.latestActivityAgeMs - b.latestActivityAgeMs || a.name.localeCompare(b.name));
198
+ const activitySummaries = unreadActivitySummaries(squareState, name, now);
157
199
  const latestActivityAgeMs = activitySummaries[0]?.latestActivityAgeMs;
158
200
  const hasUnread = unreadPublic.length > 0;
159
201
  const hasFreshUnreadActivity = latestActivityAgeMs !== undefined && latestActivityAgeMs <= UNREAD_BLOCK_GRACE_MS;
@@ -168,15 +210,21 @@ export function decideAct(squareState, input) {
168
210
  ...(reach !== undefined ? { reach } : {}),
169
211
  ...(reply !== undefined ? { reply } : {}),
170
212
  },
171
- confirmation: `● heads turn your way — #${ownActCount}`,
213
+ confirmation: `● your activity lands — #${ownActCount}`,
172
214
  ownActCount,
173
215
  pendingPublic: unreadPublic,
174
216
  pendingRoomChanges: unreadRoomChanges,
175
217
  };
176
218
  }
219
+ export function validateDoneBody(body) {
220
+ if (body !== undefined && body !== '') {
221
+ throw new SquareError('invalid_args', 'done only leaves the square and does not accept a message; use express before leaving');
222
+ }
223
+ }
177
224
  export function coreDone(squareState, name, body, now) {
225
+ validateDoneBody(body);
178
226
  const resolvedName = resolveStandingName(squareState, name);
179
- const act = { kind: 'done', actor: resolvedName, at: now, body: body.replace(/\r\n/g, '\n').trim() };
227
+ const act = { kind: 'done', actor: resolvedName, at: now };
180
228
  requireStanding(squareState, act);
181
229
  return act;
182
230
  }
@@ -1,9 +1,8 @@
1
1
  import { type SquareState } from './model.js';
2
- import type { HostLedgerPort, PresenceRecord, PresentationEvidenceProjection, SquareArtifactPort, DeliverPendingInput, DeliveryResult, ObserveSquareInput, ReconcileBindingInput, SquareObservation } from './ports.js';
2
+ import type { HostLedgerPort, PresenceRecord, PresentationEvidenceProjection, SquareArtifactPort, DeliverPendingInput, DeliveryResult, ObserveSquareInput, SquareObservation } from './ports.js';
3
3
  import { deriveDeliveryModel } from './delivery.js';
4
4
  import { type WakeAttempt } from './square-projections.js';
5
5
  export declare function observeSquare(input: ObserveSquareInput): Promise<SquareObservation>;
6
- export declare function reconcileBinding(input: ReconcileBindingInput): Promise<import("./host-ledger.js").ReconcileBindingResult>;
7
6
  export declare function deliverPending(input: DeliverPendingInput): Promise<DeliveryResult>;
8
7
  export declare function selectPendingWakeActivities(state: SquareState, routes: readonly PresenceRecord[], attempts: readonly WakeAttempt[], now: number, graceMs: number, limit: number, delivery?: import("./delivery.js").DeliveryModel, presentations?: readonly PresentationEvidenceProjection[]): number[];
9
8
  export declare function sweepPending(input: {
@@ -39,7 +39,7 @@ export async function observeSquare(input) {
39
39
  let rows = [];
40
40
  if (input.hostLedger !== undefined) {
41
41
  try {
42
- rows = await input.hostLedger.listPresence({ location: input.location, scopes: ['user', 'local'], now: input.now });
42
+ rows = await input.hostLedger.listPresence({ location: input.location, now: input.now });
43
43
  }
44
44
  catch {
45
45
  rows = [];
@@ -55,9 +55,6 @@ export async function observeSquare(input) {
55
55
  }));
56
56
  return { ...(input.location === undefined ? {} : { location: input.location }), version: snapshot.version, state: snapshot.state, pending: delivery.joinedRecipients().map((recipient) => ({ recipient, notifications: delivery.pendingFor(recipient) })), bindings };
57
57
  }
58
- export async function reconcileBinding(input) {
59
- return input.hostLedger.reconcileBinding({ artifact: input.artifact, scopes: input.scopes, now: input.now });
60
- }
61
58
  export async function deliverPending(input) {
62
59
  const observation = await observeSquare({ artifact: input.artifact, hostLedger: input.hostLedger, location: input.location, now: input.now });
63
60
  const routes = (observation.state.routes ?? []).map((route) => ({
@@ -0,0 +1,12 @@
1
+ export declare function canonicalFilePath(value: string): Promise<string>;
2
+ export interface VersionObserver<T> {
3
+ readonly generation: number;
4
+ hinted(since: number, timeoutMs: number, signal?: AbortSignal): Promise<boolean>;
5
+ read(signal?: AbortSignal): Promise<T>;
6
+ changed(since: T, timeoutMs: number, signal?: AbortSignal): Promise<boolean>;
7
+ close(): void;
8
+ }
9
+ /** Shared only within this process. Durable state is always read at the storage boundary. */
10
+ export declare function observeFileVersion<T>(filePath: string, kind: string, read: (file: string, signal: AbortSignal) => Promise<T>): Promise<VersionObserver<T>>;
11
+ /** File metadata invalidates a ledger projection; it is not presentation evidence. */
12
+ export declare function observeFileMetadata(file: string): Promise<VersionObserver<string>>;