@astrosheep/square 0.3.10 → 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.
Files changed (54) hide show
  1. package/codex-plugin/.codex-plugin/plugin.json +1 -1
  2. package/dist/activity.js +6 -7
  3. package/dist/artifact.js +337 -618
  4. package/dist/boundary-presentation.js +1 -1
  5. package/dist/cli/context.js +3 -3
  6. package/dist/cli/harness-command.js +1 -1
  7. package/dist/cli/maintenance-commands.js +12 -58
  8. package/dist/cli/observation-commands.js +14 -34
  9. package/dist/cli/program.js +3 -6
  10. package/dist/cli/registry.js +1 -2
  11. package/dist/cli/square-commands.js +39 -20
  12. package/dist/cmd/notify-once.js +5 -15
  13. package/dist/compact.js +4 -4
  14. package/dist/decisions.js +21 -7
  15. package/dist/delivery-health.js +56 -136
  16. package/dist/delivery.js +11 -47
  17. package/dist/file-lock.js +112 -0
  18. package/dist/harness-codex.js +35 -29
  19. package/dist/harness-links.js +0 -3
  20. package/dist/harness-pi.js +57 -0
  21. package/dist/harness.js +10 -15
  22. package/dist/help.js +16 -18
  23. package/dist/index.js +11 -5
  24. package/dist/list.js +3 -47
  25. package/dist/model.js +4 -6
  26. package/dist/notifications.js +217 -32
  27. package/dist/paseo-connection.js +135 -0
  28. package/dist/paseo-delivery.js +73 -144
  29. package/dist/paseo-state.js +1 -1
  30. package/dist/paseo-timeline.js +32 -42
  31. package/dist/presentation.js +24 -39
  32. package/dist/presented.js +10 -72
  33. package/dist/registry.js +23 -24
  34. package/dist/routes.js +153 -0
  35. package/dist/runtime.js +6 -21
  36. package/dist/square-application.js +56 -127
  37. package/dist/square-core.js +56 -9
  38. package/dist/stream.js +1 -1
  39. package/dist/wake-attempts.js +175 -0
  40. package/dist/wake-evidence.js +35 -0
  41. package/dist/wake-port.js +22 -0
  42. package/dist/wake-sink.js +45 -6
  43. package/dist/watch.js +1 -2
  44. package/guides/participant.md +7 -174
  45. package/package.json +6 -3
  46. package/skills/brainstorm/SKILL.md +28 -28
  47. package/skills/square/.claude-plugin/plugin.json +1 -1
  48. package/skills/square/SKILL.md +23 -14
  49. package/skills/square-feedback/SKILL.md +7 -7
  50. package/dist/doctor.js +0 -35
  51. package/dist/notification-failures.js +0 -54
  52. package/template.md +0 -4
  53. package/templates/architect.md +0 -4
  54. package/templates/brainstorm.md +0 -4
@@ -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 {
@@ -118,7 +118,7 @@ export function parseGlobalArgs(rawArgs) {
118
118
  let requestedPath;
119
119
  let name;
120
120
  for (let index = 0; index < args.length; index++) {
121
- if (args[index] === '--square-path') {
121
+ if (args[index] === '--location') {
122
122
  requestedPath = requireValue(args, index, args[index]);
123
123
  args.splice(index, 2);
124
124
  index -= 1;
@@ -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,71 +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
5
  export const doctorCommand = {
23
6
  parse(argv, context) {
24
- let fix = false;
25
- for (let index = 0; index < argv.length; index++) {
26
- const argument = argv[index];
27
- if (argument === '--fix')
28
- fix = true;
29
- else if (argument === '--before') {
30
- index += 1;
31
- if (argv[index] === undefined)
32
- usage(context.command);
33
- }
34
- else
35
- usage(context.command);
36
- }
37
- return { fix };
7
+ if (argv.length > 0)
8
+ usage(context.command);
9
+ return undefined;
38
10
  },
39
- async execute(intent, context) {
40
- if (!intent.fix) {
41
- const diagnosis = diagnoseSquare(readSquareText(context.squarePath));
42
- if (diagnosis.unfixable) {
43
- return {
44
- output: withPathOutput(context.squarePath, renderDoctorUnfixable(diagnosis.unfixable), { participantCount: inSquareCount(loadSquare(context.squarePath)) }),
45
- exitCode: 2,
46
- };
47
- }
48
- 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) {
49
14
  return {
50
- output: withPathOutput(context.squarePath, summary, { participantCount: inSquareCount(loadSquare(context.squarePath)) }),
51
- exitCode: diagnosis.problems.length === 0 ? 0 : 1,
52
- };
53
- }
54
- const repair = await repairSquare(context.squarePath);
55
- if (repair.diagnosis.unfixable) {
56
- return {
57
- 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')),
58
16
  exitCode: 2,
59
17
  };
60
18
  }
61
- const repaired = repair.repaired;
62
- const registry = pruneRegistry();
63
- if (registry.removed > 0) {
64
- repaired.actions.push({ message: `pruned ${registry.removed} obsolete registry membership(s)` });
65
- }
66
- const sidecar = quarantinePath(context.squarePath);
67
19
  return {
68
- 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
+ }),
69
23
  };
70
24
  },
71
25
  present(result) {
@@ -3,11 +3,12 @@ import { runClaudeHook } from '../claude-hook.js';
3
3
  import { runCodexHook } from '../codex-hook.js';
4
4
  import { coreActivities, coreParticipants, coreStatus } from '../decisions.js';
5
5
  import { sessionInbox } from '../inbox.js';
6
+ import { sweepPendingNotifications } from '../notifications.js';
6
7
  import { cmdListSquares } from '../list.js';
7
8
  import { sameName } from '../model.js';
8
- import { commandPrefix, participantCommandPrefix, renderActivitiesView, renderGrepActivitiesView, renderVisibleEvent, withPathOutput, } from '../presentation.js';
9
- import { recordLocalJoin } from '../registry.js';
10
- import { actId, inSquareCount, isCurrentlyJoined, nowMs, resolveRosterName, sayNumberFor, } from '../runtime.js';
9
+ import { parseActivityId } from '../square-core.js';
10
+ import { commandPrefix, participantCommandPrefix, renderActivitiesView, renderGrepActivitiesView, renderAmbientEvent, withPathOutput, } from '../presentation.js';
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';
13
14
  import { cmdWatch } from '../watch.js';
@@ -93,14 +94,15 @@ export const catchCommand = {
93
94
  },
94
95
  async execute(intent, context) {
95
96
  await cmdWatch(context.squarePath, requireParticipant(context.name), intent);
97
+ await sweepPendingNotifications(context.squarePath);
96
98
  },
97
99
  present: () => { },
98
100
  };
99
101
  function parseActRef(value, flag) {
100
- const match = value.trim().match(/^(?:act_)?(\d+)$/i);
101
- if (!match)
102
- fail(`Invalid ${flag}: expected an activity id like act_12 or 12.`);
103
- 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;
104
106
  }
105
107
  function parseTimestamp(value, flag) {
106
108
  const timestamp = parseTimeOrRelative(value, nowMs());
@@ -304,24 +306,16 @@ export const historyCommand = {
304
306
  return events.map((item) => renderFields(doc, item, options.format)).join('\n') + (events.length > 0 ? '\n' : '');
305
307
  }
306
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);
307
312
  const output = pattern === undefined || pattern === ''
308
- ? renderActivitiesView(doc, events, null, options.full, context.squarePath, options.viewer ?? '')
313
+ ? renderActivitiesView(doc, events, null, options.full, context.squarePath, options.viewer ?? '', archive ? 'archive' : 'ambient')
309
314
  : renderGrepActivitiesView(events, totalMatches, options.full, context.squarePath, pattern, options.fixed !== undefined);
310
315
  return withPathOutput(context.squarePath, output, { participantCount: inSquareCount(doc) });
311
316
  },
312
317
  present: (result) => process.stdout.write(result),
313
318
  };
314
- export const warmupCommand = {
315
- parse(argv, context) { if (argv.length > 0)
316
- usage(context.command); return undefined; },
317
- execute(_intent, context) {
318
- const doc = loadSquare(context.squarePath);
319
- return withPathOutput(context.squarePath, doc.warmup.join('\n'), {
320
- participantCount: inSquareCount(doc),
321
- });
322
- },
323
- present: (result) => process.stdout.write(result),
324
- };
325
319
  export const participantsCommand = {
326
320
  parse(argv, context) { if (argv.length > 0)
327
321
  usage(context.command); return undefined; },
@@ -382,7 +376,7 @@ export const statusCommand = {
382
376
  : undefined;
383
377
  const visible = result.latestAct === undefined
384
378
  ? ''
385
- : renderVisibleEvent(doc.acts, result.latestAct, context.name ?? '', {
379
+ : renderAmbientEvent(result.latestAct, context.name ?? '', {
386
380
  now: result.now,
387
381
  preview: 200,
388
382
  actNumber: result.latestAct.kind === 'say'
@@ -440,17 +434,3 @@ function hookCommand(runHook) {
440
434
  }
441
435
  export const claudeHookCommand = hookCommand(runClaudeHook);
442
436
  export const codexHookCommand = hookCommand(runCodexHook);
443
- /** Maintain the local discovery cache before participant-facing adapters run. */
444
- export function refreshLocalRegistration(squarePath, name) {
445
- if (name === undefined)
446
- return;
447
- try {
448
- const doc = loadSquare(squarePath);
449
- const known = resolveRosterName(doc, name);
450
- if (known !== undefined && isCurrentlyJoined(doc.acts, known))
451
- recordLocalJoin(known, squarePath);
452
- }
453
- catch {
454
- // The machine-local discovery cache never makes a Square command fail.
455
- }
456
- }
@@ -1,12 +1,12 @@
1
1
  import { helpRequest } from '../help.js';
2
2
  import { SquareError } from '../model.js';
3
3
  import { defaultContext, parseGlobalArgs } from './context.js';
4
- import { refreshLocalRegistration } from './observation-commands.js';
5
4
  import { executeRegisteredCommand, findCommand } from './registry.js';
6
5
  function isMutatingCommand(command, argv) {
6
+ void argv;
7
7
  if (['build', 'join', 'catch', 'express', 'done', 'hold', 'resume', 'compact'].includes(command))
8
8
  return true;
9
- return command === 'doctor' && argv.includes('--fix');
9
+ return false;
10
10
  }
11
11
  function handleSquareError(error) {
12
12
  if (error instanceof SquareError) {
@@ -20,7 +20,7 @@ export async function runCli(rawArgs = process.argv.slice(2)) {
20
20
  try {
21
21
  const requestedHelp = helpRequest(rawArgs);
22
22
  if (requestedHelp !== undefined) {
23
- 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'));
24
24
  return;
25
25
  }
26
26
  const parsed = parseGlobalArgs(rawArgs);
@@ -37,9 +37,6 @@ export async function runCli(rawArgs = process.argv.slice(2)) {
37
37
  process.stderr.write('✕ more than one square is active here; choose the path before changing or consuming activity.\n» square list\n');
38
38
  process.exit(2);
39
39
  }
40
- if (['express', 'catch', 'done', 'hold', 'resume'].includes(command)) {
41
- refreshLocalRegistration(parsed.squarePath, parsed.name);
42
- }
43
40
  await executeRegisteredCommand(command, parsed.args.slice(1), defaultContext(command, parsed.squarePath, parsed.name));
44
41
  }
45
42
  catch (error) {
@@ -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,10 +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';
6
- import { hasAutomaticDeliveryIdentity, recordLocalDone, recordLocalJoin } from '../registry.js';
5
+ import { participantCommandPrefix, renderEventCli, renderPublicTail, withPathOutput, } from '../presentation.js';
6
+ import { hasAutomaticDeliveryIdentity, localParticipantOwner, recordLocalDone, recordLocalJoin } from '../registry.js';
7
+ import { sweepPendingNotifications } from '../notifications.js';
7
8
  import { inSquareCount, isCurrentlyJoined, nowMs, resolveRosterName } from '../runtime.js';
8
9
  import { createSquare, execute } from '../square-application.js';
10
+ import { formatActivityId, parseActivityId } from '../square-core.js';
9
11
  import { fail, parseHardCap, parsePositiveInteger, readPipedBodyFallback, readStdinSync, requireParticipant, requireValue, resolveBody, usage, } from './context.js';
10
12
  function parseBuild(argv) {
11
13
  const options = { force: false, hardCap: null };
@@ -56,6 +58,7 @@ export const buildCommand = {
56
58
  };
57
59
  function parseJoin(argv, context) {
58
60
  let lastN = 10;
61
+ let kick = false;
59
62
  for (let index = 0; index < argv.length; index++) {
60
63
  if (argv[index] === '--last') {
61
64
  lastN = parsePositiveInteger(requireValue(argv, index, argv[index]), argv[index]);
@@ -64,11 +67,14 @@ function parseJoin(argv, context) {
64
67
  else if (argv[index] === '--all') {
65
68
  lastN = null;
66
69
  }
70
+ else if (argv[index] === '--kick') {
71
+ kick = true;
72
+ }
67
73
  else {
68
74
  usage(context.command);
69
75
  }
70
76
  }
71
- return { name: requireParticipant(context.name), lastN };
77
+ return { name: requireParticipant(context.name), lastN, kick };
72
78
  }
73
79
  export const joinCommand = {
74
80
  parse: parseJoin,
@@ -81,16 +87,18 @@ export const joinCommand = {
81
87
  const after = loadSquare(context.squarePath);
82
88
  const preamble = after.preamble.at(-1) === '---' ? after.preamble.slice(0, -1) : after.preamble;
83
89
  recordLocalJoin(joinedName, context.squarePath);
90
+ await sweepPendingNotifications(context.squarePath);
84
91
  const activities = renderPublicTail(after.acts, intent.lastN, nowMs(), joinedName);
85
92
  const contextText = preamble.join('\n').trim();
86
93
  const fallback = hasAutomaticDeliveryIdentity()
87
94
  ? []
88
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();
89
97
  const output = [
90
- `● ${joinedName} stepped into the square`,
98
+ `● You stepped into the square`,
99
+ ...(isRejoin || scene === '' ? [] : ['', scene]),
91
100
  ...(isRejoin || contextText === '' ? [] : ['', 'context', contextText]),
92
101
  ...(activities === '' ? [] : ['', 'recent activity', activities]),
93
- ...(isRejoin ? [] : ['', `» ${participantCommandPrefix(context.squarePath, joinedName)} warmup`]),
94
102
  ...fallback,
95
103
  ].join('\n');
96
104
  return withPathOutput(context.squarePath, output, { participantCount: inSquareCount(after) });
@@ -102,11 +110,26 @@ export const joinCommand = {
102
110
  const joinedName = resolveRosterName(doc, intent.name);
103
111
  if (joinedName === undefined || !isCurrentlyJoined(doc.acts, joinedName))
104
112
  throw error;
113
+ const reconnect = localParticipantOwner(context.squarePath, joinedName) !== undefined;
114
+ if (!intent.kick && !reconnect) {
115
+ fail([
116
+ `✕ ${joinedName} shoos you out of the square`,
117
+ ` · a same-named participant stands here — the name is taken`,
118
+ ` · --kick banishes her and the name becomes yours`,
119
+ `» ${participantCommandPrefix(context.squarePath, joinedName)} join --kick`,
120
+ ].join('\n'));
121
+ }
105
122
  recordLocalJoin(joinedName, context.squarePath);
123
+ await sweepPendingNotifications(context.squarePath);
106
124
  const fallback = hasAutomaticDeliveryIdentity()
107
125
  ? ''
108
126
  : `\n» ${participantCommandPrefix(context.squarePath, joinedName)} catch --idle 30m\n no session delivery detected — keep this catch open for new activity`;
109
- return withPathOutput(context.squarePath, `● ${joinedName} is already in the square${fallback}`, { participantCount: inSquareCount(doc) });
127
+ const line = reconnect && !intent.kick
128
+ ? `● you are already in the square`
129
+ : `✓ you banished the original ${joinedName} — the name is yours`;
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) });
110
133
  }
111
134
  },
112
135
  present: (result) => process.stdout.write(result),
@@ -114,7 +137,6 @@ export const joinCommand = {
114
137
  function parseActivity(argv, context) {
115
138
  let force = false;
116
139
  let noWait = false;
117
- let beside;
118
140
  let bell = false;
119
141
  let reply;
120
142
  const bodyArgs = [];
@@ -124,25 +146,21 @@ function parseActivity(argv, context) {
124
146
  force = true;
125
147
  else if (argument === '--no-wait')
126
148
  noWait = true;
127
- else if (argument === '--beside') {
128
- beside = requireValue(argv, index, argument);
129
- index += 1;
130
- }
149
+ else if (argument === '--beside')
150
+ fail('✕ express does not know --beside\n» square express --help');
131
151
  else if (argument === '--bell')
132
152
  bell = true;
133
153
  else if (argument === '--reply') {
134
- const value = requireValue(argv, index, argument).trim().match(/^(?:act_)?(\d+)$/i);
135
- if (!value || !Number.isSafeInteger(Number(value[1])))
136
- fail('Invalid --reply: expected an activity id like act_12 or 12.');
137
- 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;
138
158
  index += 1;
139
159
  }
140
160
  else
141
161
  bodyArgs.push(argument);
142
162
  }
143
- if (bell && beside !== undefined)
144
- fail('Invalid express options: --beside and --bell are mutually exclusive.');
145
- const reach = bell ? 'bell' : beside === undefined ? undefined : { beside };
163
+ const reach = bell ? 'bell' : undefined;
146
164
  if (bodyArgs.length !== 1) {
147
165
  if (bodyArgs.length === 0) {
148
166
  const piped = readPipedBodyFallback();
@@ -156,13 +174,14 @@ function parseActivity(argv, context) {
156
174
  export const expressCommand = {
157
175
  parse: parseActivity,
158
176
  async execute(intent, context) {
159
- const reachArg = intent.reach === 'bell' ? ' --bell' : intent.reach === undefined ? '' : ` --beside ${quoteShell(intent.reach.beside)}`;
177
+ await sweepPendingNotifications(context.squarePath);
178
+ const reachArg = intent.reach === 'bell' ? ' --bell' : '';
160
179
  await cmdActivity(context.squarePath, intent.name, intent.activity, resolveBody, {
161
180
  force: intent.force,
162
181
  noWait: intent.noWait,
163
182
  reach: intent.reach,
164
183
  reply: intent.reply,
165
- 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)}`} -`,
166
185
  });
167
186
  },
168
187
  present: () => { },
@@ -1,13 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { resolve } from 'node:path';
3
3
  import { setTimeout as sleep } from 'node:timers/promises';
4
- import { recordNotificationFailure } from '../notification-failures.js';
5
- import { notificationDeliveryWaitMs, processActNotificationsOnce } from '../notifications.js';
4
+ import { processActNotificationsOnce, wakeGraceMs } from '../notifications.js';
6
5
  function args(argv) {
7
6
  let squarePath;
8
7
  let actIndex;
9
8
  for (let index = 0; index < argv.length; index += 1) {
10
- if (argv[index] === '--square-path' && argv[index + 1] !== undefined)
9
+ if (argv[index] === '--location' && argv[index + 1] !== undefined)
11
10
  squarePath = resolve(argv[++index]);
12
11
  else if (argv[index] === '--act-index' && /^\d+$/.test(argv[index + 1] ?? ''))
13
12
  actIndex = Number(argv[++index]);
@@ -15,25 +14,16 @@ function args(argv) {
15
14
  throw new Error(`Unknown notify-once argument: ${argv[index]}`);
16
15
  }
17
16
  if (squarePath === undefined || actIndex === undefined)
18
- throw new Error('notify-once requires --square-path and --act-index.');
17
+ throw new Error('notify-once requires --location and --act-index.');
19
18
  return { squarePath, actIndex };
20
19
  }
21
20
  async function main() {
22
21
  if (process.env.SQUARE_DISABLE_PASEO_WAKE === '1')
23
22
  return;
24
23
  const { squarePath, actIndex } = args(process.argv.slice(2));
25
- await sleep(notificationDeliveryWaitMs());
24
+ await sleep(wakeGraceMs());
26
25
  await processActNotificationsOnce(squarePath, actIndex);
27
26
  }
28
- main().catch((error) => {
29
- const squarePath = process.argv.includes('--square-path') ? process.argv[process.argv.indexOf('--square-path') + 1] : undefined;
30
- if (squarePath) {
31
- recordNotificationFailure(squarePath, {
32
- actIndex: Number(process.argv[process.argv.indexOf('--act-index') + 1]) || 0,
33
- sink: 'worker',
34
- message: error instanceof Error ? error.message : String(error),
35
- diagnostic: { phase: 'worker' },
36
- });
37
- }
27
+ main().catch(() => {
38
28
  process.exitCode = 0;
39
29
  });
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,12 +41,21 @@ 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
- const result = validate(state, { kind: 'say', actor: name, at: now, body, ...(reach !== undefined ? { reach } : {}), ...(reply !== undefined ? { reply } : {}) }, { hardCap: doc.hardCap, throttlePerMinute: doc.throttlePerMinute, throttleWindowMs: THROTTLE_WINDOW_MS });
54
+ const result = validate(state, {
55
+ kind: 'say', actor: name, at: now, body,
56
+ ...(reach !== undefined ? { reach } : {}),
57
+ ...(reply !== undefined ? { reply } : {}),
58
+ }, { hardCap: doc.hardCap, throttlePerMinute: doc.throttlePerMinute, throttleWindowMs: THROTTLE_WINDOW_MS });
50
59
  if (!result.ok) {
51
60
  if (result.reason === 'done')
52
61
  throw new SquareError('conflict', `${name} is done; rejoin to express again`);
@@ -93,7 +102,8 @@ export function decideAct(doc, input) {
93
102
  }))
94
103
  .sort((a, b) => a.latestActivityAgeMs - b.latestActivityAgeMs || a.name.localeCompare(b.name));
95
104
  const latestActivityAgeMs = activitySummaries[0]?.latestActivityAgeMs;
96
- const hasUnread = unreadPublic.length > 0 || unreadRoomChanges.length > 0;
105
+ const hasUnreadBlockingChange = unreadRoomChanges.some((act) => act.kind !== 'join');
106
+ const hasUnread = unreadPublic.length > 0 || hasUnreadBlockingChange;
97
107
  const hasFreshUnreadActivity = latestActivityAgeMs !== undefined && latestActivityAgeMs <= UNREAD_BLOCK_GRACE_MS;
98
108
  if (!force && hasUnread && !hasFreshUnreadActivity) {
99
109
  return { type: 'blocked', activitySummaries, unreadRoomChanges };
@@ -101,7 +111,11 @@ export function decideAct(doc, input) {
101
111
  const ownActCount = (current?.activityCount ?? 0) + 1;
102
112
  return {
103
113
  type: 'sent',
104
- act: { kind: 'say', actor: name, at: now, body, ...(reach !== undefined ? { reach } : {}), ...(reply !== undefined ? { reply } : {}) },
114
+ act: {
115
+ kind: 'say', actor: name, at: now, body,
116
+ ...(reach !== undefined ? { reach } : {}),
117
+ ...(reply !== undefined ? { reply } : {}),
118
+ },
105
119
  confirmation: `● heads turn your way — #${ownActCount}`,
106
120
  ownActCount,
107
121
  pendingPublic: unreadPublic,
@@ -116,7 +130,7 @@ export function coreHold(_doc, actor, body, now) {
116
130
  return { kind: 'hold', actor, at: now, body: body.replace(/\r\n/g, '\n').trim() };
117
131
  }
118
132
  export function coreResume(_doc, actor, now) {
119
- return { kind: 'resume', actor, at: now, body: '' };
133
+ return { kind: 'resume', actor, at: now };
120
134
  }
121
135
  function presenceFor(doc, snapshot, name, now) {
122
136
  if (snapshot?.done)