@astrosheep/square 0.3.5 → 0.3.6

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 (51) hide show
  1. package/codex-plugin/.codex-plugin/plugin.json +3 -2
  2. package/dist/activity-feed.js +26 -18
  3. package/dist/activity.js +9 -10
  4. package/dist/artifact.js +126 -202
  5. package/dist/claude-hook.js +45 -21
  6. package/dist/cli/context.js +7 -7
  7. package/dist/cli/maintenance-commands.js +10 -26
  8. package/dist/cli/meta-commands.js +3 -6
  9. package/dist/cli/observation-commands.js +44 -52
  10. package/dist/cli/program.js +4 -4
  11. package/dist/cli/registry.js +5 -5
  12. package/dist/cli/square-commands.js +16 -18
  13. package/dist/cmd/notify-once.js +23 -21
  14. package/dist/compact.js +1 -1
  15. package/dist/decisions.js +53 -86
  16. package/dist/delivery-health.js +104 -210
  17. package/dist/delivery.js +68 -18
  18. package/dist/doctor.js +9 -8
  19. package/dist/harness-claude.js +38 -245
  20. package/dist/harness-codex.js +82 -616
  21. package/dist/harness-stage.js +36 -0
  22. package/dist/harness.js +3 -5
  23. package/dist/help.js +43 -35
  24. package/dist/inbox.js +12 -11
  25. package/dist/index.js +9 -121
  26. package/dist/list.js +1 -1
  27. package/dist/model.js +0 -6
  28. package/dist/notification-failures.js +54 -0
  29. package/dist/notifications.js +47 -62
  30. package/dist/paseo-timeline.js +58 -188
  31. package/dist/presentation.js +55 -63
  32. package/dist/presented.js +9 -8
  33. package/dist/registry.js +55 -45
  34. package/dist/runtime.js +27 -84
  35. package/dist/square-application.js +135 -130
  36. package/dist/square-core.js +3 -11
  37. package/dist/stream.js +27 -126
  38. package/dist/wake-sink.js +134 -188
  39. package/dist/watch.js +65 -122
  40. package/extensions/square-opencode.js +1 -1
  41. package/extensions/square-pi.js +8 -130
  42. package/guides/architect.md +3 -3
  43. package/guides/participant.md +25 -16
  44. package/package.json +2 -2
  45. package/skills/brainstorm/SKILL.md +25 -32
  46. package/skills/square/.claude-plugin/plugin.json +1 -1
  47. package/skills/square/SKILL.md +39 -107
  48. package/skills/square-feedback/SKILL.md +4 -4
  49. package/dist/harness-lifecycle.js +0 -102
  50. package/dist/square-store.js +0 -111
  51. package/dist/terminal.js +0 -125
@@ -1,12 +1,13 @@
1
1
  import { leaseOwnsNotification } from './delivery.js';
2
- import { notificationMessageId } from './delivery-health.js';
2
+ import { notificationMessageId } from './delivery.js';
3
3
  import { sessionInbox } from './inbox.js';
4
4
  import { participantCommandPrefix } from './presentation.js';
5
5
  import { presentOnce } from './presented.js';
6
6
  function pendingCount(inbox) {
7
7
  return inbox.reduce((total, membership) => total + membership.notifications.length, 0);
8
8
  }
9
- const INJECT_BODY_MAX = 2048;
9
+ const INJECT_BODY_MAX = 200;
10
+ const INJECT_TOTAL_MAX = 1200;
10
11
  /** Let a fresh blocking catch own notifications it can deliver; hook injection remains the fallback. */
11
12
  export function deferToActiveCatch(inbox) {
12
13
  return inbox
@@ -16,38 +17,61 @@ export function deferToActiveCatch(inbox) {
16
17
  return membership;
17
18
  return {
18
19
  ...membership,
19
- notifications: membership.notifications.filter((notification) => !leaseOwnsNotification(lease, notification)),
20
+ notifications: membership.notifications.filter((notification) => !leaseOwnsNotification(lease, { ...notification, recipient: membership.name })),
20
21
  };
21
22
  })
22
23
  .filter((membership) => membership.notifications.length > 0);
23
24
  }
24
- function injectBodyPreview(body, squarePath, name, actIndex) {
25
+ function injectBodyPreview(body) {
25
26
  const compact = body.replace(/\r\n/g, '\n');
26
27
  if (compact.length <= INJECT_BODY_MAX)
27
28
  return compact;
28
- const pointer = `${participantCommandPrefix(squarePath, name)} echo --ids act_${actIndex} --full`;
29
- return `${compact.slice(0, INJECT_BODY_MAX).trimEnd()}\n… [truncated] full echo: ${pointer}`;
29
+ return `${compact.slice(0, INJECT_BODY_MAX).trimEnd()}\n… [truncated; run catch --now]`;
30
30
  }
31
31
  export function renderClaudeInboxContext(inbox) {
32
32
  const count = pendingCount(inbox);
33
33
  const noun = count === 1 ? 'notification' : 'notifications';
34
- return [
35
- `<system-reminder source="square">You have ${count} unread Square ${noun}.`,
36
- ...inbox.flatMap((membership) => {
37
- const command = `${participantCommandPrefix(membership.squarePath, membership.name)} catch --now`;
38
- return membership.notifications.map((notification) => {
39
- const id = notificationMessageId(membership.squarePath, notification.actIndex);
40
- const body = injectBodyPreview(notification.body, membership.squarePath, membership.name, notification.actIndex);
41
- return [
42
- `${id} · ${membership.squarePath}: @${membership.name} from @${notification.actor} (${notification.via})`,
43
- body,
44
- `Ack with: ${command}`,
45
- ].join('\n');
46
- });
47
- }),
34
+ const header = `<system-reminder source="square">You have ${count} unread Square ${noun}.`;
35
+ const footer = [
48
36
  // Body here is a cache only. Delivered is written solely by catch.
49
37
  'Ids are stable across turns. If you already acted on an id, do not repeat the action; still run catch --now to mark delivered.',
50
38
  'Read and respond in the square before finishing the current turn.</system-reminder>',
39
+ ];
40
+ const queued = inbox.flatMap((membership) => membership.notifications.map((notification) => ({ membership, notification })));
41
+ const blocks = [];
42
+ let omitted = 0;
43
+ for (const [index, entry] of queued.entries()) {
44
+ const { membership, notification } = entry;
45
+ const command = `${participantCommandPrefix(membership.squarePath, membership.name)} catch --now`;
46
+ const id = notificationMessageId(membership.squarePath, notification.actIndex);
47
+ const block = [
48
+ `${id} · ${membership.squarePath}: @${membership.name} from @${notification.actor} (${notification.route})`,
49
+ injectBodyPreview(notification.body),
50
+ `Ack with: ${command}`,
51
+ ].join('\n');
52
+ const omittedAfter = omitted + queued.length - index - 1;
53
+ const prospective = [
54
+ header,
55
+ ...blocks,
56
+ block,
57
+ ...(omittedAfter > 0
58
+ ? [`… ${omittedAfter} unread ${omittedAfter === 1 ? 'notification' : 'notifications'} omitted. Run catch --now to receive them.`]
59
+ : []),
60
+ ...footer,
61
+ ].join('\n');
62
+ if (prospective.length > INJECT_TOTAL_MAX) {
63
+ omitted += 1;
64
+ continue;
65
+ }
66
+ blocks.push(block);
67
+ }
68
+ return [
69
+ header,
70
+ ...blocks,
71
+ ...(omitted > 0
72
+ ? [`… ${omitted} unread ${omitted === 1 ? 'notification' : 'notifications'} omitted. Run catch --now to receive them.`]
73
+ : []),
74
+ ...footer,
51
75
  ].join('\n');
52
76
  }
53
77
  function nativeHookResponse(input, lookup, env) {
@@ -58,7 +82,7 @@ function nativeHookResponse(input, lookup, env) {
58
82
  }
59
83
  if (input.hook_event_name === 'Stop' && input.stop_hook_active === true)
60
84
  return undefined;
61
- // Delivery membership is only claimed by explicit participant actions (join/act/catch/...).
85
+ // Delivery membership is only claimed by explicit participant actions (join/express/catch/...).
62
86
  // Inherited PASEO_AGENT_ID proves process ancestry, not conversational ownership.
63
87
  // Stop is the guaranteed "don't leave while undelivered" nudge; it does not consume presentation.
64
88
  if (input.hook_event_name === 'Stop') {
@@ -3,7 +3,7 @@ import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { loadSquare } from '../artifact.js';
5
5
  import { commandUsageHint } from '../help.js';
6
- import { parseParticipantList, validateParticipantName } from '../model.js';
6
+ import { parseParticipantList, validateName } from '../model.js';
7
7
  export const DEFAULT_SQUARE_PATH = '.square/SQUARE.md';
8
8
  export function readStdinSync() {
9
9
  try {
@@ -65,13 +65,13 @@ export function parseNonNegativeInteger(value, flag) {
65
65
  return parsed;
66
66
  }
67
67
  export function parseHardCap(value) {
68
- if (value === '-1')
68
+ if (value === 'unlimited')
69
69
  return null;
70
70
  if (!/^[1-9]\d*$/.test(value))
71
- fail('Invalid build option: --cap must be a positive integer or -1.');
71
+ fail('Invalid build option: --cap must be a positive integer or unlimited.');
72
72
  const parsed = Number(value);
73
73
  if (!Number.isSafeInteger(parsed))
74
- fail('Invalid build option: --cap must be a positive integer or -1.');
74
+ fail('Invalid build option: --cap must be a positive integer or unlimited.');
75
75
  return parsed;
76
76
  }
77
77
  export function parseNameList(value, flag) {
@@ -79,13 +79,13 @@ export function parseNameList(value, flag) {
79
79
  if (names.length === 0)
80
80
  fail(`Invalid ${flag}: expected at least one participant name.`);
81
81
  for (const name of names)
82
- validateParticipantName(name);
82
+ validateName(name);
83
83
  return names;
84
84
  }
85
85
  export function requireParticipant(name) {
86
86
  if (!name)
87
87
  fail('Missing required option: --as <name>.');
88
- validateParticipantName(name);
88
+ validateName(name);
89
89
  return name;
90
90
  }
91
91
  function resolveDefaultSquarePath() {
@@ -130,7 +130,7 @@ export function parseGlobalArgs(rawArgs) {
130
130
  }
131
131
  }
132
132
  if (name !== undefined)
133
- validateParticipantName(name);
133
+ validateName(name);
134
134
  const explicitSquarePath = requestedPath !== undefined;
135
135
  const command = args[0];
136
136
  const resolved = !explicitSquarePath && !['ls', 'list', 'version'].includes(command ?? '')
@@ -1,11 +1,11 @@
1
1
  import fs from 'node:fs';
2
2
  import { diagnoseSquare, loadSquare } from '../artifact.js';
3
- import { doctorDeliveryHealth, findStalePendingMentions } from '../delivery-health.js';
4
3
  import { renderDoctorClean, renderDoctorProblems, renderDoctorRepaired, renderDoctorUnfixable, withPathOutput, } from '../presentation.js';
5
4
  import { inSquareCount } from '../runtime.js';
6
- import { reconcileBacklog, repairSquare } from '../square-application.js';
5
+ import { repairSquare } from '../square-application.js';
7
6
  import { SquareError } from '../model.js';
8
- import { fail, usage } from './context.js';
7
+ import { pruneRegistry } from '../registry.js';
8
+ import { usage } from './context.js';
9
9
  function readSquareText(squarePath) {
10
10
  try {
11
11
  return fs.readFileSync(squarePath, 'utf8');
@@ -22,13 +22,10 @@ function quarantinePath(squarePath) {
22
22
  export const doctorCommand = {
23
23
  parse(argv, context) {
24
24
  let fix = false;
25
- let reconcileBacklog = false;
26
25
  for (let index = 0; index < argv.length; index++) {
27
26
  const argument = argv[index];
28
27
  if (argument === '--fix')
29
28
  fix = true;
30
- else if (argument === 'reconcile-backlog')
31
- reconcileBacklog = true;
32
29
  else if (argument === '--before') {
33
30
  index += 1;
34
31
  if (argv[index] === undefined)
@@ -37,9 +34,7 @@ export const doctorCommand = {
37
34
  else
38
35
  usage(context.command);
39
36
  }
40
- if (reconcileBacklog && !fix)
41
- fail('doctor reconcile-backlog requires --fix.');
42
- return { fix, reconcileBacklog };
37
+ return { fix };
43
38
  },
44
39
  async execute(intent, context) {
45
40
  if (!intent.fix) {
@@ -51,24 +46,9 @@ export const doctorCommand = {
51
46
  };
52
47
  }
53
48
  const summary = diagnosis.problems.length === 0 ? renderDoctorClean() : renderDoctorProblems(diagnosis.problems);
54
- const delivery = doctorDeliveryHealth(context.squarePath).join('\n');
55
- const stale = findStalePendingMentions(context.squarePath);
56
49
  return {
57
- output: withPathOutput(context.squarePath, `${summary}\n\n${delivery}`, { participantCount: inSquareCount(loadSquare(context.squarePath)) }),
58
- exitCode: diagnosis.problems.length === 0 && stale.length === 0 ? 0 : 1,
59
- };
60
- }
61
- if (intent.reconcileBacklog) {
62
- const result = await reconcileBacklog(context.squarePath);
63
- const delivery = doctorDeliveryHealth(context.squarePath).join('\n');
64
- return {
65
- output: withPathOutput(context.squarePath, [
66
- `✓ reconciled ${result.reconciled} backlog receipt(s) as delivered(reason=reconciled)`,
67
- result.skippedRecent > 0 ? `· left ${result.skippedRecent} recent liveness failure(s) untouched` : '· no recent liveness failures present',
68
- '',
69
- delivery,
70
- ].join('\n'), { participantCount: inSquareCount(loadSquare(context.squarePath)) }),
71
- exitCode: result.skippedRecent > 0 ? 1 : 0,
50
+ output: withPathOutput(context.squarePath, summary, { participantCount: inSquareCount(loadSquare(context.squarePath)) }),
51
+ exitCode: diagnosis.problems.length === 0 ? 0 : 1,
72
52
  };
73
53
  }
74
54
  const repair = await repairSquare(context.squarePath);
@@ -79,6 +59,10 @@ export const doctorCommand = {
79
59
  };
80
60
  }
81
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
+ }
82
66
  const sidecar = quarantinePath(context.squarePath);
83
67
  return {
84
68
  output: withPathOutput(context.squarePath, renderDoctorRepaired(repaired.actions, repaired.quarantinedBlocks.length, repaired.quarantinedBlocks.length > 0 ? sidecar : undefined), { participantCount: inSquareCount(loadSquare(context.squarePath)) }),
@@ -1,6 +1,5 @@
1
- import fs from 'node:fs';
2
- import { fileURLToPath } from 'node:url';
3
1
  import { renderGlobalHelp, renderSubcommandHelp } from '../help.js';
2
+ import { SQUARE_IDENTITY } from '../identity.js';
4
3
  import { fail } from './context.js';
5
4
  export const helpCommand = {
6
5
  parse(argv) {
@@ -13,7 +12,7 @@ export const helpCommand = {
13
12
  return renderGlobalHelp();
14
13
  const rendered = renderSubcommandHelp(intent.command);
15
14
  if (rendered === undefined)
16
- fail(`unknown command: ${intent.command}\nrun 'square help' to list every command`);
15
+ fail(`unknown command: ${intent.command}\nrun 'square help' to list available commands`);
17
16
  return rendered;
18
17
  },
19
18
  present: (result) => process.stdout.write(result),
@@ -23,9 +22,7 @@ export const versionCommand = {
23
22
  return undefined;
24
23
  },
25
24
  execute() {
26
- const packagePath = fileURLToPath(new URL('../../package.json', import.meta.url));
27
- const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
28
- return `${packageJson.version ?? 'unknown'}\n`;
25
+ return `${SQUARE_IDENTITY.packageVersion}\n`;
29
26
  },
30
27
  present: (result) => process.stdout.write(result),
31
28
  };
@@ -10,7 +10,7 @@ import { actId, inSquareCount, isCurrentlyJoined, nowMs, resolveRosterName, sayN
10
10
  import { cmdStream, cmdStreamNdjson } from '../stream.js';
11
11
  import { formatRelativeTime, formatTimestamp, parseTimeOrRelative } from '../time.js';
12
12
  import { cmdWatch } from '../watch.js';
13
- import { fail, parseDurationMs, parseNameList, parseNonNegativeInteger, parsePositiveInteger, readStdinSync, requireParticipant, requireValue, usage, } from './context.js';
13
+ import { fail, parseDurationMs, parseNameList, parseNonNegativeInteger, readStdinSync, requireParticipant, requireValue, usage, } from './context.js';
14
14
  export const listCommand = {
15
15
  parse: (argv) => argv,
16
16
  execute(argv, context) {
@@ -47,19 +47,13 @@ export const streamCommand = {
47
47
  export const catchCommand = {
48
48
  parse(argv, context) {
49
49
  const name = requireParticipant(context.name);
50
- let activityCount = 1;
51
50
  let idleMs;
52
51
  let mention;
53
- let force = false;
52
+ let replace = false;
54
53
  let now = false;
55
- let follow = false;
56
54
  const participants = [];
57
55
  for (let index = 0; index < argv.length; index++) {
58
- if (argv[index] === '--count') {
59
- activityCount = parsePositiveInteger(requireValue(argv, index, argv[index]), argv[index]);
60
- index += 1;
61
- }
62
- else if (argv[index] === '--by') {
56
+ if (argv[index] === '--from') {
63
57
  participants.push(...parseNameList(requireValue(argv, index, argv[index]), argv[index]));
64
58
  index += 1;
65
59
  }
@@ -77,25 +71,23 @@ export const catchCommand = {
77
71
  mention = name;
78
72
  }
79
73
  }
80
- else if (argv[index] === '-f' || argv[index] === '--force')
81
- force = true;
74
+ else if (argv[index] === '--replace')
75
+ replace = true;
82
76
  else if (argv[index] === '--now')
83
77
  now = true;
84
- else if (argv[index] === '--follow')
85
- follow = true;
86
78
  else
87
- usage(context.command);
79
+ fail(`✕ catch does not know ${argv[index]}\n» square catch --help`);
88
80
  }
89
- if (now && follow)
90
- fail('--follow cannot be combined with --now');
81
+ if (now === (idleMs !== undefined))
82
+ fail('catch requires exactly one mode: --now or --idle <duration>.');
83
+ if (replace && now)
84
+ fail('--replace can only be used with --idle.');
91
85
  return {
92
- activityCount,
93
86
  ...(participants.length > 0 ? { participants } : {}),
94
87
  ...(mention === undefined ? {} : { mention }),
95
88
  ...(idleMs === undefined ? {} : { idleMs }),
96
- ...(force ? { force } : {}),
89
+ ...(replace ? { replace } : {}),
97
90
  ...(now ? { now } : {}),
98
- ...(follow ? { follow } : {}),
99
91
  };
100
92
  },
101
93
  async execute(intent, context) {
@@ -106,7 +98,7 @@ export const catchCommand = {
106
98
  function parseActRef(value, flag) {
107
99
  const match = value.trim().match(/^(?:act_)?(\d+)$/i);
108
100
  if (!match)
109
- fail(`Invalid ${flag}: expected act id like act_12 or 12.`);
101
+ fail(`Invalid ${flag}: expected an activity id like act_12 or 12.`);
110
102
  return Number(match[1]);
111
103
  }
112
104
  function parseTimestamp(value, flag) {
@@ -115,7 +107,8 @@ function parseTimestamp(value, flag) {
115
107
  fail(`Invalid ${flag} timestamp: ${value}`);
116
108
  return timestamp;
117
109
  }
118
- function parseEcho(argv, viewer) {
110
+ function parseHistory(argv, context) {
111
+ const viewer = context.name;
119
112
  let lastN = 10;
120
113
  let lastNExplicit = false;
121
114
  let before;
@@ -125,7 +118,6 @@ function parseEcho(argv, viewer) {
125
118
  let beforeContext;
126
119
  let afterContext;
127
120
  let mention;
128
- let mentionsViewer = false;
129
121
  let pending = false;
130
122
  let full = false;
131
123
  let grep;
@@ -138,8 +130,15 @@ function parseEcho(argv, viewer) {
138
130
  const participants = [];
139
131
  for (let index = 0; index < argv.length; index++) {
140
132
  const flag = argv[index];
141
- if (flag === '--last' || flag === '--limit') {
142
- lastN = parsePositiveInteger(requireValue(argv, index, flag), flag);
133
+ if (flag === '--limit') {
134
+ const value = argv[index + 1];
135
+ const retry = `${commandPrefix(context.squarePath)} history --limit 30`;
136
+ if (value === undefined || value.startsWith('--'))
137
+ fail(`✕ --limit needs a positive number\n» ${retry}`);
138
+ if (!/^[1-9]\d*$/.test(value) || !Number.isSafeInteger(Number(value))) {
139
+ fail(`✕ --limit needs a positive number\n» ${retry}`);
140
+ }
141
+ lastN = Number(value);
143
142
  lastNExplicit = true;
144
143
  index += 1;
145
144
  }
@@ -147,11 +146,11 @@ function parseEcho(argv, viewer) {
147
146
  lastN = null;
148
147
  lastNExplicit = true;
149
148
  }
150
- else if (flag === '--by' || flag === '--from') {
149
+ else if (flag === '--from') {
151
150
  participants.push(...parseNameList(requireValue(argv, index, flag), flag));
152
151
  index += 1;
153
152
  }
154
- else if (flag === '--before' || flag === '--until') {
153
+ else if (flag === '--until') {
155
154
  before = parseTimestamp(requireValue(argv, index, flag), flag);
156
155
  index += 1;
157
156
  }
@@ -187,13 +186,6 @@ function parseEcho(argv, viewer) {
187
186
  mention = requireValue(argv, index, flag);
188
187
  index += 1;
189
188
  }
190
- else if (flag === '--mentions') {
191
- const value = requireValue(argv, index, flag);
192
- if (value !== 'me')
193
- fail(`Invalid --mentions: only 'me' is supported (got ${value}).`);
194
- mentionsViewer = true;
195
- index += 1;
196
- }
197
189
  else if (flag === '--pending')
198
190
  pending = true;
199
191
  else if (flag === '--grep') {
@@ -228,10 +220,10 @@ function parseEcho(argv, viewer) {
228
220
  else if (flag === '--json')
229
221
  json = true;
230
222
  else
231
- fail('✕ invalid arguments for echo');
223
+ fail(`✕ history does not know ${flag}\n» square history --help`);
232
224
  }
233
- if ((mentionsViewer || pending) && !viewer)
234
- fail('--mentions me / --pending require --as <name>.');
225
+ if (pending && !viewer)
226
+ fail('--pending requires --as <name>.');
235
227
  if (grep !== undefined && fixed !== undefined)
236
228
  fail('--grep and --fixed cannot be combined.');
237
229
  if (grep === '' || fixed === '')
@@ -248,7 +240,6 @@ function parseEcho(argv, viewer) {
248
240
  beforeContext,
249
241
  afterContext,
250
242
  mention,
251
- mentionsViewer,
252
243
  pending,
253
244
  viewer,
254
245
  full,
@@ -266,18 +257,18 @@ function renderFields(doc, item, fields) {
266
257
  switch (field) {
267
258
  case 'id': return actId(item.index);
268
259
  case 'author':
269
- case 'actor': return item.act.actor ?? '';
260
+ case 'actor': return item.actor ?? '';
270
261
  case 'ts':
271
- case 'at': return formatTimestamp(item.act.at);
272
- case 'kind': return item.act.kind;
273
- case 'body': return 'body' in item.act && typeof item.act.body === 'string' ? item.act.body.replace(/\s+/g, ' ').trim() : '';
274
- case 'number': return item.act.kind === 'say' ? String(sayNumberFor(doc.acts, item.act)) : '';
262
+ case 'at': return formatTimestamp(item.at);
263
+ case 'kind': return item.kind;
264
+ case 'body': return 'body' in item && typeof item.body === 'string' ? item.body.replace(/\s+/g, ' ').trim() : '';
265
+ case 'number': return item.kind === 'say' ? String(sayNumberFor(doc.acts, item)) : '';
275
266
  default: return '';
276
267
  }
277
268
  }).join('\t');
278
269
  }
279
270
  function jsonLine(doc, item) {
280
- const act = item.act;
271
+ const act = item;
281
272
  return JSON.stringify({
282
273
  id: actId(item.index),
283
274
  index: item.index,
@@ -290,8 +281,8 @@ function jsonLine(doc, item) {
290
281
  reach: act.kind === 'say' ? act.reach ?? null : null,
291
282
  });
292
283
  }
293
- export const echoCommand = {
294
- parse(argv, context) { return parseEcho(argv, context.name); },
284
+ export const historyCommand = {
285
+ parse(argv, context) { return parseHistory(argv, context); },
295
286
  execute(options, context) {
296
287
  const doc = loadSquare(context.squarePath);
297
288
  let events = coreActivities(doc, options);
@@ -333,14 +324,15 @@ export const participantsCommand = {
333
324
  usage(context.command); return undefined; },
334
325
  execute(_intent, context) {
335
326
  const doc = loadSquare(context.squarePath);
336
- const result = coreParticipants(doc, nowMs());
337
- const lines = result.participants.map((participant) => {
327
+ const now = nowMs();
328
+ const participants = coreParticipants(doc, now);
329
+ const lines = participants.map((participant) => {
338
330
  const glyph = participant.state === 'done' ? '×' : participant.presence === 'watching' ? '◎' : participant.activityCount > 0 ? '●' : '○';
339
331
  const state = participant.state === 'done' ? 'done' : participant.presence === 'watching' ? 'catching' : participant.state;
340
- const last = participant.lastActiveAt === undefined ? '—' : formatRelativeTime(participant.lastActiveAt, result.now);
341
- return ` ${glyph} ${participant.name} · ${state} · ${participant.activityCount} act${participant.activityCount === 1 ? '' : 's'} · ${last}`;
332
+ const last = participant.lastActiveAt === undefined ? '—' : formatRelativeTime(participant.lastActiveAt, now);
333
+ return ` ${glyph} ${participant.name} · ${state} · ${participant.activityCount} ${participant.activityCount === 1 ? 'activity' : 'activities'} · ${last}`;
342
334
  });
343
- const participantCount = result.participants.filter((participant) => participant.state === 'active').length;
335
+ const participantCount = participants.filter((participant) => participant.state === 'active').length;
344
336
  return withPathOutput(context.squarePath, ['participants', ...lines].join('\n'), {
345
337
  participantCount,
346
338
  });
@@ -365,7 +357,7 @@ export const statusCommand = {
365
357
  ? '◎'
366
358
  : participant.activityCount > 0 ? '●' : '○';
367
359
  const summary = participant.activityCount > 0
368
- ? `${participant.activityCount} act${participant.activityCount === 1 ? '' : 's'} · ${participant.lastActiveAt === undefined
360
+ ? `${participant.activityCount} ${participant.activityCount === 1 ? 'activity' : 'activities'} · ${participant.lastActiveAt === undefined
369
361
  ? 'just now'
370
362
  : formatRelativeTime(participant.lastActiveAt, result.now)}`
371
363
  : `quiet · ${participant.lastActiveAt === undefined ? '—' : formatRelativeTime(participant.lastActiveAt, result.now)}`;
@@ -401,7 +393,7 @@ export const statusCommand = {
401
393
  : [` ${visible.replace(/\n/g, '\n ')}`];
402
394
  if (visible.includes('more chars') && result.latestAct !== undefined) {
403
395
  const prefix = context.name === undefined ? commandPrefix(context.squarePath) : participantCommandPrefix(context.squarePath, context.name);
404
- latest.push(`» ${prefix} echo --at ${actId(result.latestAct)} -C 2 --full`);
396
+ latest.push(`» ${prefix} history --at ${actId(result.latestAct)} -C 2 --full`);
405
397
  }
406
398
  const output = [
407
399
  `${result.activeCount} active · ${result.doneCount} done · cap ${cap} · throttle ${result.throttlePerMinute === undefined ? 'none' : `${result.throttlePerMinute}/min`}`,
@@ -4,9 +4,9 @@ import { defaultContext, parseGlobalArgs } from './context.js';
4
4
  import { refreshLocalRegistration } from './observation-commands.js';
5
5
  import { executeRegisteredCommand, findCommand } from './registry.js';
6
6
  function isMutatingCommand(command, argv) {
7
- if (['build', 'join', 'catch', 'act', 'done', 'hold', 'resume', 'compact'].includes(command))
7
+ if (['build', 'join', 'catch', 'express', 'done', 'hold', 'resume', 'compact'].includes(command))
8
8
  return true;
9
- return command === 'doctor' && argv.some((argument) => argument === '--fix' || argument === 'reconcile-backlog');
9
+ return command === 'doctor' && argv.includes('--fix');
10
10
  }
11
11
  function handleSquareError(error) {
12
12
  if (error instanceof SquareError) {
@@ -34,10 +34,10 @@ export async function runCli(rawArgs = process.argv.slice(2)) {
34
34
  process.exit(2);
35
35
  }
36
36
  if (!parsed.explicitSquarePath && parsed.multipleSquares && isMutatingCommand(command, parsed.args.slice(1))) {
37
- process.stderr.write('✕ more than one square is active here; choose the path before changing or consuming activity.\n» square ls\n');
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 (['act', 'catch', 'done', 'hold', 'resume'].includes(command)) {
40
+ if (['express', 'catch', 'done', 'hold', 'resume'].includes(command)) {
41
41
  refreshLocalRegistration(parsed.squarePath, parsed.name);
42
42
  }
43
43
  await executeRegisteredCommand(command, parsed.args.slice(1), defaultContext(command, parsed.squarePath, parsed.name));
@@ -1,26 +1,26 @@
1
- import { actCommand, buildCommand, compactCommand, doneCommand, holdCommand, joinCommand, resumeCommand } from './square-commands.js';
1
+ import { buildCommand, compactCommand, doneCommand, expressCommand, holdCommand, joinCommand, resumeCommand } from './square-commands.js';
2
2
  import { doctorCommand } from './maintenance-commands.js';
3
3
  import { harnessCommand } from './harness-command.js';
4
4
  import { helpCommand, versionCommand } from './meta-commands.js';
5
- import { catchCommand, claudeHookCommand, codexHookCommand, echoCommand, inboxCommand, listCommand, participantsCommand, statusCommand, streamCommand, warmupCommand, } from './observation-commands.js';
5
+ import { catchCommand, claudeHookCommand, codexHookCommand, historyCommand, inboxCommand, listCommand, participantsCommand, statusCommand, streamCommand, warmupCommand, } 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 },
9
- { names: ['ls', 'list'], spec: listCommand },
9
+ { names: ['list', 'ls'], spec: listCommand },
10
10
  { names: ['join'], spec: joinCommand },
11
11
  { names: ['stream'], spec: streamCommand },
12
12
  { names: ['inbox'], spec: inboxCommand },
13
13
  { names: ['claude-hook'], spec: claudeHookCommand },
14
14
  { names: ['codex-hook'], spec: codexHookCommand },
15
15
  { names: ['catch'], spec: catchCommand },
16
- { names: ['act'], spec: actCommand },
16
+ { names: ['express'], spec: expressCommand },
17
17
  { names: ['done'], spec: doneCommand },
18
18
  { names: ['hold'], spec: holdCommand },
19
19
  { names: ['resume'], spec: resumeCommand },
20
20
  { names: ['harness'], spec: harnessCommand },
21
21
  { names: ['compact'], spec: compactCommand },
22
22
  { names: ['doctor'], spec: doctorCommand },
23
- { names: ['echo'], spec: echoCommand },
23
+ { names: ['history'], spec: historyCommand },
24
24
  { names: ['warmup'], spec: warmupCommand },
25
25
  { names: ['status'], spec: statusCommand },
26
26
  { names: ['participants'], spec: participantsCommand },