@astrosheep/square 0.3.11 → 0.3.12

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.
@@ -16,7 +16,7 @@ export function pendingAtBoundary(inbox) {
16
16
  return membership;
17
17
  return {
18
18
  ...membership,
19
- notifications: membership.notifications.filter((notification) => !leaseOwnsNotification(lease, { ...notification, recipient: membership.name })),
19
+ notifications: membership.notifications.filter((notification) => !leaseOwnsNotification(lease, notification)),
20
20
  };
21
21
  })
22
22
  .filter((membership) => membership.notifications.length > 0);
@@ -4,7 +4,7 @@ import path from 'node:path';
4
4
  import { loadSquare } from '../artifact.js';
5
5
  import { commandUsageHint } from '../help.js';
6
6
  import { parseParticipantList, validateName } from '../model.js';
7
- export const DEFAULT_SQUARE_PATH = '.square/SQUARE.md';
7
+ export const DEFAULT_SQUARE_PATH = '.square/SQUARE.square';
8
8
  export function readStdinSync() {
9
9
  try {
10
10
  return fs.readFileSync(0, 'utf8');
@@ -99,7 +99,7 @@ function resolveDefaultSquarePath() {
99
99
  }
100
100
  const candidates = [];
101
101
  for (const entry of entries) {
102
- if (!entry.isFile() || !entry.name.endsWith('.md'))
102
+ if (!entry.isFile() || !entry.name.endsWith('.square'))
103
103
  continue;
104
104
  const fullPath = path.join(directory, entry.name);
105
105
  try {
@@ -87,7 +87,7 @@ export function formatHarnessResult(result) {
87
87
  return `${[...result.notes, ...result.lines].join('\n')}\n`;
88
88
  }
89
89
  export function runHarnessCommand(argv, squarePath) {
90
- const context = { homeDir: os.homedir(), squarePath: squarePath ?? '.square/SQUARE.md', command: 'harness' };
90
+ const context = { homeDir: os.homedir(), squarePath: squarePath ?? '.square/SQUARE.square', command: 'harness' };
91
91
  const intent = harnessCommand.parse(argv, context);
92
92
  return Promise.resolve(harnessCommand.execute(intent, context)).then(formatHarnessResult);
93
93
  }
@@ -1,82 +1,25 @@
1
- import fs from 'node:fs';
2
- import { diagnoseSquare, loadSquare } from '../artifact.js';
3
- import { renderDoctorClean, renderDoctorProblems, renderDoctorRepaired, renderDoctorUnfixable, withPathOutput, } from '../presentation.js';
1
+ import { diagnoseSquareFile } from '../artifact.js';
2
+ import { renderDoctorClean, renderDoctorUnfixable, withPathOutput } from '../presentation.js';
4
3
  import { inSquareCount } from '../runtime.js';
5
- import { repairSquare } from '../square-application.js';
6
- import { SquareError } from '../model.js';
7
- import { pruneRegistry } from '../registry.js';
8
4
  import { usage } from './context.js';
9
- function readSquareText(squarePath) {
10
- try {
11
- return fs.readFileSync(squarePath, 'utf8');
12
- }
13
- catch (error) {
14
- if (error.code === 'ENOENT')
15
- throw new SquareError('not_found', `square file not found: ${squarePath}`);
16
- throw error;
17
- }
18
- }
19
- function quarantinePath(squarePath) {
20
- return squarePath.replace(/\.md$/, '') + '.quarantine.md';
21
- }
22
- function registryActs(squarePath) {
23
- if (!fs.existsSync(squarePath))
24
- return [];
25
- try {
26
- return loadSquare(squarePath).acts;
27
- }
28
- catch {
29
- // A temporarily unreadable artifact cannot disprove a cache binding.
30
- return undefined;
31
- }
32
- }
33
5
  export const doctorCommand = {
34
6
  parse(argv, context) {
35
- let fix = false;
36
- for (let index = 0; index < argv.length; index++) {
37
- const argument = argv[index];
38
- if (argument === '--fix')
39
- fix = true;
40
- else if (argument === '--before') {
41
- index += 1;
42
- if (argv[index] === undefined)
43
- usage(context.command);
44
- }
45
- else
46
- usage(context.command);
47
- }
48
- return { fix };
7
+ if (argv.length > 0)
8
+ usage(context.command);
9
+ return undefined;
49
10
  },
50
- async execute(intent, context) {
51
- if (!intent.fix) {
52
- const diagnosis = diagnoseSquare(readSquareText(context.squarePath));
53
- if (diagnosis.unfixable) {
54
- return {
55
- output: withPathOutput(context.squarePath, renderDoctorUnfixable(diagnosis.unfixable), { participantCount: inSquareCount(loadSquare(context.squarePath)) }),
56
- exitCode: 2,
57
- };
58
- }
59
- const summary = diagnosis.problems.length === 0 ? renderDoctorClean() : renderDoctorProblems(diagnosis.problems);
11
+ execute(_intent, context) {
12
+ const diagnosis = diagnoseSquareFile(context.squarePath);
13
+ if (diagnosis.unfixable !== undefined || diagnosis.doc === undefined) {
60
14
  return {
61
- output: withPathOutput(context.squarePath, summary, { participantCount: inSquareCount(loadSquare(context.squarePath)) }),
62
- exitCode: diagnosis.problems.length === 0 ? 0 : 1,
63
- };
64
- }
65
- const repair = await repairSquare(context.squarePath);
66
- if (repair.diagnosis.unfixable) {
67
- return {
68
- output: withPathOutput(context.squarePath, renderDoctorUnfixable(repair.diagnosis.unfixable), { participantCount: inSquareCount(loadSquare(context.squarePath)) }),
15
+ output: withPathOutput(context.squarePath, renderDoctorUnfixable(diagnosis.unfixable ?? 'the snapshot could not be decoded')),
69
16
  exitCode: 2,
70
17
  };
71
18
  }
72
- const repaired = repair.repaired;
73
- const registry = pruneRegistry(registryActs);
74
- if (registry.removed > 0) {
75
- repaired.actions.push({ message: `pruned ${registry.removed} obsolete registry membership(s)` });
76
- }
77
- const sidecar = quarantinePath(context.squarePath);
78
19
  return {
79
- output: withPathOutput(context.squarePath, renderDoctorRepaired(repaired.actions, repaired.quarantinedBlocks.length, repaired.quarantinedBlocks.length > 0 ? sidecar : undefined), { participantCount: inSquareCount(loadSquare(context.squarePath)) }),
20
+ output: withPathOutput(context.squarePath, renderDoctorClean(), {
21
+ participantCount: inSquareCount(diagnosis.doc),
22
+ }),
80
23
  };
81
24
  },
82
25
  present(result) {
@@ -6,7 +6,8 @@ import { sessionInbox } from '../inbox.js';
6
6
  import { sweepPendingNotifications } from '../notifications.js';
7
7
  import { cmdListSquares } from '../list.js';
8
8
  import { sameName } from '../model.js';
9
- import { commandPrefix, participantCommandPrefix, renderActivitiesView, renderGrepActivitiesView, renderVisibleEvent, withPathOutput, } from '../presentation.js';
9
+ import { parseActivityId } from '../square-core.js';
10
+ import { commandPrefix, participantCommandPrefix, renderActivitiesView, renderGrepActivitiesView, renderAmbientEvent, withPathOutput, } from '../presentation.js';
10
11
  import { actId, inSquareCount, nowMs, sayNumberFor } from '../runtime.js';
11
12
  import { cmdStream, cmdStreamNdjson } from '../stream.js';
12
13
  import { formatRelativeTime, formatTimestamp, parseTimeOrRelative } from '../time.js';
@@ -98,10 +99,10 @@ export const catchCommand = {
98
99
  present: () => { },
99
100
  };
100
101
  function parseActRef(value, flag) {
101
- const match = value.trim().match(/^(?:act_)?(\d+)$/i);
102
- if (!match)
103
- fail(`Invalid ${flag}: expected an activity id like act_12 or 12.`);
104
- return Number(match[1]);
102
+ const index = parseActivityId(value);
103
+ if (index === undefined)
104
+ fail(`Invalid ${flag}: expected an activity id like act/12`);
105
+ return index;
105
106
  }
106
107
  function parseTimestamp(value, flag) {
107
108
  const timestamp = parseTimeOrRelative(value, nowMs());
@@ -305,24 +306,16 @@ export const historyCommand = {
305
306
  return events.map((item) => renderFields(doc, item, options.format)).join('\n') + (events.length > 0 ? '\n' : '');
306
307
  }
307
308
  const pattern = options.grep ?? options.fixed;
309
+ const archive = options.atIndex != null
310
+ || (options.ids !== undefined && options.ids.length > 0)
311
+ || (options.lastN == null && options.full === true);
308
312
  const output = pattern === undefined || pattern === ''
309
- ? renderActivitiesView(doc, events, null, options.full, context.squarePath, options.viewer ?? '')
313
+ ? renderActivitiesView(doc, events, null, options.full, context.squarePath, options.viewer ?? '', archive ? 'archive' : 'ambient')
310
314
  : renderGrepActivitiesView(events, totalMatches, options.full, context.squarePath, pattern, options.fixed !== undefined);
311
315
  return withPathOutput(context.squarePath, output, { participantCount: inSquareCount(doc) });
312
316
  },
313
317
  present: (result) => process.stdout.write(result),
314
318
  };
315
- export const warmupCommand = {
316
- parse(argv, context) { if (argv.length > 0)
317
- usage(context.command); return undefined; },
318
- execute(_intent, context) {
319
- const doc = loadSquare(context.squarePath);
320
- return withPathOutput(context.squarePath, doc.warmup.join('\n'), {
321
- participantCount: inSquareCount(doc),
322
- });
323
- },
324
- present: (result) => process.stdout.write(result),
325
- };
326
319
  export const participantsCommand = {
327
320
  parse(argv, context) { if (argv.length > 0)
328
321
  usage(context.command); return undefined; },
@@ -383,7 +376,7 @@ export const statusCommand = {
383
376
  : undefined;
384
377
  const visible = result.latestAct === undefined
385
378
  ? ''
386
- : renderVisibleEvent(doc.acts, result.latestAct, context.name ?? '', {
379
+ : renderAmbientEvent(result.latestAct, context.name ?? '', {
387
380
  now: result.now,
388
381
  preview: 200,
389
382
  actNumber: result.latestAct.kind === 'say'
@@ -3,9 +3,10 @@ import { SquareError } from '../model.js';
3
3
  import { defaultContext, parseGlobalArgs } from './context.js';
4
4
  import { executeRegisteredCommand, findCommand } from './registry.js';
5
5
  function isMutatingCommand(command, argv) {
6
+ void argv;
6
7
  if (['build', 'join', 'catch', 'express', 'done', 'hold', 'resume', 'compact'].includes(command))
7
8
  return true;
8
- return command === 'doctor' && argv.includes('--fix');
9
+ return false;
9
10
  }
10
11
  function handleSquareError(error) {
11
12
  if (error instanceof SquareError) {
@@ -19,7 +20,7 @@ export async function runCli(rawArgs = process.argv.slice(2)) {
19
20
  try {
20
21
  const requestedHelp = helpRequest(rawArgs);
21
22
  if (requestedHelp !== undefined) {
22
- await executeRegisteredCommand('help', requestedHelp.command === undefined ? [] : [requestedHelp.command], defaultContext('help', '.square/SQUARE.md'));
23
+ await executeRegisteredCommand('help', requestedHelp.command === undefined ? [] : [requestedHelp.command], defaultContext('help', '.square/SQUARE.square'));
23
24
  return;
24
25
  }
25
26
  const parsed = parseGlobalArgs(rawArgs);
@@ -2,7 +2,7 @@ import { buildCommand, compactCommand, doneCommand, expressCommand, holdCommand,
2
2
  import { doctorCommand } from './maintenance-commands.js';
3
3
  import { harnessCommand, installCommand, uninstallCommand } from './harness-command.js';
4
4
  import { helpCommand, versionCommand } from './meta-commands.js';
5
- import { catchCommand, claudeHookCommand, codexHookCommand, historyCommand, inboxCommand, listCommand, participantsCommand, statusCommand, streamCommand, warmupCommand, } from './observation-commands.js';
5
+ import { catchCommand, claudeHookCommand, codexHookCommand, historyCommand, inboxCommand, listCommand, participantsCommand, statusCommand, streamCommand, } from './observation-commands.js';
6
6
  /** Every public command is an executable adapter, including aliases and utility commands. */
7
7
  export const commandRegistry = [
8
8
  { names: ['build'], spec: buildCommand },
@@ -23,7 +23,6 @@ export const commandRegistry = [
23
23
  { names: ['compact'], spec: compactCommand },
24
24
  { names: ['doctor'], spec: doctorCommand },
25
25
  { names: ['history'], spec: historyCommand },
26
- { names: ['warmup'], spec: warmupCommand },
27
26
  { names: ['status'], spec: statusCommand },
28
27
  { names: ['participants'], spec: participantsCommand },
29
28
  { names: ['help'], spec: helpCommand },
@@ -2,11 +2,12 @@ import { cmdActivity } from '../activity.js';
2
2
  import { loadSquare } from '../artifact.js';
3
3
  import { cmdCompact } from '../compact.js';
4
4
  import { SquareError, formatHardCap, validateName, } from '../model.js';
5
- import { participantCommandPrefix, quoteShell, renderEventCli, renderPublicTail, withPathOutput, } from '../presentation.js';
5
+ import { participantCommandPrefix, renderEventCli, renderPublicTail, withPathOutput, } from '../presentation.js';
6
6
  import { hasAutomaticDeliveryIdentity, localParticipantOwner, recordLocalDone, recordLocalJoin } from '../registry.js';
7
7
  import { sweepPendingNotifications } from '../notifications.js';
8
8
  import { inSquareCount, isCurrentlyJoined, nowMs, resolveRosterName } from '../runtime.js';
9
9
  import { createSquare, execute } from '../square-application.js';
10
+ import { formatActivityId, parseActivityId } from '../square-core.js';
10
11
  import { fail, parseHardCap, parsePositiveInteger, readPipedBodyFallback, readStdinSync, requireParticipant, requireValue, resolveBody, usage, } from './context.js';
11
12
  function parseBuild(argv) {
12
13
  const options = { force: false, hardCap: null };
@@ -92,11 +93,12 @@ export const joinCommand = {
92
93
  const fallback = hasAutomaticDeliveryIdentity()
93
94
  ? []
94
95
  : ['', `» ${participantCommandPrefix(context.squarePath, joinedName)} catch --idle 30m`, ' no session delivery detected — keep this catch open for new activity'];
96
+ const scene = after.warmup.join('\n').trim();
95
97
  const output = [
96
- `● ${joinedName} stepped into the square`,
98
+ `● You stepped into the square`,
99
+ ...(isRejoin || scene === '' ? [] : ['', scene]),
97
100
  ...(isRejoin || contextText === '' ? [] : ['', 'context', contextText]),
98
101
  ...(activities === '' ? [] : ['', 'recent activity', activities]),
99
- ...(isRejoin ? [] : ['', `» ${participantCommandPrefix(context.squarePath, joinedName)} warmup`]),
100
102
  ...fallback,
101
103
  ].join('\n');
102
104
  return withPathOutput(context.squarePath, output, { participantCount: inSquareCount(after) });
@@ -125,7 +127,9 @@ export const joinCommand = {
125
127
  const line = reconnect && !intent.kick
126
128
  ? `● you are already in the square`
127
129
  : `✓ you banished the original ${joinedName} — the name is yours`;
128
- return withPathOutput(context.squarePath, `${line}${fallback}`, { participantCount: inSquareCount(doc) });
130
+ const scene = doc.warmup.join('\n').trim();
131
+ const showScene = !(reconnect && !intent.kick) && scene !== '';
132
+ return withPathOutput(context.squarePath, `${line}${showScene ? `\n\n${scene}` : ''}${fallback}`, { participantCount: inSquareCount(doc) });
129
133
  }
130
134
  },
131
135
  present: (result) => process.stdout.write(result),
@@ -133,7 +137,6 @@ export const joinCommand = {
133
137
  function parseActivity(argv, context) {
134
138
  let force = false;
135
139
  let noWait = false;
136
- let beside;
137
140
  let bell = false;
138
141
  let reply;
139
142
  const bodyArgs = [];
@@ -143,25 +146,21 @@ function parseActivity(argv, context) {
143
146
  force = true;
144
147
  else if (argument === '--no-wait')
145
148
  noWait = true;
146
- else if (argument === '--beside') {
147
- beside = requireValue(argv, index, argument);
148
- index += 1;
149
- }
149
+ else if (argument === '--beside')
150
+ fail('✕ express does not know --beside\n» square express --help');
150
151
  else if (argument === '--bell')
151
152
  bell = true;
152
153
  else if (argument === '--reply') {
153
- const value = requireValue(argv, index, argument).trim().match(/^(?:act_)?(\d+)$/i);
154
- if (!value || !Number.isSafeInteger(Number(value[1])))
155
- fail('Invalid --reply: expected an activity id like act_12 or 12.');
156
- reply = Number(value[1]);
154
+ const replyIndex = parseActivityId(requireValue(argv, index, argument));
155
+ if (replyIndex === undefined)
156
+ fail('Invalid --reply: expected an activity id like act/12');
157
+ reply = replyIndex;
157
158
  index += 1;
158
159
  }
159
160
  else
160
161
  bodyArgs.push(argument);
161
162
  }
162
- if (bell && beside !== undefined)
163
- fail('Invalid express options: --beside and --bell are mutually exclusive.');
164
- const reach = bell ? 'bell' : beside === undefined ? undefined : { beside };
163
+ const reach = bell ? 'bell' : undefined;
165
164
  if (bodyArgs.length !== 1) {
166
165
  if (bodyArgs.length === 0) {
167
166
  const piped = readPipedBodyFallback();
@@ -176,13 +175,13 @@ export const expressCommand = {
176
175
  parse: parseActivity,
177
176
  async execute(intent, context) {
178
177
  await sweepPendingNotifications(context.squarePath);
179
- const reachArg = intent.reach === 'bell' ? ' --bell' : intent.reach === undefined ? '' : ` --beside ${quoteShell(intent.reach.beside)}`;
178
+ const reachArg = intent.reach === 'bell' ? ' --bell' : '';
180
179
  await cmdActivity(context.squarePath, intent.name, intent.activity, resolveBody, {
181
180
  force: intent.force,
182
181
  noWait: intent.noWait,
183
182
  reach: intent.reach,
184
183
  reply: intent.reply,
185
- forceCommand: `${participantCommandPrefix(context.squarePath, intent.name)} express --force${reachArg}${intent.reply === undefined ? '' : ` --reply act_${intent.reply}`} -`,
184
+ forceCommand: `${participantCommandPrefix(context.squarePath, intent.name)} express --force${reachArg}${intent.reply === undefined ? '' : ` --reply ${formatActivityId(intent.reply)}`} -`,
186
185
  });
187
186
  },
188
187
  present: () => { },
package/dist/compact.js CHANGED
@@ -1,19 +1,19 @@
1
1
  import { SquareError } from './model.js';
2
2
  import { withPathOutput } from './presentation.js';
3
3
  import { execute } from './square-application.js';
4
- function sidecarPath(squarePath) {
5
- return squarePath.replace(/\.md$/, '') + '.archive.md';
4
+ function archivePath(squarePath) {
5
+ return squarePath.replace(/\.square$/, '') + '.archive.square';
6
6
  }
7
7
  export async function cmdCompact(squarePath, opts) {
8
8
  try {
9
9
  let archivedCount;
10
10
  let keptCount;
11
- const archive = sidecarPath(squarePath);
11
+ const archive = archivePath(squarePath);
12
12
  const committed = await execute(squarePath, { type: 'compact', keep: opts.keep, archivePath: archive });
13
13
  const result = committed.result;
14
14
  archivedCount = result.archived.length;
15
15
  keptCount = result.doc.acts.length;
16
- const summary = ['✓ compacted', ` · archived ${archivedCount} activities`, ` · kept ${keptCount} activities`, ...(archivedCount > 0 ? [` · sidecar ${archive}`] : [])].join('\n');
16
+ const summary = ['✓ compacted', ` · archived ${archivedCount} activities`, ` · kept ${keptCount} activities`, ...(archivedCount > 0 ? [` · archive ${archive}`] : [])].join('\n');
17
17
  process.stdout.write(withPathOutput(squarePath, summary));
18
18
  }
19
19
  catch (err) {
package/dist/decisions.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { SquareError, sameName, validateName, } from './model.js';
2
2
  import { UNREAD_BLOCK_GRACE_MS, actId, actStableIndex, foldedState, freshWatchLease, getReadState, publicActs, readCursor, resolveRosterName, rosterNames, matchesMentionTarget, THROTTLE_WINDOW_MS, } from './runtime.js';
3
3
  import { actDelta, peerPublicActs, peerRoomChanges } from './activity-feed.js';
4
- import { validate } from './square-core.js';
4
+ import { extractMentions, formatActivityId, validate } from './square-core.js';
5
5
  import { deriveDeliveryModel } from './delivery.js';
6
6
  import { compileSearchPattern } from './search.js';
7
7
  export function resolveKnownName(doc, name) {
@@ -27,7 +27,7 @@ export function decideJoin(doc, name, now) {
27
27
  return {
28
28
  joinedName,
29
29
  addParticipant: knownName === undefined,
30
- joinAct: { kind: 'join', actor: joinedName, at: now, body: '' },
30
+ joinAct: { kind: 'join', actor: joinedName, at: now },
31
31
  };
32
32
  }
33
33
  const UNREAD_PREVIEW_LIMIT = 3;
@@ -41,10 +41,15 @@ export function decideAct(doc, input) {
41
41
  const reply = input.reply;
42
42
  if (reply !== undefined) {
43
43
  if (!Number.isSafeInteger(reply) || reply < 0 || reply >= doc.runtime.nextActIndex) {
44
- throw new SquareError('invalid_args', `Unknown reply activity: act_${reply}`);
44
+ const label = Number.isSafeInteger(reply) && reply >= 0 ? formatActivityId(reply) : String(reply);
45
+ throw new SquareError('invalid_args', `Unknown reply activity: ${label}`);
45
46
  }
46
47
  }
47
48
  const state = foldedState(doc);
49
+ if (reach !== 'bell'
50
+ && extractMentions(body).length === 0) {
51
+ throw new SquareError('invalid_args', 'express requires an @mention unless using --bell');
52
+ }
48
53
  const current = participantState(state, name);
49
54
  const result = validate(state, {
50
55
  kind: 'say', actor: name, at: now, body,
@@ -97,7 +102,8 @@ export function decideAct(doc, input) {
97
102
  }))
98
103
  .sort((a, b) => a.latestActivityAgeMs - b.latestActivityAgeMs || a.name.localeCompare(b.name));
99
104
  const latestActivityAgeMs = activitySummaries[0]?.latestActivityAgeMs;
100
- const hasUnread = unreadPublic.length > 0 || unreadRoomChanges.length > 0;
105
+ const hasUnreadBlockingChange = unreadRoomChanges.some((act) => act.kind !== 'join');
106
+ const hasUnread = unreadPublic.length > 0 || hasUnreadBlockingChange;
101
107
  const hasFreshUnreadActivity = latestActivityAgeMs !== undefined && latestActivityAgeMs <= UNREAD_BLOCK_GRACE_MS;
102
108
  if (!force && hasUnread && !hasFreshUnreadActivity) {
103
109
  return { type: 'blocked', activitySummaries, unreadRoomChanges };
@@ -124,7 +130,7 @@ export function coreHold(_doc, actor, body, now) {
124
130
  return { kind: 'hold', actor, at: now, body: body.replace(/\r\n/g, '\n').trim() };
125
131
  }
126
132
  export function coreResume(_doc, actor, now) {
127
- return { kind: 'resume', actor, at: now, body: '' };
133
+ return { kind: 'resume', actor, at: now };
128
134
  }
129
135
  function presenceFor(doc, snapshot, name, now) {
130
136
  if (snapshot?.done)
@@ -1,5 +1,6 @@
1
1
  import { loadSquare } from './artifact.js';
2
2
  import { deriveDeliveryModel, } from './delivery.js';
3
+ import { formatActivityId } from './square-core.js';
3
4
  import { formatDuration } from './time.js';
4
5
  import { joinedRecipients, wakeEvidence } from './wake-evidence.js';
5
6
  const DISPLAY_ORDER = [
@@ -44,7 +45,7 @@ export function classifyDeliveryHealth(squarePath, opts) {
44
45
  }
45
46
  function formatItem(item) {
46
47
  const evidence = item.attempt?.signature === undefined ? '' : ` · ${item.attempt.signature}`;
47
- return ` · act_${item.actIndex} → @${item.recipient} from @${item.actor} · ${formatDuration(item.ageMs)}${evidence}`;
48
+ return ` · ${formatActivityId(item.actIndex)} → @${item.recipient} from @${item.actor} · ${formatDuration(item.ageMs)}${evidence}`;
48
49
  }
49
50
  export function doctorDeliveryHealth(squarePath, graceMs, now = Date.now(), env = process.env) {
50
51
  const items = classifyDeliveryHealth(squarePath, { graceMs, now, env });
package/dist/delivery.js CHANGED
@@ -1,10 +1,8 @@
1
1
  import { findParticipantName, sameName, } from './model.js';
2
- import { actId, extractMentions, isCurrentlyJoined, lastJoinIndex, matchesMentionTarget, resolveRosterName, rosterNames } from './runtime.js';
2
+ import { audienceOf, formatActivityId, resolveAudience } from './square-core.js';
3
+ import { actId, isCurrentlyJoined, lastJoinIndex, matchesMentionTarget, resolveRosterName, rosterNames } from './runtime.js';
3
4
  export function notificationMessageId(squarePath, actIndex) {
4
- return `square:${squarePath}#act_${actIndex}`;
5
- }
6
- export function isPendingNotification(notification) {
7
- return notification.route !== 'broadcast';
5
+ return `square:${squarePath}#${formatActivityId(actIndex)}`;
8
6
  }
9
7
  function canonicalRecipient(doc, name) {
10
8
  return resolveRosterName(doc, name) ?? name;
@@ -32,16 +30,6 @@ export function recordDeliveredRuntime(runtime, recipient, actOrIndex, receipt)
32
30
  export function markDeliveredDelivery(doc, name, actOrIndex, at = Date.now()) {
33
31
  return recordDeliveredDelivery(doc, name, actOrIndex, { at });
34
32
  }
35
- function uniqueKnownMentions(body, roster) {
36
- const recipients = [];
37
- for (const mention of extractMentions(body)) {
38
- const known = findParticipantName(roster, mention);
39
- if (known !== undefined && !recipients.some((recipient) => sameName(recipient, known))) {
40
- recipients.push(known);
41
- }
42
- }
43
- return recipients;
44
- }
45
33
  /**
46
34
  * Derive delivery behavior once from the parsed Square document.
47
35
  * All consumers share these targets instead of reinterpreting artifact text or cursor state.
@@ -53,23 +41,9 @@ export function deriveDeliveryModel(doc) {
53
41
  if (item.kind !== 'say')
54
42
  return [];
55
43
  const sayItem = item;
56
- const actor = sayItem.actor;
57
- if (sayItem.reach === 'bell') {
58
- return roster
59
- .filter((recipient) => !sameName(recipient, actor))
60
- .map((recipient) => ({ item: sayItem, recipient, route: 'bell' }));
61
- }
62
- if (sayItem.reach !== undefined) {
63
- const recipient = findParticipantName(roster, sayItem.reach.beside);
64
- return recipient === undefined || sameName(recipient, actor)
65
- ? []
66
- : [{ item: sayItem, recipient, route: 'beside' }];
67
- }
68
- const mentions = uniqueKnownMentions(sayItem.body, roster).filter((recipient) => !sameName(recipient, actor));
69
- const recipients = mentions.length > 0
70
- ? mentions
71
- : roster.filter((recipient) => !sameName(recipient, actor));
72
- const route = mentions.length > 0 ? 'mention' : 'broadcast';
44
+ const audience = audienceOf(sayItem);
45
+ const recipients = resolveAudience(audience, roster).filter((recipient) => !sameName(recipient, sayItem.actor));
46
+ const route = audience.kind === 'bell' ? 'bell' : 'mention';
73
47
  return recipients.map((recipient) => ({ item: sayItem, recipient, route }));
74
48
  }
75
49
  function pendingFor(requestedRecipient) {
@@ -82,19 +56,16 @@ export function deriveDeliveryModel(doc) {
82
56
  for (const act of doc.acts) {
83
57
  if (act.kind !== 'say')
84
58
  continue;
85
- // Broadcasts can never be pending directed notifications. Skipping them here
86
- // avoids allocating one planned notification per participant per activity.
87
- if (act.reach === undefined && extractMentions(act.body).length === 0)
59
+ const audience = audienceOf(act);
60
+ if (audience.kind === 'mentions' && audience.names.length === 0)
88
61
  continue;
89
62
  for (const planned of plan(act)) {
90
- if (planned.route === 'broadcast')
91
- continue;
92
63
  const joinedAt = joinedAfter.get(planned.recipient);
93
64
  if (joinedAt === undefined || act.index <= joinedAt)
94
65
  continue;
95
66
  if (isDeliveryDelivered(doc, planned.recipient, act.index))
96
67
  continue;
97
- pendingByRecipient.get(planned.recipient)?.push({ ...planned, route: planned.route });
68
+ pendingByRecipient.get(planned.recipient)?.push(planned);
98
69
  }
99
70
  }
100
71
  }
@@ -118,7 +89,7 @@ export function markDeliveredNotifications(doc, recipient, delivered, at = Date.
118
89
  }
119
90
  /** Canonical say-activity filter shared by catch selection and hook ownership. */
120
91
  export function matchesCatchFilter(activity, filter) {
121
- if (activity.reach === 'bell')
92
+ if (audienceOf(activity).kind === 'bell')
122
93
  return true;
123
94
  if (filter.participants !== undefined &&
124
95
  !filter.participants.some((participant) => sameName(participant, activity.actor))) {
@@ -128,16 +99,9 @@ export function matchesCatchFilter(activity, filter) {
128
99
  }
129
100
  /** True only when the live catch's own filters would deliver this notification. */
130
101
  export function leaseOwnsNotification(lease, notification) {
131
- const recipient = notification.recipient;
132
- if (notification.route === 'beside' && recipient === undefined)
133
- return false;
134
102
  return matchesCatchFilter({
135
103
  actor: notification.actor,
136
104
  body: notification.body,
137
- reach: notification.route === 'bell'
138
- ? 'bell'
139
- : notification.route === 'beside'
140
- ? { beside: recipient }
141
- : undefined,
105
+ ...(notification.route === 'bell' ? { reach: 'bell' } : {}),
142
106
  }, lease.filter ?? {});
143
107
  }
package/dist/help.js CHANGED
@@ -16,9 +16,9 @@ const COMMANDS = [
16
16
  details: ['Options:', ' --last <N> Show the last N public activities (default 10).', ' --all Show the complete history.'],
17
17
  },
18
18
  {
19
- names: ['express'], usage: '--as <name> express [-f|--force] [--no-wait] [--beside <name> | --bell] [--reply <act_N>] <activity | ->', usesSquare: true, group: 'participant',
19
+ names: ['express'], usage: '--as <name> express [-f|--force] [--no-wait] [--bell] [--reply <activity-id>] <activity | ->', usesSquare: true, group: 'participant',
20
20
  summary: 'Speak, gesture, or do both.',
21
- details: ['Options:', ' -f, --force Express without first catching unread activity.', ' --no-wait If held or throttled, save a draft and return.', ' --beside <name> Speak aside to one participant.', " --bell Call every participant's attention to this activity.", ' --reply <act_N> Mark this activity as a reply to an earlier activity.'],
21
+ details: ['Reach:', ' @name Address someone in the square. They hear the body; everyone else sees you walk over.', " --bell Call every participant's attention to this activity without a mention.", '', 'Options:', ' -f, --force Express without first catching unread activity.', ' --no-wait If held or throttled, save a draft and return.', ' --reply <activity-id> Mark this activity as a reply to an earlier activity (for example act/12).'],
22
22
  },
23
23
  {
24
24
  names: ['catch'], usage: '--as <name> catch (--now | --idle <duration>) [--from <names>] [--mention [name]] [--replace]', usesSquare: true, group: 'participant',
@@ -39,10 +39,9 @@ const COMMANDS = [
39
39
  { names: ['claude-hook', 'codex-hook'], usage: '{command}', summary: 'Present pending attention at one native agent boundary.', hiddenFromIndex: true },
40
40
  {
41
41
  names: ['history'], usage: '[--as <name>] history [filters] [output]', usesSquare: true, group: 'participant',
42
- summary: 'Read or search what happened without changing what you have caught.',
43
- details: ['Filters:', ' --from <names> Match activities from participants.', ' --since <time>, --until <time> Match a time window.', ' --grep <regex> | --fixed <s> Search ids, participant names, and bodies.', ' --mention <name> Match mentions.', ' --pending Match attention waiting for --as <name>.', ' --ids <ids> | --at <id> Match stable activity ids.', ' -B, -A, -C <N> Set non-negative context around --at.', ' --after <id> Match activities after an id.', '', 'Results:', ' --limit <N> | --all Bound the newest matches (default 10).', ' --order <asc|desc> Set display order.', '', 'Output:', ' --full --json --format <fields> --count'],
42
+ summary: 'Read or search the archive without changing what you have caught.',
43
+ details: ['Filters:', ' --from <names> Match activities from participants.', ' --since <time>, --until <time> Match a time window.', ' --grep <regex> | --fixed <s> Search ids, participant names, and original bodies.', ' --mention <name> Match mentions.', ' --pending Match attention waiting for --as <name>.', ' --ids <ids> | --at <id> Match stable activity ids and show original bodies.', ' -B, -A, -C <N> Set non-negative context around --at.', ' --after <id> Match activities after an id.', '', 'Results:', ' --limit <N> | --all Bound the newest matches (default 10).', ' --order <asc|desc> Set display order.', '', 'Output:', ' --full --json --format <fields> --count'],
44
44
  },
45
- { names: ['warmup'], usage: 'warmup', usesSquare: true, group: 'host', summary: 'Print the complete embedded participant warmup.' },
46
45
  { names: ['status'], usage: '[--as <name>] status', usesSquare: true, group: 'participant', summary: 'Show who is present and what happened most recently.' },
47
46
  { names: ['participants'], usage: 'participants', usesSquare: true, group: 'host', summary: 'Show the full participant roster and current states.' },
48
47
  { names: ['hold'], usage: '--as <name> hold [reason | -]', usesSquare: true, group: 'participant', summary: 'Raise a hand and pause participant activity.' },
@@ -64,9 +63,8 @@ const COMMANDS = [
64
63
  },
65
64
  { names: ['compact'], usage: 'compact [--keep N]', usesSquare: true, group: 'host', summary: 'Move older activity out of the working artifact while keeping the latest N.' },
66
65
  {
67
- names: ['doctor'], usage: 'doctor [--fix]', usesSquare: true, group: 'maintenance',
68
- summary: 'Diagnose artifact integrity.',
69
- details: ['Options:', ' --fix Repair recoverable artifact problems.'],
66
+ names: ['doctor'], usage: 'doctor', usesSquare: true, group: 'maintenance',
67
+ summary: 'Validate binary artifact integrity.',
70
68
  },
71
69
  ];
72
70
  function definitionFor(command) {
@@ -78,8 +76,8 @@ function isHelpFlag(value) {
78
76
  export function renderGlobalHelp() {
79
77
  const groups = [
80
78
  { key: 'participant', title: 'In the square:', order: ['join', 'express', 'catch', 'history', 'status', 'hold', 'resume', 'done'] },
81
- { key: 'host', title: 'Prepare and manage:', order: ['build', 'list', 'participants', 'warmup', 'compact'] },
82
- { key: 'maintenance', title: 'Setup and repair:', order: ['install', 'uninstall', 'doctor'] },
79
+ { key: 'host', title: 'Prepare and manage:', order: ['build', 'list', 'participants', 'compact'] },
80
+ { key: 'maintenance', title: 'Setup:', order: ['install', 'uninstall', 'doctor'] },
83
81
  ];
84
82
  const commandLines = groups.flatMap(({ key, title, order }) => [
85
83
  title,
package/dist/index.js CHANGED
@@ -1,19 +1,21 @@
1
1
  export { SquareError } from './model.js';
2
+ export { extractMentions, formatActivityId, parseActivityId } from './square-core.js';
2
3
  export { loadSquare } from './artifact.js';
3
- export { extractMentions, countSays, joinedNames, doneNames, isCurrentlyJoined, publicActs, readCursor, } from './runtime.js';
4
+ export { countSays, joinedNames, doneNames, isCurrentlyJoined, publicActs, readCursor, } from './runtime.js';
4
5
  import { loadSquare } from './artifact.js';
5
6
  import { dispatchActNotifications, hasDeliveredNotification as hasDeliveredNotificationImpl, sweepPendingNotifications, waitForDeliveredNotification as waitForDeliveredNotificationImpl, } from './notifications.js';
6
7
  import { WATCH_STALE_MS, freshWatchLease, getReadState as getDocReadState, } from './runtime.js';
7
8
  import { resolveKnownName } from './decisions.js';
8
9
  import { execute } from './square-application.js';
10
+ import { parseActivityId } from './square-core.js';
9
11
  export { WATCH_STALE_MS };
10
12
  function actRefIndex(ref) {
11
13
  if (typeof ref === 'number')
12
14
  return ref;
13
- const match = ref.match(/^act_(\d+)$/);
14
- if (!match)
15
+ const index = parseActivityId(ref);
16
+ if (index === undefined)
15
17
  throw new Error(`Invalid act ref: ${ref}`);
16
- return Number(match[1]);
18
+ return index;
17
19
  }
18
20
  export function getReadState(squarePath, name) {
19
21
  const doc = loadSquare(squarePath);