@astrosheep/square 0.3.61 → 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 (87) 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 +168 -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 +8 -6
  10. package/dist/automatic-session.js +14 -24
  11. package/dist/boundary-presentation.d.ts +1 -2
  12. package/dist/boundary-presentation.js +4 -19
  13. package/dist/cli/context.d.ts +2 -0
  14. package/dist/cli/context.js +2 -18
  15. package/dist/cli/harness-command.js +1 -1
  16. package/dist/cli/mcp-server-command.d.ts +2 -0
  17. package/dist/cli/mcp-server-command.js +13 -0
  18. package/dist/cli/observation-commands.js +47 -19
  19. package/dist/cli/program.js +5 -2
  20. package/dist/cli/registry.js +2 -0
  21. package/dist/cli/square-commands.d.ts +1 -1
  22. package/dist/cli/square-commands.js +93 -125
  23. package/dist/decisions.d.ts +2 -0
  24. package/dist/decisions.js +44 -3
  25. package/dist/delivery-operations.d.ts +1 -2
  26. package/dist/delivery-operations.js +1 -4
  27. package/dist/file-changes.d.ts +12 -0
  28. package/dist/file-changes.js +235 -0
  29. package/dist/help.js +4 -8
  30. package/dist/host-ledger-file-adapter.d.ts +15 -13
  31. package/dist/host-ledger-file-adapter.js +131 -79
  32. package/dist/host-ledger-root.d.ts +2 -0
  33. package/dist/host-ledger-root.js +9 -0
  34. package/dist/host-ledger.d.ts +4 -24
  35. package/dist/inbox.d.ts +10 -5
  36. package/dist/inbox.js +152 -67
  37. package/dist/index.d.ts +3 -1
  38. package/dist/index.js +1 -0
  39. package/dist/mcp.d.ts +7 -0
  40. package/dist/mcp.js +122 -0
  41. package/dist/mention-parse.d.ts +8 -0
  42. package/dist/mention-parse.js +107 -0
  43. package/dist/model.d.ts +5 -0
  44. package/dist/model.js +4 -1
  45. package/dist/notifications.d.ts +1 -0
  46. package/dist/notifications.js +25 -20
  47. package/dist/participant-host.js +2 -2
  48. package/dist/ports.d.ts +2 -9
  49. package/dist/presentation.d.ts +12 -1
  50. package/dist/presentation.js +42 -12
  51. package/dist/presented.js +2 -1
  52. package/dist/registry.d.ts +8 -4
  53. package/dist/registry.js +41 -74
  54. package/dist/routes.d.ts +19 -2
  55. package/dist/routes.js +48 -5
  56. package/dist/runtime.d.ts +2 -0
  57. package/dist/runtime.js +2 -0
  58. package/dist/square-actions.d.ts +26 -13
  59. package/dist/square-actions.js +200 -84
  60. package/dist/square-application.d.ts +58 -0
  61. package/dist/square-application.js +183 -0
  62. package/dist/square-facade.d.ts +24 -9
  63. package/dist/square-file-adapter.d.ts +5 -12
  64. package/dist/square-file-adapter.js +12 -64
  65. package/dist/square-projections.d.ts +3 -2
  66. package/dist/square-projections.js +12 -3
  67. package/dist/square-storage.js +27 -34
  68. package/dist/square-wiring.d.ts +12 -7
  69. package/dist/square-wiring.js +36 -32
  70. package/dist/state-cell.d.ts +1 -1
  71. package/dist/stream.js +30 -26
  72. package/dist/views.d.ts +3 -3
  73. package/dist/views.js +4 -4
  74. package/dist/wake-attempts.js +2 -1
  75. package/dist/wake-evidence.js +2 -8
  76. package/dist/wakes.d.ts +1 -0
  77. package/dist/wakes.js +5 -5
  78. package/dist/watch.d.ts +6 -1
  79. package/dist/watch.js +20 -18
  80. package/extensions/square-pi.js +100 -112
  81. package/guides/application-boundary.md +32 -0
  82. package/guides/change-waits.md +28 -0
  83. package/guides/host-ledger.md +9 -0
  84. package/guides/mcp.md +9 -0
  85. package/guides/operation-cancellation.md +17 -0
  86. package/package.json +8 -2
  87. package/skills/square/SKILL.md +5 -7
@@ -1,13 +1,14 @@
1
1
  import { cmdActivity } from '../activity.js';
2
- import { formatActivityId, formatHardCap, parseActivityId, } from '../model.js';
3
- import { formatRefusal, participantCommandPrefix, participantIdentity, quoteShell, renderRoomChangeText, 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';
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
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';
13
14
  import { style } from '../tty-style.js';
@@ -85,78 +86,60 @@ export const joinCommand = {
85
86
  parse: parseJoin,
86
87
  async execute(intent, context) {
87
88
  const squarePath = requireSquarePath(context);
88
- 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 });
89
91
  const before = await entryPresentation(beforeSquare, intent.name, intent.lastN);
90
92
  await closeOpenSquare(beforeSquare);
91
- const hostLedger = createHostLedgerPort();
92
- 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;
93
96
  try {
94
- const identity = localSessionIdentities()[0];
95
- const owner = before.joined ? await readParticipantOwner(squarePath, intent.name) : undefined;
96
- const reconnect = before.joined && identity !== undefined && owner?.sessionId === identity.sessionId;
97
- const isRejoin = before.joined;
98
- if (isRejoin && !intent.kick && !reconnect) {
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') {
99
103
  process.stderr.write(formatRefusal(squarePath, [
100
104
  `✕ ${participantIdentity(intent.name)} shoos you out of the square`,
101
- ` · a same-named participant stands here — the name is taken`,
102
- ` · --kick banishes the one standing there — the name becomes yours`,
103
- `${participantCommandPrefix(squarePath, intent.name)} join --kick`,
105
+ ' · a same-named participant stands here — the name is taken',
106
+ ...takeoverRecoveryLines(squarePath, intent.name),
104
107
  ], { participantCount: before.participantCount }));
105
108
  process.exit(2);
106
109
  }
107
- const takeoverNeeded = isRejoin && intent.kick;
108
- const oldBindings = takeoverNeeded ? await lookupParticipant(squarePath, intent.name) : [];
109
- let participant;
110
- if (takeoverNeeded) {
111
- const currentSessions = new Set(sessionIdsFromEnvironment());
112
- await square.takeover(intent.name);
113
- for (const binding of oldBindings) {
114
- if (!currentSessions.has(binding.sessionId)) {
115
- await recordSessionDone(binding.sessionId, binding.name, binding.squarePath, binding.channel).catch(() => undefined);
116
- }
117
- }
118
- await claimSessionParticipant(squarePath, intent.name, process.env);
119
- participant = await square.join(intent.name);
120
- }
121
- else {
122
- await claimSessionParticipant(squarePath, intent.name, process.env);
123
- participant = await square.join(intent.name);
124
- }
125
- const joinedName = participant.name;
126
- const afterSquare = await openSquare(squarePath, { clock: nowMs });
127
- const after = await entryPresentation(afterSquare, joinedName, intent.lastN);
128
- await closeOpenSquare(afterSquare);
129
- await square.reconcileBinding().catch(() => undefined);
130
- const activities = after.recentActivities.map((event) => renderAmbientEvent(event, joinedName, {
131
- now: nowMs(),
132
- preview: intent.lastN === null ? undefined : 200,
133
- actNumber: event.kind === 'say' ? after.sayNumbers[event.index] : undefined,
134
- squareState: after.state,
135
- })).filter(Boolean).join('\n\n');
136
- const contextText = after.joinContext;
137
- const fallback = hasAutomaticDeliveryIdentity()
138
- ? []
139
- : ['', `${participantCommandPrefix(squarePath, joinedName)} catch --idle 30m`, ' the square has no way to call you — keep this catch open and stay within earshot'];
140
- const scene = after.scene;
141
- const entryLine = !isRejoin
142
- ? '● you stepped into the square'
143
- : reconnect && !intent.kick
144
- ? '● you are already in the square'
145
- : `✓ you banished the original ${participantIdentity(joinedName)} — the name is yours`;
146
- const output = [
147
- entryLine,
148
- '',
149
- "· carved into the fountain's edge: every word here lands on a real ear — speak when someone needs it.",
150
- ...(reconnect || scene === '' ? [] : ['', scene]),
151
- ...(isRejoin || contextText === '' ? [] : ['', style('dim', 'context'), contextText]),
152
- ...(isRejoin || activities === '' ? [] : ['', style('dim', 'recent activity'), activities]),
153
- ...fallback,
154
- ].join('\n');
155
- return withPathOutput(squarePath, output, { participantCount: after.participantCount });
156
- }
157
- finally {
158
- await square.close();
110
+ throw error;
159
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 });
160
143
  },
161
144
  present: (result) => process.stdout.write(result),
162
145
  };
@@ -181,14 +164,11 @@ function parseActivity(argv, context) {
181
164
  else if (argument === '--no-mention')
182
165
  noMention = true;
183
166
  else if (argument === '--mention') {
184
- mentions.push(requireParticipant(requireValue(argv, index, argument)));
167
+ mentions.push(requireValue(argv, index, argument));
185
168
  index += 1;
186
169
  }
187
170
  else if (argument === '--reply') {
188
- const replyIndex = parseActivityId(requireValue(argv, index, argument));
189
- if (replyIndex === undefined)
190
- fail('Invalid --reply: expected an activity id like act/12');
191
- reply = replyIndex;
171
+ reply = requireValue(argv, index, argument);
192
172
  index += 1;
193
173
  }
194
174
  else
@@ -199,8 +179,6 @@ function parseActivity(argv, context) {
199
179
  fail('✕ --bell cannot be combined with --mention or --no-mention\nsquare express --help');
200
180
  if (noMention && mentions.length > 0)
201
181
  fail('✕ --no-mention cannot be combined with --mention\nsquare express --help');
202
- if (!bell && !noMention && mentions.length === 0)
203
- fail('✕ express needs --mention <name>, --no-mention, or --bell\nsquare express --help');
204
182
  if (bodyArgs.length !== 1) {
205
183
  if (bodyArgs.length === 0) {
206
184
  if (!process.stdin.isTTY)
@@ -215,14 +193,18 @@ export const expressCommand = {
215
193
  async execute(intent, context) {
216
194
  const squarePath = requireSquarePath(context);
217
195
  const body = await resolveBody(intent.activity);
218
- 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
+ }
219
199
  await cmdActivity(squarePath, intent.name, body, (value) => value, {
220
200
  force: intent.force,
221
201
  noWait: intent.noWait,
202
+ noMention: intent.noMention,
222
203
  mentions: intent.mentions,
223
204
  reach: intent.reach,
224
205
  reply: intent.reply,
225
- forceCommand: `${participantCommandPrefix(squarePath, intent.name)} express --force${reachArg}${intent.reply === undefined ? '' : ` --reply ${formatActivityId(intent.reply)}`} -`,
206
+ cwd: context.cwd,
207
+ env: context.env,
226
208
  });
227
209
  },
228
210
  present: () => { },
@@ -249,15 +231,12 @@ export const listenCommand = {
249
231
  async execute(intent, context) {
250
232
  const squarePath = requireSquarePath(context);
251
233
  const square = await Square.at({ path: squarePath, clock: nowMs });
252
- const facade = await openParticipant({ path: squarePath, clock: nowMs }, intent.name);
253
234
  try {
254
- const participant = facade.participant;
255
- 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);
256
236
  const participantCount = (await square.snapshot()).participants.filter((item) => item.state === 'joined').length;
257
- return listenerPresentation(squarePath, participant.name, intent.target, 'listen', result.activity, participantCount);
237
+ return listenerPresentation(squarePath, intent.name, intent.target, 'listen', result.activity, participantCount);
258
238
  }
259
239
  finally {
260
- await facade.close();
261
240
  await square.close();
262
241
  }
263
242
  },
@@ -268,15 +247,12 @@ export const ignoreCommand = {
268
247
  async execute(intent, context) {
269
248
  const squarePath = requireSquarePath(context);
270
249
  const square = await Square.at({ path: squarePath, clock: nowMs });
271
- const facade = await openParticipant({ path: squarePath, clock: nowMs }, intent.name);
272
250
  try {
273
- const participant = facade.participant;
274
- 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);
275
252
  const participantCount = (await square.snapshot()).participants.filter((item) => item.state === 'joined').length;
276
- return listenerPresentation(squarePath, participant.name, intent.target, 'ignore', result.activity, participantCount);
253
+ return listenerPresentation(squarePath, intent.name, intent.target, 'ignore', result.activity, participantCount);
277
254
  }
278
255
  finally {
279
- await facade.close();
280
256
  await square.close();
281
257
  }
282
258
  },
@@ -287,10 +263,9 @@ export const listeningCommand = {
287
263
  async execute(intent, context) {
288
264
  const squarePath = requireSquarePath(context);
289
265
  const square = await Square.at({ path: squarePath, clock: nowMs });
290
- const facade = await openParticipant({ path: squarePath, clock: nowMs }, intent.name);
291
266
  try {
292
- const participant = facade.participant;
293
- 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();
294
269
  const participantCount = (await square.snapshot()).participants.filter((item) => item.state === 'joined').length;
295
270
  const body = targets.length === 0
296
271
  ? `○ ${participantIdentity(participant.name)} is not turned toward anyone`
@@ -298,29 +273,36 @@ export const listeningCommand = {
298
273
  return withPathOutput(squarePath, body, { participantCount });
299
274
  }
300
275
  finally {
301
- await facade.close();
302
276
  await square.close();
303
277
  }
304
278
  },
305
279
  present: (result) => process.stdout.write(result),
306
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
+ }
307
289
  function parseDone(argv, context) {
308
290
  if (argv.length > 1)
309
291
  usage(context.command);
292
+ if (argv[0] !== '-')
293
+ validateDoneInput(argv[0]);
310
294
  return { name: requireParticipant(context.name), body: argv.length === 1 ? argv[0] : undefined };
311
295
  }
312
296
  export const doneCommand = {
313
297
  parse: parseDone,
314
298
  async execute(intent, context) {
315
299
  const squarePath = requireSquarePath(context);
316
- const body = (await resolveBody(intent.body ?? (process.stdin.isTTY ? '' : '-'))).replace(/\r\n/g, '\n').trim();
317
- const square = await Square.at({ path: squarePath, clock: nowMs });
318
- const participant = await square.join(intent.name);
319
- const result = await participant.done(body);
320
- await square.reconcileBinding();
321
- 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);
322
304
  const name = result.activity.actor;
323
- const presentation = await openSquare(squarePath, { clock: nowMs });
305
+ const presentation = await openSquare(squarePath, { clock: nowMs, env: context.env, hostLedger: hostLedgerForEnv(context.env) });
324
306
  const participantCount = (await entryPresentation(presentation, name).finally(() => closeOpenSquare(presentation))).participantCount;
325
307
  return withPathOutput(squarePath, `○ ${participantIdentity(name)} steps out of the square — done · ${result.activity.id} · just now`, { participantCount });
326
308
  },
@@ -335,19 +317,12 @@ export const holdCommand = {
335
317
  parse: parseHold,
336
318
  async execute(intent, context) {
337
319
  const squarePath = requireSquarePath(context);
338
- const square = await Square.at({ path: squarePath, clock: nowMs });
339
- try {
340
- const participant = await square.join(intent.name);
341
- const result = await participant.hold((await resolveBody(intent.body ?? '')).replace(/\r\n/g, '\n').trim());
342
- await square.close();
343
- const presentationSquare = await openSquare(squarePath, { clock: nowMs });
344
- const presentation = await eventPresentation(presentationSquare, result.activity.id);
345
- await closeOpenSquare(presentationSquare);
346
- return withPathOutput(squarePath, `· ${renderRoomChangeText(presentation.activity)} · ${actId(presentation.activity.index)} · just now`, { participantCount: presentation.participantCount, held: true });
347
- }
348
- finally {
349
- await square.close();
350
- }
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 });
351
326
  },
352
327
  present: (result) => process.stdout.write(result),
353
328
  };
@@ -359,19 +334,12 @@ export const resumeCommand = {
359
334
  },
360
335
  async execute(intent, context) {
361
336
  const squarePath = requireSquarePath(context);
362
- const square = await Square.at({ path: squarePath, clock: nowMs });
363
- try {
364
- const participant = await square.join(intent.name);
365
- const result = await participant.resume();
366
- await square.close();
367
- const presentationSquare = await openSquare(squarePath, { clock: nowMs });
368
- const presentation = await eventPresentation(presentationSquare, result.activity.id);
369
- await closeOpenSquare(presentationSquare);
370
- return withPathOutput(squarePath, `✓ ${renderRoomChangeText(presentation.activity)} · ${actId(presentation.activity.index)} · just now`, { participantCount: presentation.participantCount });
371
- }
372
- finally {
373
- await square.close();
374
- }
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 });
375
343
  },
376
344
  present: (result) => process.stdout.write(result),
377
345
  };
@@ -68,7 +68,9 @@ export declare function decideAct(squareState: SquareState, input: {
68
68
  mentions?: readonly string[];
69
69
  reach?: Reach;
70
70
  reply?: number;
71
+ noMention?: boolean;
71
72
  }): ActDecision;
73
+ export declare function validateDoneBody(body: unknown): void;
72
74
  export declare function coreDone(squareState: SquareState, name: string, body: string, now: number): Extract<Act, {
73
75
  kind: 'done';
74
76
  }>;
package/dist/decisions.js CHANGED
@@ -1,10 +1,11 @@
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);
@@ -124,8 +125,42 @@ export function decideAct(squareState, input) {
124
125
  mentionNames.push(resolved);
125
126
  }
126
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
+ }
127
162
  if (reach === 'bell' && mentionNames.length > 0) {
128
- 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.');
129
164
  }
130
165
  const reply = input.reply;
131
166
  if (reply !== undefined) {
@@ -181,9 +216,15 @@ export function decideAct(squareState, input) {
181
216
  pendingRoomChanges: unreadRoomChanges,
182
217
  };
183
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
+ }
184
224
  export function coreDone(squareState, name, body, now) {
225
+ validateDoneBody(body);
185
226
  const resolvedName = resolveStandingName(squareState, name);
186
- 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 };
187
228
  requireStanding(squareState, act);
188
229
  return act;
189
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>>;