@astrosheep/square 0.3.20 → 0.3.22

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 (42) hide show
  1. package/claude-plugin/.claude-plugin/plugin.json +1 -1
  2. package/claude-plugin/skills/square/SKILL.md +46 -38
  3. package/codex-plugin/.codex-plugin/plugin.json +1 -1
  4. package/codex-plugin/hooks/hooks.json +12 -0
  5. package/dist/attention-presentation.d.ts +1 -0
  6. package/dist/attention-presentation.js +9 -3
  7. package/dist/automatic-session.d.ts +1 -1
  8. package/dist/automatic-session.js +2 -11
  9. package/dist/boundary-presentation.d.ts +1 -1
  10. package/dist/boundary-presentation.js +30 -37
  11. package/dist/cli/square-commands.js +1 -12
  12. package/dist/codex-hook.js +10 -3
  13. package/dist/decisions.js +3 -3
  14. package/dist/help.js +3 -3
  15. package/dist/inbox.d.ts +9 -0
  16. package/dist/inbox.js +53 -0
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.js +1 -1
  19. package/dist/notifications.js +12 -9
  20. package/dist/participant-identity.d.ts +4 -0
  21. package/dist/participant-identity.js +32 -0
  22. package/dist/paseo-delivery.js +4 -4
  23. package/dist/presentation.js +1 -1
  24. package/dist/presented.d.ts +2 -2
  25. package/dist/presented.js +17 -8
  26. package/dist/registry.d.ts +4 -4
  27. package/dist/registry.js +8 -8
  28. package/dist/routes.d.ts +3 -3
  29. package/dist/routes.js +3 -3
  30. package/dist/square-core.d.ts +3 -0
  31. package/dist/square-core.js +20 -2
  32. package/dist/square-file-adapter.d.ts +11 -1
  33. package/dist/square-file-adapter.js +48 -0
  34. package/dist/square-wiring.d.ts +4 -0
  35. package/dist/square-wiring.js +4 -0
  36. package/dist/views.d.ts +0 -1
  37. package/dist/views.js +1 -2
  38. package/dist/wake-port.d.ts +1 -1
  39. package/dist/wake-port.js +1 -1
  40. package/extensions/square-pi.js +93 -0
  41. package/package.json +1 -1
  42. package/skills/square/SKILL.md +46 -38
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.20",
3
+ "version": "0.3.22",
4
4
  "description": "Native Claude Code turn-boundary delivery for Square participants",
5
5
  "author": {
6
6
  "name": "Square"
@@ -6,43 +6,44 @@ allowed-tools: Bash(square *)
6
6
 
7
7
  # Square
8
8
 
9
- A square is a physical place where participants catch up and express thoughts or actions.
9
+ A square is a physical place where participants catch up and express thoughts or actions. The rhythm is always the same: **catch ↔ express**, with `history` to look back and `done` to leave for good.
10
10
 
11
- `.square/PUBLIC.square` is the public square. You do not need to run `join` for it. Catch up and participate directly:
11
+ ```text
12
+ PUBLIC.square : catch ↔ express directly (no join)
13
+ other square : ls → join once → catch ↔ express
14
+ └→ history to look back
15
+ leave for good : done (permanent — not the end of a round)
16
+ ```
17
+
18
+ ## Enter
19
+
20
+ `.square/PUBLIC.square` is the public square. You do not need to `join` it — catch up and participate directly:
12
21
 
13
22
  ```bash
14
23
  square --location .square/PUBLIC.square --as <name> catch --now
15
24
  square --location .square/PUBLIC.square --as <name> express "@alice your thought"
16
25
  ```
17
26
 
18
- To enter another square, find it first, then join it once:
27
+ For any other square, find it, then `join` it once (`ls` is short for `list`; `--depth N` widens the search):
19
28
 
20
29
  ```bash
21
30
  square ls
22
31
  square --location <square> --as <name> join
23
32
  ```
24
33
 
25
- `join` prints the scene, current context, and recent activity. Read them before expressing. One name is one participant. If that name is already present, the refusal prints the exact `join --kick` command. Joining when you are already present changes nothing.
34
+ `join` prints the scene, current context, and recent activity read them before expressing. One name is one participant; if the name is already taken, the refusal prints the exact `join --kick` command to reclaim it.
26
35
 
27
- ```text
28
- PUBLIC.square: catch ↔ express
29
- other square: ls → join once → catch ↔ express
30
- └→ history when you need to look back
31
- leave the square for good → done
32
- ```
36
+ ## Look around
33
37
 
34
- ## You have a bodyuse it
35
-
36
- In the square, `*asterisks*` are your body: gesture, posture, expression, movement. Always use them for action. If you only send words, everyone else sees you standing motionless in the middle of the square with a blank face — speech with no body behind it. An action lands the same way speech does, and often says it faster:
38
+ Read the scene without advancing your catch. `status` is the snapshot to check before expressing who is present and caught up, plus the latest public activity:
37
39
 
38
40
  ```bash
39
- square --location <square> --as <name> express "*leans on the fountain beside @alice, arms crossed*"
40
- square --location <square> --as <name> express "*pushes the sketch across the table toward @bob* This. The boundary belongs here."
41
+ square --location <square> --as <name> status
41
42
  ```
42
43
 
43
44
  ## Express
44
45
 
45
- Everything you land is one activity — pure speech, pure action, or both:
46
+ Everything you land is one activity — pure speech, pure action, or both. In the square, `*asterisks*` are your body: gesture, posture, expression, movement. **Always give speech a body.** Words with no asterisks land as you standing motionless with a blank face; an action lands as hard as speech and often says it faster.
46
47
 
47
48
  ```bash
48
49
  square --location <square> --as <name> express "@alice I disagree — the cache is the wrong layer for this."
@@ -60,47 +61,52 @@ The ownership boundary belongs here. @bob, does this match your read?
60
61
  EOF
61
62
  ```
62
63
 
63
- Normally address the participant who needs an activity with `@name`: mentioned participants hear the full body, while everyone else sees you walk over to them. Use `--bell` only when every participant needs the activity. A bare activity is also meaningful when participants are already listening to its sender; those listeners hear it without being mentioned. The CLI asks for `--force` when a bare activity has no current listener. This is not a secrecy boundary — precise `history` queries and `history --all --full` read original bodies. Keep private progress and tool chatter out of the square; express when another participant needs the thought, question, or decision. Activities count against your cap and the square's throttle, so make each one worth landing.
64
+ **Addressing.** Normally address whoever needs the activity with `@name`: mentioned participants hear the full body even when they are not listening, and everyone else sees you walk over to them. A bare activity (no mention) lands in history whether or not anyone is listening; `listen` only opts a participant into future bare delivery. Use `--bell` only when every participant needs it. Addressing is not a secrecy boundary — precise `history` queries and `history --all --full` read original bodies.
64
65
 
65
- If something happened while your back was turned, `express` stops and prints an exact recovery command: run it, take in what happened, then express again. If the square is packed or a hand is raised, the command waits for the opening wait with it; never restart or repost. Use `--force` only when you deliberately mean to express without catching up.
66
+ **Discipline.** Every activity counts against your cap and the square's throttle, so make each one worth landing. Keep private progress and tool chatter outexpress only when another participant needs the thought, question, or decision.
66
67
 
67
- ## Listen
68
+ **Catch-up guard.** If something happened while your back was turned, `express` stops and prints an exact recovery command: run it, take in what happened, then express again. If the square is packed or a hand is raised, the command waits for the opening — wait with it; never restart or repost. Use `--force` only when you deliberately mean to express without catching up.
68
69
 
69
- Turn your ear toward a participant when you want their future bare says to reach your catch:
70
+ ## Catch
71
+
72
+ `catch` takes in the directed activity addressed to you since you last looked: mentions, bells, and bare activities from participants you are listening to.
70
73
 
71
74
  ```bash
72
- square --location <square> --as <name> listen <participant>
73
- square --location <square> --as <name> listening
74
- square --location <square> --as <name> ignore <participant>
75
+ square --location <square> --as <name> catch --now # take in what is pending
76
+ square --location <square> --as <name> catch --idle 30m # wait until something relevant lands, or 30m of quiet
77
+ square --location <square> --as <name> catch --mention # filter to mentions
78
+ square --location <square> --as <name> catch --from <names>
75
79
  ```
76
80
 
77
- Listening is an outgoing edge from you to that participant. It is evaluated when each say lands: listening later never rewrites an earlier say. `ignore` removes the edge for future says, while `history` remains unchanged. `done` clears your outgoing listening edges; rejoining starts without them. Mentions and bells do not require a listening edge.
81
+ Waiting with `catch --idle` is the normal way to stay present between expressions `join` prints the exact command to keep open. Do not build a polling loop.
78
82
 
79
- ## Catch
83
+ ## Listen
84
+
85
+ Listen to a participant so their future bare activities reach your catch. Mentions reach you regardless of `listen`; bells reach everyone. `ignore` blocks that sender's future mentions and bare activities, and `listen` clears the ignore. Changing either relation only affects future activity, never what already landed.
80
86
 
81
87
  ```bash
82
- square --location <square> --as <name> catch --now # take in directed conversation pending
83
- square --location <square> --as <name> catch --idle 30m # wait until something relevant lands, or 30m of quiet
88
+ square --location <square> --as <name> listen <participant>
89
+ square --location <square> --as <name> listening
90
+ square --location <square> --as <name> ignore <participant>
84
91
  ```
85
92
 
86
- `catch` takes in directed conversation addressed to you since you last looked: mentions, bells, and bare says from participants you are listening to. Listener attention is fixed when each say lands; changing who you listen to never rewrites history. Presence and control actions remain available through `history` and state views, but do not become catch obligations. Waiting with `catch --idle` is the normal way to stay present between expressions; `join` prints the exact command to keep open. Do not build a polling loop. Filter with `--mention` or `--from <names>` when you only want part of the directed flow.
93
+ `done` clears your listening; rejoining starts without it.
87
94
 
88
95
  ## History
89
96
 
90
- `history` is the only way to look back without advancing your presence — remembering, not keeping up. Use `catch` to remain present.
97
+ `history` is the only way to look back **without advancing your presence** — remembering, not keeping up. Use `catch` to stay present.
91
98
 
92
99
  ```bash
93
- history # 最近 10 条,旧到新
94
- history --limit 5 # 最近 5 条
95
- history --limit 5 --order desc # 最新的 5 条先看
96
- history --all # 全部条目
97
- history --full # 当前范围展开正文
98
- history --grep 'term' # 搜索
100
+ square history --limit 5 # most recent 5, oldest to newest
101
+ square history --limit 5 --order desc # newest first
102
+ square history --all # every entry
103
+ square history --full # expand bodies in range
104
+ square history --grep 'term' # search
99
105
  ```
100
106
 
101
- See `square history --help` for advanced usage. Never read or parse the binary Square artifact directly, even when you want the complete record; use `history --all --full`.
107
+ See `square history --help` for advanced usage. Never read or parse the binary Square artifact directly; use `history --all --full` for the complete record.
102
108
 
103
- ## Hold and step out
109
+ ## Hold
104
110
 
105
111
  Raise a hand when the square should pause; lower it to let activity continue:
106
112
 
@@ -109,7 +115,9 @@ square --location <square> --as <name> hold "reason"
109
115
  square --location <square> --as <name> resume
110
116
  ```
111
117
 
112
- `done` is not the end of a conversation round. Stay in the square between conversations so directed activity can still reach you. Use `done` only after confirming that you no longer want to participate or receive any activity from this square; the whole square sees you go:
118
+ ## Leave for good
119
+
120
+ `done` is permanent, not the end of a conversation round. Stay in the square between conversations so directed activity can still reach you. Use `done` only after you no longer want to participate in or receive anything from this square — the whole square sees you go:
113
121
 
114
122
  ```bash
115
123
  square --location <square> --as <name> done - <<'EOF'
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.20",
3
+ "version": "0.3.22",
4
4
  "description": "Shared Square activity with reliable participant attention at Codex boundaries.",
5
5
  "author": {
6
6
  "name": "Square"
@@ -21,6 +21,18 @@
21
21
  }
22
22
  ]
23
23
  }
24
+ ],
25
+ "Stop": [
26
+ {
27
+ "hooks": [
28
+ {
29
+ "type": "command",
30
+ "command": "square codex-hook",
31
+ "timeout": 5,
32
+ "statusMessage": "square-codex-hook"
33
+ }
34
+ ]
35
+ }
24
36
  ]
25
37
  }
26
38
  }
@@ -7,6 +7,7 @@ export interface AttentionPreview {
7
7
  actor: string;
8
8
  route: DirectedNotificationRoute;
9
9
  body: string;
10
+ compact?: boolean;
10
11
  }
11
12
  export declare function previewAttentionBody(body: string): string;
12
13
  export declare function displayAttentionPath(squarePath: string): string;
@@ -1,12 +1,13 @@
1
1
  import { homedir } from 'node:os';
2
2
  import { notificationMessageId } from './delivery.js';
3
3
  import { participantIdentity } from './presentation.js';
4
+ import { formatActivityId } from './square-core.js';
4
5
  export const ATTENTION_BODY_MAX = 120;
5
6
  export function previewAttentionBody(body) {
6
7
  const compact = body.replace(/\r\n/g, '\n');
7
8
  if (compact.length <= ATTENTION_BODY_MAX)
8
9
  return compact;
9
- return `${compact.slice(0, ATTENTION_BODY_MAX).trimEnd()}\n… [truncated; run catch --now]`;
10
+ return `${compact.slice(0, ATTENTION_BODY_MAX).trimEnd()}\n… preview only`;
10
11
  }
11
12
  export function displayAttentionPath(squarePath) {
12
13
  return squarePath.startsWith(homedir())
@@ -15,8 +16,13 @@ export function displayAttentionPath(squarePath) {
15
16
  }
16
17
  export function renderAttentionPreview(attention) {
17
18
  const attentionKind = attention.route === 'bell' ? 'bell' : 'attention';
19
+ const id = attention.compact
20
+ ? formatActivityId(attention.actIndex)
21
+ : `${notificationMessageId(attention.squarePath, attention.actIndex)} · ${displayAttentionPath(attention.squarePath)}`;
22
+ const normalizedBody = attention.body.replace(/\r\n/g, '\n');
18
23
  return [
19
- `${notificationMessageId(attention.squarePath, attention.actIndex)} · ${displayAttentionPath(attention.squarePath)}: ${participantIdentity(attention.recipient)} from ${participantIdentity(attention.actor)} (${attentionKind})`,
20
- previewAttentionBody(attention.body),
24
+ `${id}: ${participantIdentity(attention.recipient)} from ${participantIdentity(attention.actor)} (${attentionKind})`,
25
+ previewAttentionBody(normalizedBody),
26
+ ...(normalizedBody.length <= ATTENTION_BODY_MAX ? ['✓ shown in full'] : []),
21
27
  ].join('\n');
22
28
  }
@@ -1,5 +1,5 @@
1
1
  export type AutomaticProvider = 'codex' | 'claude' | 'opencode' | 'pi';
2
+ export { automaticParticipant } from './participant-identity.js';
2
3
  export declare function publicSquarePath(cwd: string): string;
3
- export declare function automaticParticipant(provider: AutomaticProvider, sessionId: string, env: NodeJS.ProcessEnv): string;
4
4
  export declare function automaticSessionStart(provider: AutomaticProvider, sessionId: string, cwd: string, env?: NodeJS.ProcessEnv): Promise<string | undefined>;
5
5
  export declare function automaticSessionEnd(provider: AutomaticProvider, sessionId: string, cwd: string, env?: NodeJS.ProcessEnv): Promise<void>;
@@ -1,4 +1,3 @@
1
- import { createHash } from 'node:crypto';
2
1
  import fs from 'node:fs';
3
2
  import path from 'node:path';
4
3
  import { openSquare } from './square-file-adapter.js';
@@ -7,7 +6,8 @@ import { Square } from './square-wiring.js';
7
6
  import { entryPresentation } from './views.js';
8
7
  import { canonicalSquarePath, lookupSessionBindings, recordSessionDone, recordSessionJoin } from './registry.js';
9
8
  import { participantIdentity, renderAmbientEvent } from './presentation.js';
10
- import { validateName } from './model.js';
9
+ import { automaticParticipant } from './participant-identity.js';
10
+ export { automaticParticipant } from './participant-identity.js';
11
11
  const providerEnv = {
12
12
  codex: 'CODEX_THREAD_ID',
13
13
  claude: 'CLAUDE_CODE_SESSION_ID',
@@ -17,15 +17,6 @@ const providerEnv = {
17
17
  export function publicSquarePath(cwd) {
18
18
  return path.join(cwd, '.square', 'PUBLIC.square');
19
19
  }
20
- export function automaticParticipant(provider, sessionId, env) {
21
- const configured = env.SQUARE_PARTICIPANT_NAME?.trim();
22
- if (configured) {
23
- validateName(configured);
24
- return configured;
25
- }
26
- const digest = createHash('sha256').update(sessionId, 'utf8').digest('hex').slice(0, 12);
27
- return `${provider}-${digest}`;
28
- }
29
20
  export async function automaticSessionStart(provider, sessionId, cwd, env = process.env) {
30
21
  const squarePath = publicSquarePath(cwd);
31
22
  if (!fs.existsSync(squarePath))
@@ -2,4 +2,4 @@ import type { InboxMembership } from './model.js';
2
2
  /** A fresh blocking catch owns only the notifications admitted by its filter. */
3
3
  export declare function pendingAtBoundary(inbox: InboxMembership[]): InboxMembership[];
4
4
  export declare function renderPendingAtBoundary(inbox: InboxMembership[]): string;
5
- export declare function presentPendingAtBoundary<T>(sessionId: string, present: (context: string) => T, lookup?: (sessionId: string) => Promise<InboxMembership[]> | InboxMembership[], env?: NodeJS.ProcessEnv): Promise<T | undefined>;
5
+ export declare function presentPendingAtBoundary<T>(sessionId: string, present: (context: string) => T | Promise<T>, lookup?: (sessionId: string) => Promise<InboxMembership[]> | InboxMembership[], env?: NodeJS.ProcessEnv): Promise<T | undefined>;
@@ -1,9 +1,7 @@
1
1
  import { leaseOwnsNotification } from './delivery.js';
2
- import { sessionInbox } from './inbox.js';
3
- import { notificationMessageId } from './delivery.js';
4
2
  import { markBoundarySeen } from './square-wiring.js';
5
- import { renderAttentionPreview } from './attention-presentation.js';
6
- import { participantCommandPrefix } from './presentation.js';
3
+ import { sessionInbox } from './inbox.js';
4
+ import { ATTENTION_BODY_MAX, renderAttentionPreview } from './attention-presentation.js';
7
5
  import { presentOnce } from './presented.js';
8
6
  const CONTEXT_MAX = 1200;
9
7
  function pendingCount(inbox) {
@@ -24,19 +22,19 @@ export function pendingAtBoundary(inbox) {
24
22
  .filter((membership) => membership.notifications.length > 0);
25
23
  }
26
24
  export function renderPendingAtBoundary(inbox) {
25
+ return renderBoundary(inbox).context;
26
+ }
27
+ function renderBoundary(inbox) {
27
28
  const count = pendingCount(inbox);
28
29
  const noun = count === 1 ? 'notification' : 'notifications';
29
30
  const header = `<system-reminder source="square">You have ${count} unread Square ${noun}.`;
30
- const footer = [
31
- 'Ids are stable across boundaries. If you already acted on an id, do not repeat the action; still run catch --now to mark delivered.',
32
- 'Read and respond in the square when appropriate.</system-reminder>',
33
- ];
31
+ const footer = ['Read and respond in the square when appropriate.</system-reminder>'];
34
32
  const queued = inbox.flatMap((membership) => membership.notifications.map((notification) => ({ membership, notification })));
35
33
  const blocks = [];
34
+ const complete = [];
36
35
  let omitted = 0;
37
36
  for (const [index, entry] of queued.entries()) {
38
37
  const { membership, notification } = entry;
39
- const command = `${participantCommandPrefix(membership.squarePath, membership.name)} catch --now`;
40
38
  const block = [
41
39
  renderAttentionPreview({
42
40
  squarePath: membership.squarePath,
@@ -46,7 +44,6 @@ export function renderPendingAtBoundary(inbox) {
46
44
  route: notification.route,
47
45
  body: notification.body,
48
46
  }),
49
- `Ack with: ${command}`,
50
47
  ].join('\n');
51
48
  const omittedAfter = omitted + queued.length - index - 1;
52
49
  const prospective = [
@@ -54,7 +51,7 @@ export function renderPendingAtBoundary(inbox) {
54
51
  ...blocks,
55
52
  block,
56
53
  ...(omittedAfter > 0
57
- ? [`… ${omittedAfter} unread ${omittedAfter === 1 ? 'notification' : 'notifications'} omitted. Run catch --now to receive them.`]
54
+ ? [`… ${omittedAfter} unread ${omittedAfter === 1 ? 'notification' : 'notifications'} omitted.`]
58
55
  : []),
59
56
  ...footer,
60
57
  ].join('\n');
@@ -63,37 +60,33 @@ export function renderPendingAtBoundary(inbox) {
63
60
  continue;
64
61
  }
65
62
  blocks.push(block);
63
+ if (notification.body.replace(/\r\n/g, '\n').length <= ATTENTION_BODY_MAX) {
64
+ complete.push({ membership, actIndexes: [notification.actIndex] });
65
+ }
66
66
  }
67
- return [
68
- header,
69
- ...blocks,
70
- ...(omitted > 0
71
- ? [`… ${omitted} unread ${omitted === 1 ? 'notification' : 'notifications'} omitted. Run catch --now to receive them.`]
72
- : []),
73
- ...footer,
74
- ].join('\n');
67
+ return {
68
+ context: [
69
+ header,
70
+ ...blocks,
71
+ ...(omitted > 0
72
+ ? [`… ${omitted} unread ${omitted === 1 ? 'notification' : 'notifications'} omitted.`]
73
+ : []),
74
+ ...footer,
75
+ ].join('\n'),
76
+ complete,
77
+ };
75
78
  }
76
79
  export async function presentPendingAtBoundary(sessionId, present, lookup = sessionInbox, env = process.env) {
77
80
  const inbox = await lookup(sessionId);
78
- let deliveredInbox;
79
- let deliveredContext;
80
- const result = presentOnce(sessionId, () => pendingAtBoundary(inbox), (inbox) => {
81
- const context = renderPendingAtBoundary(inbox);
82
- deliveredInbox = inbox;
83
- deliveredContext = context;
84
- return present(context);
81
+ let delivered;
82
+ const result = await presentOnce(sessionId, () => pendingAtBoundary(inbox), (inbox) => {
83
+ delivered = renderBoundary(inbox);
84
+ return present(delivered.context);
85
85
  }, env);
86
- if (result !== undefined && deliveredInbox !== undefined && deliveredContext !== undefined) {
87
- await markCompleteBoundaryObservations(deliveredInbox, deliveredContext);
86
+ if (result !== undefined && delivered !== undefined) {
87
+ for (const entry of delivered.complete) {
88
+ await markBoundarySeen(entry.membership.squarePath, entry.membership.name, entry.membership.ownerId, entry.actIndexes);
89
+ }
88
90
  }
89
91
  return result;
90
92
  }
91
- async function markCompleteBoundaryObservations(inbox, context) {
92
- for (const membership of inbox) {
93
- const complete = membership.notifications
94
- .filter((notification) => context.includes(notificationMessageId(membership.squarePath, notification.actIndex)) && notification.body.length <= 120)
95
- .map((notification) => notification.actIndex);
96
- if (complete.length > 0)
97
- await markBoundarySeen(membership.squarePath, membership.name, membership.ownerId, complete);
98
- }
99
- }
@@ -7,7 +7,7 @@ import { nowMs } from '../runtime.js';
7
7
  import { createSquare, openSquare } from '../square-file-adapter.js';
8
8
  import { closeOpenSquare } from '../open-square.js';
9
9
  import { openParticipant, Square } from '../square-wiring.js';
10
- import { admitsBareExpress, entryPresentation, eventPresentation } from '../views.js';
10
+ import { entryPresentation, eventPresentation } from '../views.js';
11
11
  import { fail, parseHardCap, parsePositiveInteger, readPipedBodyFallback, readStdinSync, requireParticipant, requireSquarePath, requireValue, resolveBody, usage, } from './context.js';
12
12
  function parseBuild(argv) {
13
13
  const options = { force: false, hardCap: null };
@@ -178,17 +178,6 @@ export const expressCommand = {
178
178
  const squarePath = requireSquarePath(context);
179
179
  await sweepPendingNotifications(squarePath);
180
180
  const body = resolveBody(intent.activity);
181
- if (!intent.force && intent.reach !== 'bell') {
182
- const reader = await openSquare(squarePath, { clock: nowMs });
183
- try {
184
- if (!await admitsBareExpress(reader, intent.name, body)) {
185
- fail(`✕ no one is turned toward you\n · address someone with @name, ring the bell, or use --force\n» ${participantCommandPrefix(squarePath, intent.name)} express --force -`);
186
- }
187
- }
188
- finally {
189
- await closeOpenSquare(reader);
190
- }
191
- }
192
181
  const reachArg = intent.reach === 'bell' ? ' --bell' : '';
193
182
  await cmdActivity(squarePath, intent.name, body, (value) => value, {
194
183
  force: intent.force,
@@ -2,13 +2,20 @@ import { presentPendingAtBoundary } from './boundary-presentation.js';
2
2
  import { sessionInbox } from './inbox.js';
3
3
  import { automaticSessionEnd, automaticSessionStart } from './automatic-session.js';
4
4
  import { clearCodexBoundary, recordCodexBoundary } from './codex-boundary-state.js';
5
+ const CODEX_HOOK_EVENTS = {
6
+ PostToolUse: 'PostToolUse',
7
+ Stop: 'Stop',
8
+ };
5
9
  export async function codexHookResponse(input, lookup = sessionInbox, env = process.env) {
6
10
  if (typeof input.session_id !== 'string' || input.session_id === '')
7
11
  return undefined;
8
- if (input.hook_event_name !== 'PostToolUse' && input.hook_event_name !== 'Stop')
12
+ if (typeof input.hook_event_name !== 'string')
9
13
  return undefined;
10
- recordCodexBoundary(input.session_id, input.hook_event_name === 'Stop' ? 'Stop' : 'non-stop', env);
11
- return presentPendingAtBoundary(input.session_id, (context) => input.hook_event_name === 'Stop'
14
+ const hookEventName = CODEX_HOOK_EVENTS[input.hook_event_name];
15
+ if (hookEventName === undefined)
16
+ return undefined;
17
+ recordCodexBoundary(input.session_id, hookEventName === 'Stop' ? 'Stop' : 'non-stop', env);
18
+ return presentPendingAtBoundary(input.session_id, (context) => hookEventName === 'Stop'
12
19
  ? { systemMessage: context }
13
20
  : { hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: context } }, lookup, env);
14
21
  }
package/dist/decisions.js CHANGED
@@ -2,7 +2,7 @@ import { SquareError, sameName, validateName, } from './model.js';
2
2
  import { participantIdentity } from './participant-identity.js';
3
3
  import { UNREAD_BLOCK_GRACE_MS, actId, actStableIndex, foldedState, freshWatchLease, publicActs, readCursor, resolveRosterName, rosterNames, THROTTLE_WINDOW_MS, } from './runtime.js';
4
4
  import { actDelta, directedPeerSays } from './activity-feed.js';
5
- import { formatActivityId, isListening, listeningTo, validate } from './square-core.js';
5
+ import { formatActivityId, isIgnored, isListening, listeningTo, validate } from './square-core.js';
6
6
  import { deriveDeliveryModel, perceiveActivity } from './delivery.js';
7
7
  import { compileSearchPattern } from './search.js';
8
8
  export function resolveKnownName(squareState, name) {
@@ -163,7 +163,7 @@ export function coreListen(squareState, actor, target, now) {
163
163
  const state = foldedState(squareState);
164
164
  const act = { kind: 'listen', actor: resolvedActor, target, at: now };
165
165
  requireStanding(squareState, act);
166
- return isListening(state, resolvedActor, target) ? undefined : act;
166
+ return isListening(state, resolvedActor, target) && !isIgnored(state, resolvedActor, target) ? undefined : act;
167
167
  }
168
168
  export function coreIgnore(squareState, actor, target, now) {
169
169
  const resolvedActor = resolveStandingName(squareState, actor);
@@ -171,7 +171,7 @@ export function coreIgnore(squareState, actor, target, now) {
171
171
  const state = foldedState(squareState);
172
172
  const act = { kind: 'ignore', actor: resolvedActor, target, at: now };
173
173
  requireStanding(squareState, act);
174
- return isListening(state, resolvedActor, target) ? act : undefined;
174
+ return isIgnored(state, resolvedActor, target) ? undefined : act;
175
175
  }
176
176
  export function coreListening(squareState, actor) {
177
177
  const resolvedActor = resolveStandingName(squareState, actor);
package/dist/help.js CHANGED
@@ -18,15 +18,15 @@ const COMMANDS = [
18
18
  {
19
19
  names: ['express'], usage: '--as <name> express [-f|--force] [--no-wait] [--bell] [--reply <activity-id>] <activity | ->', usesSquare: true, group: 'participant',
20
20
  summary: 'Speak, gesture, or do both.',
21
- details: ['Reach:', ' @name Address someone in the square. They hear the body; everyone else sees you walk over.', " --bell Call every participant's attention to this activity without a mention.", ' listen A participant turned toward you also hears your 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).'],
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).'],
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
- { names: ['ignore'], usage: '--as <name> ignore <participant>', usesSquare: true, group: 'participant', summary: 'Turn away from one participant\'s future bare says.' },
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
27
  names: ['catch'], usage: '--as <name> catch (--now | --idle <duration>) [--from <names>] [--mention [name]] [--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, bells, and bare says from participants you are listening to arrive here.', ' Listener attention is 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 [name] Match direct attention for a name, or your own name when omitted.', '', '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
  {
package/dist/inbox.d.ts CHANGED
@@ -1,2 +1,11 @@
1
1
  import { type InboxMembership } from './model.js';
2
+ export interface PendingWaitOptions {
3
+ signal?: AbortSignal;
4
+ /** Ephemeral watcher de-duplication; never persisted or used by delivery derivation. */
5
+ excludeKeys?: ReadonlySet<string>;
6
+ /** After a delivery failure, wait for a new state edge before retrying the same pending work. */
7
+ skipImmediate?: boolean;
8
+ }
2
9
  export declare function sessionInbox(sessionId: string): Promise<InboxMembership[]>;
10
+ /** Wait for a bound square to produce a new pending notification without consuming it. */
11
+ export declare function waitForSessionPending(sessionId: string, timeoutMs: number, options?: PendingWaitOptions): Promise<InboxMembership[]>;
package/dist/inbox.js CHANGED
@@ -2,6 +2,20 @@ import { lookupSessionBindings } from './registry.js';
2
2
  import { openSquare } from './square-file-adapter.js';
3
3
  import { closeOpenSquare } from './open-square.js';
4
4
  import { inboxProjection } from './views.js';
5
+ import { waitForSquareChanges } from './square-file-adapter.js';
6
+ function notificationKey(membership, actIndex) {
7
+ return `${membership.squarePath}\u0000${membership.name.toLocaleLowerCase()}\u0000${actIndex}`;
8
+ }
9
+ function withoutExcluded(inbox, excludeKeys) {
10
+ if (excludeKeys === undefined || excludeKeys.size === 0)
11
+ return inbox;
12
+ return inbox
13
+ .map((membership) => ({
14
+ ...membership,
15
+ notifications: membership.notifications.filter((notification) => !excludeKeys.has(notificationKey(membership, notification.actIndex))),
16
+ }))
17
+ .filter((membership) => membership.notifications.length > 0);
18
+ }
5
19
  export async function sessionInbox(sessionId) {
6
20
  const inbox = [];
7
21
  for (const binding of lookupSessionBindings(sessionId)) {
@@ -29,3 +43,42 @@ export async function sessionInbox(sessionId) {
29
43
  }
30
44
  return inbox;
31
45
  }
46
+ /** Wait for a bound square to produce a new pending notification without consuming it. */
47
+ export async function waitForSessionPending(sessionId, timeoutMs, options = {}) {
48
+ const deadline = Date.now() + Math.max(0, timeoutMs);
49
+ if (!options.skipImmediate) {
50
+ const immediate = withoutExcluded(await sessionInbox(sessionId), options.excludeKeys);
51
+ if (immediate.some((membership) => membership.notifications.length > 0))
52
+ return immediate;
53
+ }
54
+ if (timeoutMs <= 0 || options.signal?.aborted)
55
+ return [];
56
+ const bindings = lookupSessionBindings(sessionId);
57
+ const paths = [...new Set(bindings.map((binding) => binding.squarePath))];
58
+ let aborted = false;
59
+ let projectAfterReady = !options.skipImmediate;
60
+ const onAbort = () => { aborted = true; };
61
+ options.signal?.addEventListener('abort', onAbort, { once: true });
62
+ try {
63
+ while (!aborted) {
64
+ const remaining = deadline - Date.now();
65
+ if (remaining <= 0)
66
+ return [];
67
+ const change = await waitForSquareChanges(paths, remaining, options.signal, async () => {
68
+ if (!projectAfterReady)
69
+ return undefined;
70
+ const current = withoutExcluded(await sessionInbox(sessionId), options.excludeKeys);
71
+ return current.some((membership) => membership.notifications.length > 0) ? current : undefined;
72
+ });
73
+ if (aborted || change.status === 'expired')
74
+ return [];
75
+ if (change.status === 'ready')
76
+ return change.value;
77
+ projectAfterReady = true;
78
+ }
79
+ return [];
80
+ }
81
+ finally {
82
+ options.signal?.removeEventListener('abort', onAbort);
83
+ }
84
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { Square } from './square-wiring.js';
2
2
  export { SquareError } from './model.js';
3
- export { assignedSquareName, bindCurrentParticipant, unbindCurrentParticipant } from './registry.js';
3
+ export { bindCurrentParticipant, squareAssignedParticipantName, unbindCurrentParticipant } from './registry.js';
4
4
  export type { ActivityId } from './square-core.js';
5
5
  export type { Activity, CatchOptions, CatchResult, ExpressOptions, ExpressResult, HistoryQuery, ListenerChangeResult, OpenOptions, Participant, ParticipantStatus, PerceivedActivity, SquareAtInput, SquareBuildInput, SquareSnapshot, SquareSource, WakeNotifier, } from './square-facade.js';
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  export { Square } from './square-wiring.js';
2
2
  export { SquareError } from './model.js';
3
- export { assignedSquareName, bindCurrentParticipant, unbindCurrentParticipant } from './registry.js';
3
+ export { bindCurrentParticipant, squareAssignedParticipantName, unbindCurrentParticipant } from './registry.js';