@astrosheep/square 0.3.34 → 0.3.36

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 (61) hide show
  1. package/claude-plugin/.claude-plugin/plugin.json +1 -1
  2. package/claude-plugin/skills/square/SKILL.md +10 -10
  3. package/codex-plugin/.codex-plugin/plugin.json +1 -1
  4. package/codex-plugin/hooks/hooks.json +1 -1
  5. package/dist/activity-feed.js +1 -1
  6. package/dist/activity.d.ts +1 -0
  7. package/dist/activity.js +6 -3
  8. package/dist/artifact.js +2 -1
  9. package/dist/automatic-session.js +17 -1
  10. package/dist/boundary-presentation.d.ts +1 -0
  11. package/dist/boundary-presentation.js +12 -1
  12. package/dist/catch-decisions.d.ts +4 -0
  13. package/dist/catch-decisions.js +23 -7
  14. package/dist/cli/context.d.ts +1 -0
  15. package/dist/cli/context.js +26 -1
  16. package/dist/cli/observation-commands.d.ts +7 -1
  17. package/dist/cli/observation-commands.js +149 -63
  18. package/dist/cli/program.js +8 -2
  19. package/dist/cli/square-commands.d.ts +3 -1
  20. package/dist/cli/square-commands.js +42 -16
  21. package/dist/codex-queue.d.ts +1 -0
  22. package/dist/codex-queue.js +1 -1
  23. package/dist/decisions.d.ts +1 -0
  24. package/dist/decisions.js +29 -14
  25. package/dist/delivery-health.d.ts +1 -0
  26. package/dist/delivery-health.js +19 -6
  27. package/dist/delivery.d.ts +2 -0
  28. package/dist/delivery.js +1 -0
  29. package/dist/harness-links.js +15 -3
  30. package/dist/harness.js +3 -1
  31. package/dist/help.js +25 -16
  32. package/dist/inbox.d.ts +2 -0
  33. package/dist/inbox.js +4 -2
  34. package/dist/list.js +77 -24
  35. package/dist/model.d.ts +1 -3
  36. package/dist/paseo-connection.js +9 -3
  37. package/dist/paseo-state.d.ts +4 -1
  38. package/dist/paseo-state.js +2 -2
  39. package/dist/presentation.d.ts +4 -0
  40. package/dist/presentation.js +41 -12
  41. package/dist/runtime.d.ts +1 -0
  42. package/dist/square-actions.d.ts +5 -0
  43. package/dist/square-actions.js +31 -7
  44. package/dist/square-core.d.ts +11 -1
  45. package/dist/square-core.js +12 -9
  46. package/dist/square-facade.d.ts +5 -2
  47. package/dist/square-file-adapter.d.ts +1 -1
  48. package/dist/square-file-adapter.js +11 -4
  49. package/dist/square-wiring.d.ts +1 -0
  50. package/dist/square-wiring.js +5 -1
  51. package/dist/stream.d.ts +8 -1
  52. package/dist/stream.js +17 -6
  53. package/dist/views.d.ts +11 -5
  54. package/dist/views.js +42 -16
  55. package/dist/wake-sink.d.ts +2 -0
  56. package/dist/wake-sink.js +10 -1
  57. package/dist/watch.js +26 -9
  58. package/extensions/square-pi.js +41 -7
  59. package/package.json +1 -1
  60. package/skills/brainstorm/SKILL.md +8 -6
  61. package/skills/square/SKILL.md +10 -10
package/dist/decisions.js CHANGED
@@ -1,16 +1,15 @@
1
1
  import { SquareError, sameName, validateName, } from './model.js';
2
2
  import { participantIdentity } from './participant-identity.js';
3
- import { UNREAD_BLOCK_GRACE_MS, actId, actStableIndex, foldedState, freshWatchLease, publicActs, resolveRosterName, rosterNames, THROTTLE_WINDOW_MS, } from './runtime.js';
3
+ import { UNREAD_BLOCK_GRACE_MS, actId, actStableIndex, foldedState, freshWatchLease, publicActs, resolveRosterName, THROTTLE_WINDOW_MS, } from './runtime.js';
4
4
  import { actDelta, directedPeerSays } from './activity-feed.js';
5
- import { formatActivityId, isIgnored, isListening, listeningTo, validate } from './square-core.js';
5
+ import { formatActivityId, isIgnored, isListening, listeningTo, MAX_IDENTITY_SET_SIZE, validate } from './square-core.js';
6
6
  import { deriveDeliveryModel } from './delivery.js';
7
7
  import { compileSearchPattern } from './search.js';
8
8
  export function resolveKnownName(squareState, name) {
9
9
  validateName(name);
10
10
  const known = resolveRosterName(squareState, name);
11
11
  if (known === undefined) {
12
- const roster = rosterNames(squareState);
13
- throw new SquareError('invalid_args', `Unknown participant "${participantIdentity(name)}". Expected one of: ${roster.map(participantIdentity).join(', ')}.`);
12
+ throw new SquareError('invalid_args', `Unknown participant "${participantIdentity(name)}".`);
14
13
  }
15
14
  return known;
16
15
  }
@@ -62,6 +61,8 @@ function requireStanding(squareState, act) {
62
61
  throw new SquareError('already_done', `${act.actor === undefined ? 'participant' : participantIdentity(act.actor)} is already done`);
63
62
  if (result.reason === 'not_joined')
64
63
  throw new SquareError('not_joined', `${act.actor === undefined ? 'participant' : participantIdentity(act.actor)} has not joined this square`);
64
+ if (result.reason === 'listening_limit')
65
+ throw new SquareError('invalid_args', `A participant can listen to at most ${result.limit} others`);
65
66
  throw new Error(`Unexpected standing validation result: ${result.reason}`);
66
67
  }
67
68
  const UNREAD_PREVIEW_LIMIT = 3;
@@ -72,6 +73,26 @@ export function decideAct(squareState, input) {
72
73
  if (body.trim() === '')
73
74
  throw new SquareError('invalid_args', 'express body cannot be empty');
74
75
  const reach = input.reach;
76
+ const state = foldedState(squareState);
77
+ const requestedMentions = input.mentions ?? [];
78
+ const mentionNames = [];
79
+ const joinedNames = state.participants.filter((participant) => participant.joined).map((participant) => participant.name);
80
+ for (const requested of requestedMentions) {
81
+ validateName(requested);
82
+ const resolved = joinedNames.find((candidate) => sameName(candidate, requested));
83
+ if (resolved === undefined) {
84
+ throw new SquareError('invalid_args', `Unknown mention target ${participantIdentity(requested)}.`);
85
+ }
86
+ if (!mentionNames.some((existing) => sameName(existing, resolved))) {
87
+ if (mentionNames.length >= MAX_IDENTITY_SET_SIZE) {
88
+ throw new SquareError('invalid_args', `An activity can mention at most ${MAX_IDENTITY_SET_SIZE} participants`);
89
+ }
90
+ mentionNames.push(resolved);
91
+ }
92
+ }
93
+ if (reach === 'bell' && mentionNames.length > 0) {
94
+ throw new SquareError('invalid_args', 'A bell cannot be combined with --mention.');
95
+ }
75
96
  const reply = input.reply;
76
97
  if (reply !== undefined) {
77
98
  if (!Number.isSafeInteger(reply) || reply < 0 || reply >= squareState.runtime.nextActIndex) {
@@ -79,16 +100,17 @@ export function decideAct(squareState, input) {
79
100
  throw new SquareError('invalid_args', `Unknown reply activity: ${label}`);
80
101
  }
81
102
  }
82
- const state = foldedState(squareState);
83
103
  const current = participantState(state, name);
84
104
  const result = validate(state, {
85
- kind: 'say', actor: name, at: now, body,
105
+ kind: 'say', actor: name, at: now, body, mentions: mentionNames,
86
106
  ...(reach !== undefined ? { reach } : {}),
87
107
  ...(reply !== undefined ? { reply } : {}),
88
108
  }, { hardCap: squareState.hardCap, throttlePerMinute: squareState.throttlePerMinute, throttleWindowMs: THROTTLE_WINDOW_MS });
89
109
  if (!result.ok) {
90
110
  if (result.reason === 'done')
91
111
  throw new SquareError('already_done', `${name} is already done`);
112
+ if (result.reason === 'mention_limit')
113
+ throw new SquareError('invalid_args', `An activity can mention at most ${result.limit} participants`);
92
114
  if (result.reason === 'held')
93
115
  return { type: 'held', reason: result.hold.reason };
94
116
  if (result.reason === 'hard_cap')
@@ -142,7 +164,7 @@ export function decideAct(squareState, input) {
142
164
  return {
143
165
  type: 'sent',
144
166
  act: {
145
- kind: 'say', actor: name, at: now, body,
167
+ kind: 'say', actor: name, at: now, body, mentions: mentionNames,
146
168
  ...(reach !== undefined ? { reach } : {}),
147
169
  ...(reply !== undefined ? { reply } : {}),
148
170
  },
@@ -254,7 +276,6 @@ export function coreParticipants(squareState, now, delivery) {
254
276
  export function coreActivities(squareState, opts, suppliedDelivery) {
255
277
  const participants = opts.participants ?? [];
256
278
  const canonicalParticipants = participants.map((participant) => resolveKnownName(squareState, participant));
257
- const viewer = opts.viewer !== undefined ? resolveKnownName(squareState, opts.viewer) : undefined;
258
279
  let acts = [...squareState.acts];
259
280
  let delivery = suppliedDelivery;
260
281
  const projected = () => delivery ??= deriveDeliveryModel(squareState);
@@ -285,12 +306,6 @@ export function coreActivities(squareState, opts, suppliedDelivery) {
285
306
  const mention = resolveKnownName(squareState, opts.mention);
286
307
  acts = acts.filter((act) => act.kind === 'say' && projected().plan(act).some((item) => sameName(item.recipient, mention)));
287
308
  }
288
- if (opts.pending) {
289
- if (viewer === undefined)
290
- return [];
291
- const pendingIndexes = new Set(projected().pendingFor(viewer).map((notification) => notification.item.index));
292
- acts = acts.filter((act) => pendingIndexes.has(act.index));
293
- }
294
309
  const search = opts.grep !== undefined ? { pattern: opts.grep, fixed: false } : opts.fixed !== undefined ? { pattern: opts.fixed, fixed: true } : undefined;
295
310
  if (search !== undefined && search.pattern !== '') {
296
311
  // Search only the public activity model rendered by history, but include all
@@ -18,4 +18,5 @@ export declare function classifyDeliveryHealth(squarePath: string, opts: {
18
18
  now?: number;
19
19
  env?: NodeJS.ProcessEnv;
20
20
  }): Promise<DeliveryHealthItem[]>;
21
+ export declare function renderDeliveryHealth(items: readonly DeliveryHealthItem[]): string[];
21
22
  export declare function doctorDeliveryHealth(squarePath: string, graceMs: number, now?: number, env?: NodeJS.ProcessEnv): Promise<string[]>;
@@ -1,5 +1,5 @@
1
1
  import { formatActivityId } from './square-core.js';
2
- import { participantIdentity } from './presentation.js';
2
+ import { participantIdentity, truncateChars } from './presentation.js';
3
3
  import { formatDuration } from './time.js';
4
4
  import { wakeEvidence } from './wake-evidence.js';
5
5
  import { openSquare } from './square-file-adapter.js';
@@ -13,6 +13,8 @@ const DISPLAY_ORDER = [
13
13
  'unreachable',
14
14
  ];
15
15
  const ACTIONABLE = new Set(['wake-unknown', 'unreachable']);
16
+ const DETAIL_LIMIT = 20;
17
+ const DISPLAY_FIELD_LIMIT = 160;
16
18
  /** Purely classify current pending attention from the artifact and durable ledgers. */
17
19
  export async function classifyDeliveryHealth(squarePath, opts) {
18
20
  const now = opts.now ?? Date.now();
@@ -49,21 +51,32 @@ export async function classifyDeliveryHealth(squarePath, opts) {
49
51
  }
50
52
  return items;
51
53
  }
54
+ function displayField(value) {
55
+ const truncated = truncateChars(value, DISPLAY_FIELD_LIMIT - 1);
56
+ return truncated.remaining === 0 ? truncated.text : `${truncated.text}…`;
57
+ }
52
58
  function formatItem(item) {
53
- const evidence = item.attempt?.signature === undefined ? '' : ` · ${item.attempt.signature}`;
54
- return ` · ${formatActivityId(item.actIndex)} → ${participantIdentity(item.recipient)} from ${participantIdentity(item.actor)} · ${formatDuration(item.ageMs)}${evidence}`;
59
+ const evidence = item.attempt?.signature === undefined ? '' : ` · ${displayField(item.attempt.signature)}`;
60
+ return ` · ${formatActivityId(item.actIndex)} → ${displayField(participantIdentity(item.recipient))} from ${displayField(participantIdentity(item.actor))} · ${formatDuration(item.ageMs)}${evidence}`;
55
61
  }
56
- export async function doctorDeliveryHealth(squarePath, graceMs, now = Date.now(), env = process.env) {
57
- const items = await classifyDeliveryHealth(squarePath, { graceMs, now, env });
62
+ export function renderDeliveryHealth(items) {
58
63
  if (items.length === 0)
59
64
  return ['✓ no pending delivery attention'];
60
65
  const out = [`· delivery attention · ${items.length} pending`];
66
+ let displayed = 0;
61
67
  for (const kind of DISPLAY_ORDER) {
62
68
  const group = items.filter((item) => item.kind === kind);
63
69
  if (group.length === 0)
64
70
  continue;
65
71
  out.push(`${ACTIONABLE.has(kind) ? '✕' : '○'} ${kind}: ${group.length}`);
66
- out.push(...group.map(formatItem));
72
+ const details = group.slice(0, DETAIL_LIMIT - displayed);
73
+ out.push(...details.map(formatItem));
74
+ displayed += details.length;
67
75
  }
76
+ if (items.length > DETAIL_LIMIT)
77
+ out.push(`${DETAIL_LIMIT} of ${items.length} pending details shown`);
68
78
  return out;
69
79
  }
80
+ export async function doctorDeliveryHealth(squarePath, graceMs, now = Date.now(), env = process.env) {
81
+ return renderDeliveryHealth(await classifyDeliveryHealth(squarePath, { graceMs, now, env }));
82
+ }
@@ -46,12 +46,14 @@ export interface WakeAdapter {
46
46
  export interface RoutedNotification {
47
47
  actor: string;
48
48
  body: string;
49
+ mentions?: readonly string[];
49
50
  route: DirectedNotificationRoute;
50
51
  recipient?: string;
51
52
  }
52
53
  export interface CatchFilterShape {
53
54
  actor: string;
54
55
  body: string;
56
+ mentions?: readonly string[];
55
57
  reach?: Reach;
56
58
  recipients?: readonly string[];
57
59
  }
package/dist/delivery.js CHANGED
@@ -125,6 +125,7 @@ export function leaseOwnsNotification(lease, notification) {
125
125
  return matchesCatchFilter({
126
126
  actor: notification.actor,
127
127
  body: notification.body,
128
+ ...(notification.mentions === undefined ? {} : { mentions: notification.mentions }),
128
129
  ...(notification.recipient === undefined ? {} : { recipients: [notification.recipient] }),
129
130
  ...(notification.route === 'bell' ? { reach: 'bell' } : {}),
130
131
  }, lease.filter ?? {});
@@ -4,6 +4,7 @@ import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import crossSpawn from 'cross-spawn';
6
6
  import { SQUARE_IDENTITY } from './identity.js';
7
+ import { truncateExternalDiagnostic } from './presentation.js';
7
8
  function packageRoot() {
8
9
  // Emitted modules live in dist; package assets are one level above them.
9
10
  return fileURLToPath(new URL('../', import.meta.url));
@@ -75,7 +76,16 @@ export function doctorHarnessLinks(links) {
75
76
  : `○ Square ${link.kind ?? 'link'} missing ${link.target}`);
76
77
  }
77
78
  function runOpenCode(homeDir, args) {
78
- const result = crossSpawn.sync(process.env.SQUARE_OPENCODE_BIN || 'opencode', args, {
79
+ const prefix = (() => {
80
+ try {
81
+ const parsed = JSON.parse(process.env.SQUARE_OPENCODE_BIN_ARGS ?? '[]');
82
+ return Array.isArray(parsed) && parsed.every((item) => typeof item === 'string') ? parsed : [];
83
+ }
84
+ catch {
85
+ return [];
86
+ }
87
+ })();
88
+ const result = crossSpawn.sync(process.env.SQUARE_OPENCODE_BIN || 'opencode', [...prefix, ...args], {
79
89
  encoding: 'utf8',
80
90
  env: { ...process.env, HOME: homeDir, XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME ?? path.join(homeDir, '.config') },
81
91
  timeout: 30_000,
@@ -125,7 +135,8 @@ export function verifyOpenCodeRuntime(homeDir, run = runOpenCode) {
125
135
  try {
126
136
  const result = run(homeDir, ['debug', 'config']);
127
137
  if (result.status !== 0) {
128
- return `✕ OpenCode debug config failed: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.status}`}`;
138
+ const diagnostic = result.stderr.trim() || result.stdout.trim() || `exit ${result.status}`;
139
+ return `✕ OpenCode debug config failed: ${truncateExternalDiagnostic(diagnostic)}`;
129
140
  }
130
141
  let config;
131
142
  try {
@@ -141,7 +152,8 @@ export function verifyOpenCodeRuntime(homeDir, run = runOpenCode) {
141
152
  return `○ OpenCode npm plugin not loaded: ${expected}`;
142
153
  }
143
154
  catch (error) {
144
- return `○ OpenCode runtime unavailable (${error instanceof Error ? error.message : String(error)})`;
155
+ const diagnostic = error instanceof Error ? error.message : String(error);
156
+ return `○ OpenCode runtime unavailable (${truncateExternalDiagnostic(diagnostic)})`;
145
157
  }
146
158
  }
147
159
  export function skillLinks(homeDir = os.homedir(), parents = ['.claude', '.agents']) {
package/dist/harness.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import { doctorDeliveryHealth } from './delivery-health.js';
3
3
  import { wakeGraceMs } from './notifications.js';
4
+ import { truncateExternalDiagnostic } from './presentation.js';
4
5
  import { doctorClaudePlugin, installClaudePlugin, uninstallClaudePlugin, } from './harness-claude.js';
5
6
  import { doctorCodexPlugin, installCodexPlugin, uninstallCodexPlugin, } from './harness-codex.js';
6
7
  import { doctorPiPackage, installPiPackage, uninstallPiPackage, } from './harness-pi.js';
@@ -13,7 +14,8 @@ async function doctorHost(label, inspect) {
13
14
  return result(await inspect());
14
15
  }
15
16
  catch (error) {
16
- return result([`○ ${label} doctor unavailable (${error instanceof Error ? error.message : String(error)})`]);
17
+ const diagnostic = error instanceof Error ? error.message : String(error);
18
+ return result([`○ ${label} doctor unavailable (${truncateExternalDiagnostic(diagnostic)})`]);
17
19
  }
18
20
  }
19
21
  function openCodeLinks(homeDir) {
package/dist/help.js CHANGED
@@ -7,46 +7,50 @@ const COMMANDS = [
7
7
  details: ['Options:', ' --cap <N|unlimited> Set a per-participant activity cap (default unlimited).', ' --template <name> Append a packaged activity guide.', ' --throttle <N> Allow at most N public activities per minute.', ' -f, --force Replace an existing artifact.'],
8
8
  },
9
9
  {
10
- names: ['list', 'ls'], usage: '{command} [--depth N]', summary: 'List nearby squares below the current directory.', group: 'host',
11
- details: ['Options:', ' --depth <N> Descend through at most N directory levels (default 4; 0 scans only the current directory).'],
10
+ names: ['list', 'ls'], usage: '{command} [--depth N] [--limit N] [--after path]', summary: 'List nearby squares below the current directory.', group: 'host',
11
+ details: ['Options:', ' --depth <N> Descend through at most N directory levels (default 4; maximum 16; 0 scans only the current directory).', ' --limit <N> Show one page (default 20, maximum 100).', ' --after <path> Resume after a relative path from a previous page.'],
12
12
  },
13
13
  {
14
- names: ['join'], usage: '--as <name> join [--last N | --all] [--kick]', usesSquare: true, group: 'participant',
14
+ names: ['join'], usage: '--as <name> join [--last N] [--kick]', usesSquare: true, group: 'participant',
15
15
  summary: 'Step into the square and read its current context.',
16
- details: ['Options:', ' --last <N> Show the last N public activities (default 10).', ' --all Show the complete history.'],
16
+ details: ['Options:', ' --last <N> Show the last N public activities (default 10, maximum 100).', ' --kick End the standing participant and reclaim the name for this session.'],
17
17
  },
18
18
  {
19
- names: ['express'], usage: '--as <name> express [-f|--force] [--no-wait] [--bell] [--reply <activity-id>] <activity | ->', usesSquare: true, group: 'participant',
19
+ names: ['express'], usage: '--as <name> express [-f|--force] [--no-wait] (--mention <name>... | --no-mention | --bell) [--reply <activity-id>] <activity | ->', usesSquare: true, group: 'participant',
20
20
  summary: 'Speak, gesture, or do both.',
21
- details: ['Reach:', ' @name Address someone in the square. They hear the body even without listen; everyone else sees you walk over.', ' bare Lands in history even with no listener; listen opts someone into future bare delivery.', " --bell Call every participant's attention to this activity without a mention.", '', 'Options:', ' -f, --force Express without first catching unread activity or attention etiquette.', ' --no-wait If held or throttled, save a draft and return.', ' --reply <activity-id> Mark this activity as a reply to an earlier activity (for example act/12).'],
21
+ details: ['Reach:', ' --mention <name> Address one participant; repeat for multiple participants. The body stays unchanged.', ' --no-mention Land a bare activity explicitly; listen opts someone into future bare delivery.', " --bell Call every participant's attention to this activity.", '', 'Options:', ' -f, --force Express without first catching unread activity or attention etiquette.', ' --no-wait If held or throttled, save a draft and return.', ' --reply <activity-id> Mark this activity as a reply to an earlier activity (for example act/12).'],
22
22
  },
23
23
  { names: ['listen'], usage: '--as <name> listen <participant>', usesSquare: true, group: 'participant', summary: 'Turn an ear toward one participant\'s future bare says.' },
24
24
  { names: ['ignore'], usage: '--as <name> ignore <participant>', usesSquare: true, group: 'participant', summary: 'Ignore one participant\'s future mentions and bare says.' },
25
25
  { names: ['listening'], usage: '--as <name> listening', usesSquare: true, group: 'participant', summary: 'Show who you are turned toward.', details: ['Listening is future-only: the edge is fixed when a say lands; it never rewrites history.'] },
26
26
  {
27
- names: ['catch'], usage: '--as <name> catch (--now | --idle <duration>) [--from <names>] [--mention [name]] [--replace]', usesSquare: true, group: 'participant',
27
+ names: ['catch'], usage: '--as <name> catch (--now | --idle <duration>) [--from <names>] [--mention] [--limit <count>] [--replace]', usesSquare: true, group: 'participant',
28
28
  summary: 'Catch directed conversation since you last looked.',
29
- details: ['Modes:', ' --now Catch up immediately.', ' --idle <duration> Wait for something relevant, or for quiet to last this long.', '', 'Attention:', ' Mentions arrive when you are addressed; bells arrive for everyone; bare says require listen.', ' listen and ignore are future-only and fixed when each say lands.', '', 'Filters:', ' --from <names> Match only comma-separated participants.', ' --mention [name] Match direct attention for a name, or your own name when omitted.', '', 'Recovery:', ' --replace Replace another active catch for this participant.'],
29
+ details: ['Modes:', ' --now Catch up immediately.', ' --idle <duration> Wait for something relevant, or for quiet to last this long.', '', 'Attention:', ' Mentions arrive when you are addressed; bells arrive for everyone; bare says require listen.', ' listen and ignore are future-only and fixed when each say lands.', '', 'Filters:', ' --from <names> Match only comma-separated participants.', ' --mention Match direct attention for your current participant (and bells).', ' --limit <count> Return one page (default 10, maximum 100).', '', 'Recovery:', ' --replace Replace another active catch for this participant.'],
30
30
  },
31
31
  { names: ['done'], usage: '--as <name> done [final | -]', usesSquare: true, group: 'participant', summary: 'Step out, optionally leaving a final note.' },
32
32
  {
33
- names: ['stream'], usage: 'stream [--ndjson [--for <name>]]', usesSquare: true, hiddenFromIndex: true,
33
+ names: ['stream'], usage: 'stream [--ndjson [--for <name>] [--last <N> | --after <id>]]', usesSquare: true, hiddenFromIndex: true,
34
34
  summary: 'Follow activity without consuming participant presence.',
35
- details: ['Options:', ' --ndjson Emit one JSON event per line.', ' --for <name> With --ndjson, emit notifications for one participant.'],
35
+ details: ['Options:', ' --ndjson Emit one JSON event per line.', ' --for <name> With --ndjson, emit notifications for one participant.', ' --last <N> Emit the last N eligible activities first (default 10, maximum 100).', ' --after <id> Resume after a canonical activity id (act/12).'],
36
36
  },
37
37
  {
38
- names: ['inbox'], usage: 'inbox --for-session <session-id> [--json]', hiddenFromIndex: true,
39
- summary: 'Inspect bounded machine-local notifications for a native session.',
40
- details: ['Options:', ' --for-session <id> Required harness session id.', ' --json Emit structured JSON.'],
38
+ names: ['inbox'], usage: 'inbox --for-session <session-id> [--limit <N>] [--json]', hiddenFromIndex: true,
39
+ summary: 'Inspect a bounded machine-local notification snapshot for a native session.',
40
+ details: ['Options:', ' --for-session <id> Required harness session id.', ' --limit <N> Snapshot memberships (default 20, maximum 100).', ' --json Emit the bounded snapshot as structured JSON.'],
41
41
  },
42
42
  { names: ['claude-hook', 'codex-hook'], usage: '{command}', summary: 'Present pending attention at one native agent boundary.', hiddenFromIndex: true },
43
43
  {
44
- names: ['history'], usage: '[--as <name>] history [filters] [output]', usesSquare: true, group: 'participant',
44
+ names: ['history'], usage: 'history [filters] [output]', usesSquare: true, group: 'participant',
45
45
  summary: 'Read or search the archive without changing what you have caught.',
46
- details: ['Filters:', ' --from <names> Match activities from participants.', ' --since <time> Match activities after a time.', ' --grep <regex> | --fixed <s> Search activity ids, participants, and original bodies.', ' --mention <name> Match direct attention for a participant.', ' --pending Match attention waiting for --as <name>.', ' --at <ids> Center on comma-separated activity ids; may repeat.', ' -B, -A, -C <N> Set non-negative context around every --at coordinate.', ' --before <id> Read the page immediately before an activity.', ' --after <id> Read the page immediately after an activity.', '', 'Results:', ' --limit <N> Page size (default 10).', ' --order <asc|desc> Set display order (default oldest first).', '', 'Output:', ' --no-truncate --json --format <fields>', ' Bodies are previews by default; --no-truncate expands them. --as keeps participant perception.'],
46
+ details: ['Filters:', ' --from <names> Match activities from participants.', ' --since <time> Match activities after a time.', ' --grep <regex> | --fixed <s> Search activity ids, participants, and original bodies.', ' --mention <name> Match direct attention for a participant.', ' --at <ids> Center on comma-separated activity ids; may repeat.', ' -B, -A, -C <N> Set non-negative context around every --at coordinate.', ' --before <id> Read the page immediately before an activity.', ' --after <id> Read the page immediately after an activity.', '', 'Results:', ' --limit <N> Page size (default 10, maximum 100).', ' --order <asc|desc> Set display order (default oldest first).', '', 'Output:', ' --no-truncate --json --format <fields>', ' One result shows its full body; multiple results use previews.', ' --no-truncate shows every original body.'],
47
47
  },
48
48
  { names: ['status'], usage: '[--as <name>] status', usesSquare: true, group: 'participant', summary: 'Show who is present and what happened most recently.' },
49
- { names: ['participants'], usage: 'participants', usesSquare: true, group: 'host', summary: 'Show the full participant roster and current states.' },
49
+ {
50
+ names: ['participants'], usage: 'participants [--limit <count>]', usesSquare: true, group: 'host',
51
+ summary: 'Show the participant roster and current states.',
52
+ details: ['Options:', ' --limit <count> Show the first count names in roster order (default 20, maximum 100).'],
53
+ },
50
54
  { names: ['hold'], usage: '--as <name> hold [reason | -]', usesSquare: true, group: 'participant', summary: 'Raise a hand and pause participant activity.' },
51
55
  { names: ['resume'], usage: '--as <name> resume', usesSquare: true, group: 'participant', summary: 'Lower the raised hand and resume activity.' },
52
56
  {
@@ -121,17 +125,22 @@ export function renderSubcommandHelp(command) {
121
125
  }
122
126
  export function helpRequest(rawArgs) {
123
127
  const args = [];
128
+ let hasExplicitName = false;
124
129
  for (let index = 0; index < rawArgs.length; index++) {
125
130
  const arg = rawArgs[index];
126
131
  if (arg === '--location' || arg === '--as') {
127
132
  const value = rawArgs[index + 1];
128
133
  if (value === undefined || value.startsWith('--'))
129
134
  return undefined;
135
+ if (arg === '--as')
136
+ hasExplicitName = true;
130
137
  index++;
131
138
  continue;
132
139
  }
133
140
  args.push(arg);
134
141
  }
142
+ if (hasExplicitName && args[0] === 'history')
143
+ return undefined;
135
144
  if (isHelpFlag(args[0]))
136
145
  return {};
137
146
  if (args[0] === 'help') {
package/dist/inbox.d.ts CHANGED
@@ -5,6 +5,8 @@ export interface PendingWaitOptions {
5
5
  excludeKeys?: ReadonlySet<string>;
6
6
  /** After a delivery failure, wait for a new state edge before retrying the same pending work. */
7
7
  skipImmediate?: boolean;
8
+ /** Reports whether a change wait has been established for a deferred retry. */
9
+ onChangeArmed?: (armed: boolean) => void;
8
10
  }
9
11
  export declare function sessionInbox(sessionId: string, env?: NodeJS.ProcessEnv): Promise<InboxMembership[]>;
10
12
  /** Wait for a bound square to produce a new pending notification without consuming it. */
package/dist/inbox.js CHANGED
@@ -59,8 +59,10 @@ export async function waitForSessionPending(sessionId, timeoutMs, options = {},
59
59
  if (immediate.some((membership) => membership.notifications.length > 0))
60
60
  return immediate;
61
61
  }
62
- if (timeoutMs <= 0 || options.signal?.aborted)
62
+ if (timeoutMs <= 0 || options.signal?.aborted) {
63
+ options.onChangeArmed?.(false);
63
64
  return [];
65
+ }
64
66
  const bindings = await projectSessionBindings({ hostLedger: hostLedgerForEnv(env), sessionId });
65
67
  const paths = [...new Set(bindings.map((binding) => binding.location))];
66
68
  let aborted = false;
@@ -77,7 +79,7 @@ export async function waitForSessionPending(sessionId, timeoutMs, options = {},
77
79
  return undefined;
78
80
  const current = withoutExcluded(await sessionInbox(sessionId, env), options.excludeKeys);
79
81
  return current.some((membership) => membership.notifications.length > 0) ? current : undefined;
80
- });
82
+ }, options.onChangeArmed);
81
83
  if (aborted || change.status === 'expired')
82
84
  return [];
83
85
  if (change.status === 'ready')
package/dist/list.js CHANGED
@@ -4,10 +4,15 @@ import { probeSquare } from './square-file-adapter.js';
4
4
  import { closeOpenSquare } from './open-square.js';
5
5
  import { listPresentation } from './views.js';
6
6
  import { formatRelativeTime } from './time.js';
7
- import { participantIdentity } from './presentation.js';
7
+ import { participantIdentity, quoteShell } from './presentation.js';
8
8
  const DEFAULT_LIST_DEPTH = 4;
9
+ const MAX_LIST_DEPTH = 16;
10
+ const DEFAULT_LIST_LIMIT = 20;
11
+ const MAX_LIST_LIMIT = 100;
12
+ const LIST_DISCOVERY_BUDGET = 10_000;
9
13
  const CONTEXT_PREVIEW_LINES = 2;
10
14
  const PARTICIPANT_PREVIEW_COUNT = 3;
15
+ const DISPLAY_CHARACTER_LIMIT = 160;
11
16
  const LIST_SKIP_DIRS = new Set(['.git', 'node_modules', 'dist']);
12
17
  function contextLines(lines) {
13
18
  return lines.map((line) => line.trim()).filter(Boolean);
@@ -35,7 +40,11 @@ async function readSquareListItem(filePath, root) {
35
40
  }
36
41
  async function collectSquareList(root, maxDepth) {
37
42
  const items = [];
43
+ let examined = 0;
44
+ let stopped = false;
38
45
  async function walk(dir, depth) {
46
+ if (stopped)
47
+ return;
39
48
  let entries;
40
49
  try {
41
50
  entries = await fs.promises.readdir(dir, { withFileTypes: true });
@@ -44,10 +53,16 @@ async function collectSquareList(root, maxDepth) {
44
53
  // Directory vanished or became unreadable mid-walk — skip it, don't abort the scan.
45
54
  return;
46
55
  }
56
+ entries.sort((left, right) => left.name.localeCompare(right.name));
47
57
  for (const entry of entries) {
58
+ if (stopped)
59
+ return;
60
+ examined += 1;
61
+ if (examined === LIST_DISCOVERY_BUDGET)
62
+ stopped = true;
48
63
  const fullPath = path.join(dir, entry.name);
49
64
  if (entry.isDirectory()) {
50
- if (depth < maxDepth && !LIST_SKIP_DIRS.has(entry.name))
65
+ if (!stopped && depth < maxDepth && !LIST_SKIP_DIRS.has(entry.name))
51
66
  await walk(fullPath, depth + 1);
52
67
  continue;
53
68
  }
@@ -59,46 +74,84 @@ async function collectSquareList(root, maxDepth) {
59
74
  }
60
75
  }
61
76
  await walk(root, 0);
62
- return items.sort((a, b) => a.path.localeCompare(b.path));
77
+ return { items: items.sort((a, b) => a.path.localeCompare(b.path)), stopped };
78
+ }
79
+ function characterPreview(value) {
80
+ const characters = Array.from(value);
81
+ return characters.length <= DISPLAY_CHARACTER_LIMIT ? value : `${characters.slice(0, DISPLAY_CHARACTER_LIMIT - 1).join('')}…`;
63
82
  }
64
- function renderSquareList(items) {
65
- if (items.length === 0)
66
- return '(no squares found)\n';
83
+ function renderSquareList(items, stopped, depth, limit, after) {
84
+ const page = after === undefined ? items : items.filter((item) => item.path.localeCompare(after) > 0);
85
+ const shownItems = page.slice(0, limit);
86
+ if (items.length === 0) {
87
+ return [`(no squares found)`, ...(stopped ? [`○ discovery stopped after examining ${LIST_DISCOVERY_BUDGET} filesystem entries; results may be incomplete`] : [])].join('\n') + '\n';
88
+ }
67
89
  const now = Date.now();
68
90
  const lines = ['squares'];
69
- for (const item of items) {
70
- lines.push(`${item.activities > 0 ? '●' : '○'} ${item.path} · ${formatRelativeTime(item.lastActiveAt, now)} · ${item.participants.length} in square · ${item.activities} activities`);
91
+ for (const item of shownItems) {
92
+ lines.push(`${item.activities > 0 ? '●' : '○'} ${characterPreview(item.path)} · ${formatRelativeTime(item.lastActiveAt, now)} · ${item.participants.length} in square · ${item.activities} activities`);
71
93
  const shownContext = item.context.slice(0, CONTEXT_PREVIEW_LINES);
72
94
  if (shownContext.length === 0) {
73
95
  lines.push(' context · (none)');
74
96
  }
75
97
  else {
76
- shownContext.forEach((line, index) => lines.push(` ${index === 0 ? 'context' : ' '} · ${line}`));
98
+ shownContext.forEach((line, index) => lines.push(` ${index === 0 ? 'context' : ' '} · ${characterPreview(line)}`));
77
99
  const hiddenContext = item.context.length - shownContext.length;
78
100
  if (hiddenContext > 0)
79
101
  lines.push(` · … ${hiddenContext} more ${hiddenContext === 1 ? 'line' : 'lines'}`);
80
102
  }
81
103
  const shownParticipants = item.participants.slice(0, PARTICIPANT_PREVIEW_COUNT);
82
104
  const hiddenParticipants = item.participants.length - shownParticipants.length;
83
- lines.push(` participants · ${shownParticipants.length === 0 ? 'nobody' : shownParticipants.map(participantIdentity).join(' · ')}${hiddenParticipants > 0 ? ` · … ${hiddenParticipants} more` : ''}`);
105
+ lines.push(` participants · ${shownParticipants.length === 0 ? 'nobody' : shownParticipants.map((participant) => participantIdentity(characterPreview(participant))).join(' · ')}${hiddenParticipants > 0 ? ` · … ${hiddenParticipants} more` : ''}`);
84
106
  }
107
+ const hiddenItems = page.length - shownItems.length;
108
+ if (hiddenItems > 0) {
109
+ const cursor = shownItems.at(-1)?.path;
110
+ lines.push(stopped ? ' … more squares' : ` … ${hiddenItems} more ${hiddenItems === 1 ? 'square' : 'squares'}`);
111
+ if (cursor !== undefined)
112
+ lines.push(`» square list --depth ${depth} --limit ${limit} --after ${quoteShell(cursor)}`);
113
+ }
114
+ if (stopped)
115
+ lines.push(`○ discovery stopped after examining ${LIST_DISCOVERY_BUDGET} filesystem entries; results may be incomplete`);
85
116
  return lines.join('\n') + '\n';
86
117
  }
87
- function parseMaxDepth(args, usage) {
88
- if (args.length === 0)
89
- return DEFAULT_LIST_DEPTH;
90
- if (args.length !== 2 || args[0] !== '--depth' || !/^\d+$/.test(args[1])) {
91
- usage();
92
- return DEFAULT_LIST_DEPTH;
93
- }
94
- const depth = Number(args[1]);
95
- if (!Number.isSafeInteger(depth)) {
96
- usage();
97
- return DEFAULT_LIST_DEPTH;
118
+ function parseListOptions(args, usage) {
119
+ let depth = DEFAULT_LIST_DEPTH;
120
+ let limit = DEFAULT_LIST_LIMIT;
121
+ let after;
122
+ for (let index = 0; index < args.length; index += 2) {
123
+ const option = args[index];
124
+ const value = args[index + 1];
125
+ if (value === undefined || (option !== '--depth' && option !== '--limit' && option !== '--after')) {
126
+ usage();
127
+ return { depth, limit };
128
+ }
129
+ if (option === '--after') {
130
+ if (after !== undefined || value.length === 0 || path.isAbsolute(value)) {
131
+ usage();
132
+ return { depth, limit };
133
+ }
134
+ after = value;
135
+ continue;
136
+ }
137
+ if (!/^\d+$/.test(value)) {
138
+ usage();
139
+ return { depth, limit };
140
+ }
141
+ const parsed = Number(value);
142
+ if (!Number.isSafeInteger(parsed) || (option === '--depth' ? parsed > MAX_LIST_DEPTH : parsed === 0 || parsed > MAX_LIST_LIMIT)) {
143
+ usage();
144
+ return { depth, limit };
145
+ }
146
+ if (option === '--depth')
147
+ depth = parsed;
148
+ else
149
+ limit = parsed;
98
150
  }
99
- return depth;
151
+ return { depth, limit, after };
100
152
  }
101
153
  export async function cmdListSquares(args, usage) {
102
- const maxDepth = parseMaxDepth(args, usage);
103
- process.stdout.write(renderSquareList(await collectSquareList(process.cwd(), maxDepth)));
154
+ const options = parseListOptions(args, usage);
155
+ const discovery = await collectSquareList(process.cwd(), options.depth);
156
+ process.stdout.write(renderSquareList(discovery.items, discovery.stopped, options.depth, options.limit, options.after));
104
157
  }
package/dist/model.d.ts CHANGED
@@ -101,9 +101,6 @@ export interface ActivitiesOptions {
101
101
  beforeContext?: number;
102
102
  afterContext?: number;
103
103
  mention?: string;
104
- /** Undelivered mention/bell items for viewer only (read-only; requires viewer). */
105
- pending?: boolean;
106
- viewer?: string;
107
104
  grep?: string;
108
105
  fixed?: string;
109
106
  order?: 'asc' | 'desc';
@@ -113,6 +110,7 @@ export interface ActivitiesOptions {
113
110
  export interface WatchOptions {
114
111
  participants?: string[];
115
112
  mention?: string;
113
+ limit?: number;
116
114
  idleMs?: number;
117
115
  replace?: boolean;
118
116
  now?: boolean;
@@ -46,6 +46,9 @@ function pidHost(env) {
46
46
  function isIpc(host) {
47
47
  return host !== undefined && (host.startsWith('unix://') || host.startsWith('pipe://'));
48
48
  }
49
+ function supportsIpcHost(host) {
50
+ return process.platform !== 'win32' || !host.startsWith('unix://');
51
+ }
49
52
  /** Match the host precedence used by the Paseo CLI for local daemon connections. */
50
53
  export function paseoDaemonHosts(env = process.env) {
51
54
  const explicit = normalizeHost(env.PASEO_HOST);
@@ -55,11 +58,11 @@ export function paseoDaemonHosts(env = process.env) {
55
58
  const listen = normalizeHost(env.PASEO_LISTEN);
56
59
  const pid = pidHost(env);
57
60
  const configured = configuredHost(env);
58
- if (isIpc(listen))
61
+ if (isIpc(listen) && supportsIpcHost(listen))
59
62
  candidates.push(listen);
60
- if (isIpc(pid))
63
+ if (isIpc(pid) && supportsIpcHost(pid))
61
64
  candidates.push(pid);
62
- if (isIpc(configured))
65
+ if (isIpc(configured) && supportsIpcHost(configured))
63
66
  candidates.push(configured);
64
67
  if (configured !== undefined && !isIpc(configured) && configured !== '127.0.0.1:6767')
65
68
  candidates.push(configured);
@@ -73,6 +76,9 @@ function uriPassword(uri) {
73
76
  export function resolvePaseoDaemonTarget(host, env = process.env) {
74
77
  const passwordFromEnv = env.PASEO_PASSWORD?.trim() || undefined;
75
78
  if (host.startsWith('unix://') || host.startsWith('pipe://')) {
79
+ if (process.platform === 'win32' && host.startsWith('unix://')) {
80
+ throw new Error('Paseo Unix socket targets are unsupported on Windows; use pipe:// or tcp://.');
81
+ }
76
82
  const prefix = host.startsWith('unix://') ? 'unix://' : 'pipe://';
77
83
  const socketPath = expandHome(host.slice(prefix.length).trim());
78
84
  if (socketPath === '')
@@ -4,7 +4,10 @@ export interface PaseoAgent {
4
4
  status: string;
5
5
  cwd?: string;
6
6
  }
7
- export declare function discoverPaseoAgents(timeoutMs?: number): {
7
+ export declare function discoverPaseoAgents(timeoutMs?: number, opts?: {
8
+ args?: string[];
9
+ bin?: string;
10
+ }): {
8
11
  agents: PaseoAgent[];
9
12
  error?: string;
10
13
  };
@@ -1,8 +1,8 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
  import { waitForPaseoToolBoundary } from './paseo-timeline.js';
3
- export function discoverPaseoAgents(timeoutMs = 5000) {
3
+ export function discoverPaseoAgents(timeoutMs = 5000, opts = {}) {
4
4
  try {
5
- const raw = execFileSync(process.env.SQUARE_PASEO_BIN || 'paseo', ['ls', '--global', '--json'], {
5
+ const raw = execFileSync(opts.bin ?? process.env.SQUARE_PASEO_BIN ?? 'paseo', [...(opts.args ?? []), 'ls', '--global', '--json'], {
6
6
  encoding: 'utf8',
7
7
  timeout: timeoutMs,
8
8
  stdio: ['ignore', 'pipe', 'pipe'],