@astrosheep/square 0.3.5 → 0.3.7

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 (57) hide show
  1. package/codex-plugin/.codex-plugin/plugin.json +3 -2
  2. package/codex-plugin/hooks/hooks.json +3 -14
  3. package/dist/activity-feed.js +26 -18
  4. package/dist/activity.js +10 -10
  5. package/dist/artifact.js +138 -203
  6. package/dist/boundary-presentation.js +77 -0
  7. package/dist/claude-hook.js +4 -94
  8. package/dist/cli/context.js +7 -7
  9. package/dist/cli/maintenance-commands.js +10 -26
  10. package/dist/cli/meta-commands.js +3 -6
  11. package/dist/cli/observation-commands.js +48 -53
  12. package/dist/cli/program.js +4 -4
  13. package/dist/cli/registry.js +5 -5
  14. package/dist/cli/square-commands.js +27 -20
  15. package/dist/cmd/notify-once.js +23 -21
  16. package/dist/codex-hook.js +22 -0
  17. package/dist/compact.js +1 -1
  18. package/dist/decisions.js +61 -88
  19. package/dist/delivery-health.js +104 -210
  20. package/dist/delivery.js +68 -18
  21. package/dist/doctor.js +9 -8
  22. package/dist/harness-claude.js +38 -245
  23. package/dist/harness-codex.js +82 -616
  24. package/dist/harness-stage.js +36 -0
  25. package/dist/harness.js +3 -5
  26. package/dist/help.js +43 -35
  27. package/dist/inbox.js +12 -11
  28. package/dist/index.js +10 -121
  29. package/dist/list.js +1 -1
  30. package/dist/model.js +0 -6
  31. package/dist/notification-failures.js +54 -0
  32. package/dist/notifications.js +47 -62
  33. package/dist/paseo-delivery.js +160 -0
  34. package/dist/paseo-state.js +31 -0
  35. package/dist/paseo-timeline.js +58 -188
  36. package/dist/presentation.js +57 -64
  37. package/dist/presented.js +9 -8
  38. package/dist/registry.js +55 -45
  39. package/dist/runtime.js +27 -84
  40. package/dist/square-application.js +135 -130
  41. package/dist/square-core.js +3 -11
  42. package/dist/stream.js +27 -126
  43. package/dist/wake-sink.js +3 -214
  44. package/dist/watch.js +65 -122
  45. package/extensions/square-opencode.js +8 -73
  46. package/extensions/square-pi.js +8 -132
  47. package/guides/architect.md +3 -3
  48. package/guides/participant.md +25 -16
  49. package/package.json +2 -2
  50. package/skills/brainstorm/SKILL.md +25 -32
  51. package/skills/square/.claude-plugin/plugin.json +1 -1
  52. package/skills/square/SKILL.md +39 -107
  53. package/skills/square/hooks/hooks.json +2 -13
  54. package/skills/square-feedback/SKILL.md +4 -4
  55. package/dist/harness-lifecycle.js +0 -102
  56. package/dist/square-store.js +0 -111
  57. package/dist/terminal.js +0 -125
@@ -1,88 +1,11 @@
1
- import { leaseOwnsNotification } from './delivery.js';
2
- import { notificationMessageId } from './delivery-health.js';
1
+ import { presentPendingAtBoundary } from './boundary-presentation.js';
3
2
  import { sessionInbox } from './inbox.js';
4
- import { participantCommandPrefix } from './presentation.js';
5
- import { presentOnce } from './presented.js';
6
- function pendingCount(inbox) {
7
- return inbox.reduce((total, membership) => total + membership.notifications.length, 0);
8
- }
9
- const INJECT_BODY_MAX = 2048;
10
- /** Let a fresh blocking catch own notifications it can deliver; hook injection remains the fallback. */
11
- export function deferToActiveCatch(inbox) {
12
- return inbox
13
- .map((membership) => {
14
- const lease = membership.catchLease;
15
- if (lease === undefined)
16
- return membership;
17
- return {
18
- ...membership,
19
- notifications: membership.notifications.filter((notification) => !leaseOwnsNotification(lease, notification)),
20
- };
21
- })
22
- .filter((membership) => membership.notifications.length > 0);
23
- }
24
- function injectBodyPreview(body, squarePath, name, actIndex) {
25
- const compact = body.replace(/\r\n/g, '\n');
26
- if (compact.length <= INJECT_BODY_MAX)
27
- 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}`;
30
- }
31
- export function renderClaudeInboxContext(inbox) {
32
- const count = pendingCount(inbox);
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
- }),
48
- // Body here is a cache only. Delivered is written solely by catch.
49
- '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
- 'Read and respond in the square before finishing the current turn.</system-reminder>',
51
- ].join('\n');
52
- }
53
- function nativeHookResponse(input, lookup, env) {
3
+ export function claudeHookResponse(input, lookup = sessionInbox, env = process.env) {
54
4
  if (typeof input.session_id !== 'string' || input.session_id === '')
55
5
  return undefined;
56
- if (input.hook_event_name !== 'UserPromptSubmit' && input.hook_event_name !== 'Stop') {
57
- return undefined;
58
- }
59
- if (input.hook_event_name === 'Stop' && input.stop_hook_active === true)
6
+ if (input.hook_event_name !== 'PostToolBatch')
60
7
  return undefined;
61
- // Delivery membership is only claimed by explicit participant actions (join/act/catch/...).
62
- // Inherited PASEO_AGENT_ID proves process ancestry, not conversational ownership.
63
- // Stop is the guaranteed "don't leave while undelivered" nudge; it does not consume presentation.
64
- if (input.hook_event_name === 'Stop') {
65
- const pending = lookup(input.session_id).filter((membership) => membership.notifications.length > 0);
66
- if (pendingCount(pending) === 0)
67
- return undefined;
68
- return { decision: 'block', reason: renderClaudeInboxContext(pending) };
69
- }
70
- return presentOnce(input.session_id, (sessionId) => deferToActiveCatch(lookup(sessionId)), (inbox) => ({
71
- hookSpecificOutput: {
72
- hookEventName: 'UserPromptSubmit',
73
- additionalContext: renderClaudeInboxContext(inbox),
74
- },
75
- }), env);
76
- }
77
- export function claudeHookResponse(input, lookup = sessionInbox, env = process.env) {
78
- return nativeHookResponse(input, lookup, env);
79
- }
80
- /** Codex shares Claude's turn-boundary protocol; keep a dedicated command for churn isolation. */
81
- export function codexHookResponse(input, lookup = sessionInbox, env = process.env) {
82
- return nativeHookResponse(input, lookup, env);
83
- }
84
- export function opencodeHookResponse(input, lookup = sessionInbox, env = process.env) {
85
- return nativeHookResponse(input, lookup, env);
8
+ return presentPendingAtBoundary(input.session_id, (context) => ({ hookSpecificOutput: { hookEventName: 'PostToolBatch', additionalContext: context } }), lookup, env);
86
9
  }
87
10
  export function runClaudeHook(inputText, env = process.env) {
88
11
  let input;
@@ -97,16 +20,3 @@ export function runClaudeHook(inputText, env = process.env) {
97
20
  const response = claudeHookResponse(input, sessionInbox, env);
98
21
  return response === undefined ? '' : `${JSON.stringify(response)}\n`;
99
22
  }
100
- export function runCodexHook(inputText, env = process.env) {
101
- let input;
102
- try {
103
- input = JSON.parse(inputText);
104
- }
105
- catch {
106
- return '';
107
- }
108
- if (input === null || typeof input !== 'object')
109
- return '';
110
- const response = codexHookResponse(input, sessionInbox, env);
111
- return response === undefined ? '' : `${JSON.stringify(response)}\n`;
112
- }
@@ -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
  };
@@ -1,5 +1,6 @@
1
1
  import { loadSquare } from '../artifact.js';
2
- import { runClaudeHook, runCodexHook } from '../claude-hook.js';
2
+ import { runClaudeHook } from '../claude-hook.js';
3
+ import { runCodexHook } from '../codex-hook.js';
3
4
  import { coreActivities, coreParticipants, coreStatus } from '../decisions.js';
4
5
  import { sessionInbox } from '../inbox.js';
5
6
  import { cmdListSquares } from '../list.js';
@@ -10,7 +11,7 @@ import { actId, inSquareCount, isCurrentlyJoined, nowMs, resolveRosterName, sayN
10
11
  import { cmdStream, cmdStreamNdjson } from '../stream.js';
11
12
  import { formatRelativeTime, formatTimestamp, parseTimeOrRelative } from '../time.js';
12
13
  import { cmdWatch } from '../watch.js';
13
- import { fail, parseDurationMs, parseNameList, parseNonNegativeInteger, parsePositiveInteger, readStdinSync, requireParticipant, requireValue, usage, } from './context.js';
14
+ import { fail, parseDurationMs, parseNameList, parseNonNegativeInteger, readStdinSync, requireParticipant, requireValue, usage, } from './context.js';
14
15
  export const listCommand = {
15
16
  parse: (argv) => argv,
16
17
  execute(argv, context) {
@@ -47,19 +48,13 @@ export const streamCommand = {
47
48
  export const catchCommand = {
48
49
  parse(argv, context) {
49
50
  const name = requireParticipant(context.name);
50
- let activityCount = 1;
51
51
  let idleMs;
52
52
  let mention;
53
- let force = false;
53
+ let replace = false;
54
54
  let now = false;
55
- let follow = false;
56
55
  const participants = [];
57
56
  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') {
57
+ if (argv[index] === '--from') {
63
58
  participants.push(...parseNameList(requireValue(argv, index, argv[index]), argv[index]));
64
59
  index += 1;
65
60
  }
@@ -77,25 +72,23 @@ export const catchCommand = {
77
72
  mention = name;
78
73
  }
79
74
  }
80
- else if (argv[index] === '-f' || argv[index] === '--force')
81
- force = true;
75
+ else if (argv[index] === '--replace')
76
+ replace = true;
82
77
  else if (argv[index] === '--now')
83
78
  now = true;
84
- else if (argv[index] === '--follow')
85
- follow = true;
86
79
  else
87
- usage(context.command);
80
+ fail(`✕ catch does not know ${argv[index]}\n» square catch --help`);
88
81
  }
89
- if (now && follow)
90
- fail('--follow cannot be combined with --now');
82
+ if (now === (idleMs !== undefined))
83
+ fail('catch requires exactly one mode: --now or --idle <duration>.');
84
+ if (replace && now)
85
+ fail('--replace can only be used with --idle.');
91
86
  return {
92
- activityCount,
93
87
  ...(participants.length > 0 ? { participants } : {}),
94
88
  ...(mention === undefined ? {} : { mention }),
95
89
  ...(idleMs === undefined ? {} : { idleMs }),
96
- ...(force ? { force } : {}),
90
+ ...(replace ? { replace } : {}),
97
91
  ...(now ? { now } : {}),
98
- ...(follow ? { follow } : {}),
99
92
  };
100
93
  },
101
94
  async execute(intent, context) {
@@ -106,7 +99,7 @@ export const catchCommand = {
106
99
  function parseActRef(value, flag) {
107
100
  const match = value.trim().match(/^(?:act_)?(\d+)$/i);
108
101
  if (!match)
109
- fail(`Invalid ${flag}: expected act id like act_12 or 12.`);
102
+ fail(`Invalid ${flag}: expected an activity id like act_12 or 12.`);
110
103
  return Number(match[1]);
111
104
  }
112
105
  function parseTimestamp(value, flag) {
@@ -115,7 +108,8 @@ function parseTimestamp(value, flag) {
115
108
  fail(`Invalid ${flag} timestamp: ${value}`);
116
109
  return timestamp;
117
110
  }
118
- function parseEcho(argv, viewer) {
111
+ function parseHistory(argv, context) {
112
+ const viewer = context.name;
119
113
  let lastN = 10;
120
114
  let lastNExplicit = false;
121
115
  let before;
@@ -125,7 +119,6 @@ function parseEcho(argv, viewer) {
125
119
  let beforeContext;
126
120
  let afterContext;
127
121
  let mention;
128
- let mentionsViewer = false;
129
122
  let pending = false;
130
123
  let full = false;
131
124
  let grep;
@@ -138,8 +131,15 @@ function parseEcho(argv, viewer) {
138
131
  const participants = [];
139
132
  for (let index = 0; index < argv.length; index++) {
140
133
  const flag = argv[index];
141
- if (flag === '--last' || flag === '--limit') {
142
- lastN = parsePositiveInteger(requireValue(argv, index, flag), flag);
134
+ if (flag === '--limit') {
135
+ const value = argv[index + 1];
136
+ const retry = `${commandPrefix(context.squarePath)} history --limit 30`;
137
+ if (value === undefined || value.startsWith('--'))
138
+ fail(`✕ --limit needs a positive number\n» ${retry}`);
139
+ if (!/^[1-9]\d*$/.test(value) || !Number.isSafeInteger(Number(value))) {
140
+ fail(`✕ --limit needs a positive number\n» ${retry}`);
141
+ }
142
+ lastN = Number(value);
143
143
  lastNExplicit = true;
144
144
  index += 1;
145
145
  }
@@ -147,11 +147,11 @@ function parseEcho(argv, viewer) {
147
147
  lastN = null;
148
148
  lastNExplicit = true;
149
149
  }
150
- else if (flag === '--by' || flag === '--from') {
150
+ else if (flag === '--from') {
151
151
  participants.push(...parseNameList(requireValue(argv, index, flag), flag));
152
152
  index += 1;
153
153
  }
154
- else if (flag === '--before' || flag === '--until') {
154
+ else if (flag === '--until') {
155
155
  before = parseTimestamp(requireValue(argv, index, flag), flag);
156
156
  index += 1;
157
157
  }
@@ -187,13 +187,6 @@ function parseEcho(argv, viewer) {
187
187
  mention = requireValue(argv, index, flag);
188
188
  index += 1;
189
189
  }
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
190
  else if (flag === '--pending')
198
191
  pending = true;
199
192
  else if (flag === '--grep') {
@@ -228,10 +221,10 @@ function parseEcho(argv, viewer) {
228
221
  else if (flag === '--json')
229
222
  json = true;
230
223
  else
231
- fail('✕ invalid arguments for echo');
224
+ fail(`✕ history does not know ${flag}\n» square history --help`);
232
225
  }
233
- if ((mentionsViewer || pending) && !viewer)
234
- fail('--mentions me / --pending require --as <name>.');
226
+ if (pending && !viewer)
227
+ fail('--pending requires --as <name>.');
235
228
  if (grep !== undefined && fixed !== undefined)
236
229
  fail('--grep and --fixed cannot be combined.');
237
230
  if (grep === '' || fixed === '')
@@ -248,7 +241,6 @@ function parseEcho(argv, viewer) {
248
241
  beforeContext,
249
242
  afterContext,
250
243
  mention,
251
- mentionsViewer,
252
244
  pending,
253
245
  viewer,
254
246
  full,
@@ -266,18 +258,19 @@ function renderFields(doc, item, fields) {
266
258
  switch (field) {
267
259
  case 'id': return actId(item.index);
268
260
  case 'author':
269
- case 'actor': return item.act.actor ?? '';
261
+ case 'actor': return item.actor ?? '';
270
262
  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)) : '';
263
+ case 'at': return formatTimestamp(item.at);
264
+ case 'kind': return item.kind;
265
+ case 'body': return 'body' in item && typeof item.body === 'string' ? item.body.replace(/\s+/g, ' ').trim() : '';
266
+ case 'number': return item.kind === 'say' ? String(sayNumberFor(doc.acts, item)) : '';
267
+ case 'reply': return item.kind === 'say' && item.reply !== undefined ? actId(item.reply) : '';
275
268
  default: return '';
276
269
  }
277
270
  }).join('\t');
278
271
  }
279
272
  function jsonLine(doc, item) {
280
- const act = item.act;
273
+ const act = item;
281
274
  return JSON.stringify({
282
275
  id: actId(item.index),
283
276
  index: item.index,
@@ -288,10 +281,11 @@ function jsonLine(doc, item) {
288
281
  body: 'body' in act && typeof act.body === 'string' ? act.body : '',
289
282
  number: act.kind === 'say' ? sayNumberFor(doc.acts, act) : null,
290
283
  reach: act.kind === 'say' ? act.reach ?? null : null,
284
+ reply: act.kind === 'say' && act.reply !== undefined ? actId(act.reply) : null,
291
285
  });
292
286
  }
293
- export const echoCommand = {
294
- parse(argv, context) { return parseEcho(argv, context.name); },
287
+ export const historyCommand = {
288
+ parse(argv, context) { return parseHistory(argv, context); },
295
289
  execute(options, context) {
296
290
  const doc = loadSquare(context.squarePath);
297
291
  let events = coreActivities(doc, options);
@@ -333,14 +327,15 @@ export const participantsCommand = {
333
327
  usage(context.command); return undefined; },
334
328
  execute(_intent, context) {
335
329
  const doc = loadSquare(context.squarePath);
336
- const result = coreParticipants(doc, nowMs());
337
- const lines = result.participants.map((participant) => {
330
+ const now = nowMs();
331
+ const participants = coreParticipants(doc, now);
332
+ const lines = participants.map((participant) => {
338
333
  const glyph = participant.state === 'done' ? '×' : participant.presence === 'watching' ? '◎' : participant.activityCount > 0 ? '●' : '○';
339
334
  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}`;
335
+ const last = participant.lastActiveAt === undefined ? '—' : formatRelativeTime(participant.lastActiveAt, now);
336
+ return ` ${glyph} ${participant.name} · ${state} · ${participant.activityCount} ${participant.activityCount === 1 ? 'activity' : 'activities'} · ${last}`;
342
337
  });
343
- const participantCount = result.participants.filter((participant) => participant.state === 'active').length;
338
+ const participantCount = participants.filter((participant) => participant.state === 'active').length;
344
339
  return withPathOutput(context.squarePath, ['participants', ...lines].join('\n'), {
345
340
  participantCount,
346
341
  });
@@ -365,7 +360,7 @@ export const statusCommand = {
365
360
  ? '◎'
366
361
  : participant.activityCount > 0 ? '●' : '○';
367
362
  const summary = participant.activityCount > 0
368
- ? `${participant.activityCount} act${participant.activityCount === 1 ? '' : 's'} · ${participant.lastActiveAt === undefined
363
+ ? `${participant.activityCount} ${participant.activityCount === 1 ? 'activity' : 'activities'} · ${participant.lastActiveAt === undefined
369
364
  ? 'just now'
370
365
  : formatRelativeTime(participant.lastActiveAt, result.now)}`
371
366
  : `quiet · ${participant.lastActiveAt === undefined ? '—' : formatRelativeTime(participant.lastActiveAt, result.now)}`;
@@ -401,7 +396,7 @@ export const statusCommand = {
401
396
  : [` ${visible.replace(/\n/g, '\n ')}`];
402
397
  if (visible.includes('more chars') && result.latestAct !== undefined) {
403
398
  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`);
399
+ latest.push(`» ${prefix} history --at ${actId(result.latestAct)} -C 2 --full`);
405
400
  }
406
401
  const output = [
407
402
  `${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 },