@astrosheep/square 0.3.62 → 0.3.64

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 (168) hide show
  1. package/claude-plugin/.claude-plugin/plugin.json +3 -2
  2. package/claude-plugin/hooks/hooks.json +1 -22
  3. package/claude-plugin/hooks/register.js +119 -0
  4. package/claude-plugin/types.d.ts +7 -0
  5. package/codex-plugin/.codex-plugin/plugin.json +1 -1
  6. package/dist/activity-feed.d.ts +1 -10
  7. package/dist/activity-feed.js +1 -27
  8. package/dist/activity.js +57 -25
  9. package/dist/artifact.js +12 -24
  10. package/dist/attention-presentation.js +1 -0
  11. package/dist/automatic-session.d.ts +3 -1
  12. package/dist/automatic-session.js +21 -34
  13. package/dist/boundary-presentation.js +4 -13
  14. package/dist/canonical-path.d.ts +11 -0
  15. package/dist/canonical-path.js +47 -0
  16. package/dist/catch-decisions.js +14 -10
  17. package/dist/catch-selection.d.ts +6 -0
  18. package/dist/catch-selection.js +14 -0
  19. package/dist/claude-delivery.d.ts +11 -0
  20. package/dist/claude-delivery.js +145 -0
  21. package/dist/claude-mod.d.ts +8 -0
  22. package/dist/claude-mod.js +164 -0
  23. package/dist/cli/context.d.ts +0 -1
  24. package/dist/cli/context.js +9 -21
  25. package/dist/cli/harness-command.d.ts +0 -1
  26. package/dist/cli/harness-command.js +0 -6
  27. package/dist/cli/observation-commands.d.ts +1 -1
  28. package/dist/cli/observation-commands.js +31 -58
  29. package/dist/cli/program.js +20 -18
  30. package/dist/cli/registry.js +2 -2
  31. package/dist/cli/square-commands.js +10 -6
  32. package/dist/codex-hook.js +9 -12
  33. package/dist/codex-queue.d.ts +2 -2
  34. package/dist/codex-queue.js +7 -5
  35. package/dist/decisions.d.ts +2 -6
  36. package/dist/decisions.js +16 -17
  37. package/dist/delivery-health.d.ts +1 -2
  38. package/dist/delivery-health.js +4 -5
  39. package/dist/delivery-operations.d.ts +2 -5
  40. package/dist/delivery-operations.js +77 -214
  41. package/dist/delivery.d.ts +25 -47
  42. package/dist/delivery.js +14 -46
  43. package/dist/diagnostic-redaction.js +7 -9
  44. package/dist/file-changes.d.ts +0 -1
  45. package/dist/file-changes.js +2 -19
  46. package/dist/harness-claude.js +8 -6
  47. package/dist/harness-codex.d.ts +0 -2
  48. package/dist/harness-codex.js +0 -2
  49. package/dist/harness-links.js +13 -17
  50. package/dist/harness.js +3 -7
  51. package/dist/help.js +4 -3
  52. package/dist/host-ledger-file-adapter.d.ts +17 -8
  53. package/dist/host-ledger-file-adapter.js +164 -80
  54. package/dist/host-ledger.d.ts +55 -50
  55. package/dist/inbox.d.ts +0 -3
  56. package/dist/inbox.js +13 -30
  57. package/dist/index.d.ts +1 -1
  58. package/dist/index.js +1 -1
  59. package/dist/list.js +1 -4
  60. package/dist/mcp.js +18 -40
  61. package/dist/mention-parse.js +3 -9
  62. package/dist/model.d.ts +11 -2
  63. package/dist/model.js +1 -1
  64. package/dist/notifications.d.ts +3 -16
  65. package/dist/notifications.js +41 -122
  66. package/dist/opencode-delivery.d.ts +14 -0
  67. package/dist/opencode-delivery.js +171 -0
  68. package/dist/opencode.d.ts +39 -18
  69. package/dist/opencode.js +145 -40
  70. package/dist/packages/agent-delivery/src/claude-auth.d.ts +13 -0
  71. package/dist/packages/agent-delivery/src/claude-auth.js +70 -0
  72. package/dist/packages/agent-delivery/src/claude-native.d.ts +28 -0
  73. package/dist/packages/agent-delivery/src/claude-native.js +65 -0
  74. package/dist/packages/agent-delivery/src/claude.d.ts +14 -0
  75. package/dist/packages/agent-delivery/src/claude.js +66 -0
  76. package/dist/packages/agent-delivery/src/deadline.d.ts +11 -0
  77. package/dist/packages/agent-delivery/src/deadline.js +41 -0
  78. package/dist/packages/agent-delivery/src/errors.d.ts +16 -0
  79. package/dist/packages/agent-delivery/src/errors.js +41 -0
  80. package/dist/packages/agent-delivery/src/guards.d.ts +6 -0
  81. package/dist/packages/agent-delivery/src/guards.js +6 -0
  82. package/dist/packages/agent-delivery/src/index.d.ts +83 -0
  83. package/dist/packages/agent-delivery/src/index.js +110 -0
  84. package/dist/packages/agent-delivery/src/local-endpoint.d.ts +26 -0
  85. package/dist/packages/agent-delivery/src/local-endpoint.js +114 -0
  86. package/dist/packages/agent-delivery/src/opencode-native.d.ts +33 -0
  87. package/dist/packages/agent-delivery/src/opencode-native.js +75 -0
  88. package/dist/packages/agent-delivery/src/opencode.d.ts +14 -0
  89. package/dist/packages/agent-delivery/src/opencode.js +127 -0
  90. package/dist/packages/agent-delivery/src/paseo-native.d.ts +58 -0
  91. package/dist/packages/agent-delivery/src/paseo-native.js +122 -0
  92. package/dist/packages/agent-delivery/src/paseo.d.ts +19 -0
  93. package/dist/packages/agent-delivery/src/paseo.js +106 -0
  94. package/dist/packages/agent-delivery/src/pi-client.d.ts +10 -0
  95. package/dist/packages/agent-delivery/src/pi-client.js +106 -0
  96. package/dist/packages/agent-delivery/src/pi-protocol.d.ts +15 -0
  97. package/dist/packages/agent-delivery/src/pi-protocol.js +50 -0
  98. package/dist/packages/agent-delivery/src/pi-send.d.ts +23 -0
  99. package/dist/packages/agent-delivery/src/pi-send.js +4 -0
  100. package/dist/packages/agent-delivery/src/pi.d.ts +25 -0
  101. package/dist/packages/agent-delivery/src/pi.js +209 -0
  102. package/dist/packages/agent-delivery/src/types.d.ts +120 -0
  103. package/dist/participant-host.js +12 -13
  104. package/dist/participant-identity.d.ts +21 -0
  105. package/dist/participant-identity.js +32 -11
  106. package/dist/paseo-delivery.d.ts +2 -2
  107. package/dist/paseo-delivery.js +35 -27
  108. package/dist/paseo-timeline.js +25 -21
  109. package/dist/paseo.d.ts +2 -2
  110. package/dist/paseo.js +1 -2
  111. package/dist/ports.d.ts +7 -20
  112. package/dist/presentation-operations.js +16 -4
  113. package/dist/presentation.d.ts +19 -10
  114. package/dist/presentation.js +99 -177
  115. package/dist/registry.d.ts +15 -66
  116. package/dist/registry.js +45 -123
  117. package/dist/routes.d.ts +1 -28
  118. package/dist/routes.js +22 -104
  119. package/dist/runtime.d.ts +3 -9
  120. package/dist/runtime.js +14 -67
  121. package/dist/search.d.ts +0 -2
  122. package/dist/search.js +3 -10
  123. package/dist/square-actions.js +48 -72
  124. package/dist/square-application.js +25 -23
  125. package/dist/square-core.d.ts +3 -0
  126. package/dist/square-core.js +22 -5
  127. package/dist/square-facade.d.ts +2 -0
  128. package/dist/square-file-adapter.js +4 -7
  129. package/dist/square-projections.d.ts +40 -45
  130. package/dist/square-projections.js +43 -73
  131. package/dist/square-state.js +1 -4
  132. package/dist/square-storage.d.ts +5 -8
  133. package/dist/square-storage.js +13 -36
  134. package/dist/square-wiring.d.ts +0 -5
  135. package/dist/square-wiring.js +3 -21
  136. package/dist/stream.js +4 -6
  137. package/dist/time.js +2 -7
  138. package/dist/views.d.ts +4 -16
  139. package/dist/views.js +12 -19
  140. package/dist/wake-attempts.d.ts +2 -56
  141. package/dist/wake-attempts.js +15 -110
  142. package/dist/wake-eligibility.d.ts +43 -0
  143. package/dist/wake-eligibility.js +106 -0
  144. package/dist/wake-evidence.d.ts +32 -9
  145. package/dist/wake-evidence.js +29 -24
  146. package/dist/wake-sink.d.ts +13 -9
  147. package/dist/wake-sink.js +35 -54
  148. package/dist/watch.js +13 -21
  149. package/extensions/square-pi.js +10 -5
  150. package/guides/architect.md +77 -77
  151. package/guides/brainstorm.md +197 -197
  152. package/guides/claude-native.md +101 -0
  153. package/guides/host-ledger.md +3 -1
  154. package/guides/participant.md +14 -7
  155. package/guides/pi-native-delivery.md +43 -0
  156. package/package.json +4 -3
  157. package/dist/claude-hook.d.ts +0 -10
  158. package/dist/claude-hook.js +0 -60
  159. package/dist/paseo-connection.d.ts +0 -15
  160. package/dist/paseo-connection.js +0 -141
  161. package/dist/presence.d.ts +0 -2
  162. package/dist/presence.js +0 -24
  163. package/dist/presented.d.ts +0 -2
  164. package/dist/presented.js +0 -10
  165. package/dist/state-cell.d.ts +0 -14
  166. package/dist/wake-port.d.ts +0 -23
  167. package/dist/wake-port.js +0 -27
  168. /package/dist/{state-cell.js → packages/agent-delivery/src/types.js} +0 -0
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.62",
4
- "description": "Native Claude Code turn-boundary delivery for Square participants",
3
+ "version": "0.3.64",
4
+ "description": "Native Claude Code inbox delivery for Square participants",
5
+ "types": "./types.d.ts",
5
6
  "author": {
6
7
  "name": "Square"
7
8
  }
@@ -1,22 +1 @@
1
- {
2
- "description": "Bounded Square inbox admission between Claude Code agent steps",
3
- "hooks": {
4
- "SessionStart": [
5
- { "hooks": [{ "type": "command", "command": "square claude-hook", "timeout": 5 }] }
6
- ],
7
- "SessionEnd": [
8
- { "hooks": [{ "type": "command", "command": "square claude-hook", "timeout": 5 }] }
9
- ],
10
- "PostToolBatch": [
11
- {
12
- "hooks": [
13
- {
14
- "type": "command",
15
- "command": "square claude-hook",
16
- "timeout": 5
17
- }
18
- ]
19
- }
20
- ]
21
- }
22
- }
1
+ {"modules":["./register.js"]}
@@ -0,0 +1,119 @@
1
+ // Claude Code 2.1.295 mod. No Node imports, shell interpolation, or child credentials.
2
+ const hasToken = (text) => typeof text === 'string' && /(?:^|\n)\[square-inbox:[a-zA-Z0-9-]+\](?:\n|$)/.test(text);
3
+
4
+ async function coordinate($) {
5
+ return { sessionId: await $.session.id(), cwd: await $.session.cwd(), version: (await $.session.version()).version, endpoint: await $.env.get('CLAUDE_CODE_MESSAGING_SOCKET') };
6
+ }
7
+ async function bridge($, value, budget = 2800) {
8
+ const result = await $.process.run(['square', 'claude-mod'], { cwd: value.cwd, stdin: JSON.stringify(value), timeoutMs: Math.max(1, Math.min(2800, budget)) });
9
+ if (result.exitCode !== 0 || result.isStdoutTruncated) throw new Error('Square native inbox bridge unavailable');
10
+ return JSON.parse(result.stdout);
11
+ }
12
+ async function snapshot($, id) { return (await $.state.get({ plugin: 'square', key: 'inbox', id })).value; }
13
+ async function cancellation($, id) { return (await $.state.get({ plugin: 'square', key: 'cancelAt', id })).value; }
14
+ function confirmLater($, c, operation, text) {
15
+ // State reads are dispatch snapshots. This ONE fresh timer dispatch is not polling.
16
+ // Reload cancels it; lost evidence stays unknown and is never blindly replayed.
17
+ try { $.clock.after(0, () => {
18
+ (async () => {
19
+ if (await $.session.id() !== c.sessionId) return;
20
+ const old = await snapshot($, c.sessionId);
21
+ const cancelled = await cancellation($, c.sessionId);
22
+ await bridge($, { ...c, cancelAt: cancelled?.at, cancelledBindings: cancelled?.bindings, bindings: old?.bindings, operation, text });
23
+ })().catch(() => undefined);
24
+ }); } catch { /* denied scheduling leaves evidence unknown, never a second next */ }
25
+ }
26
+ async function save($, id, update, version) {
27
+ return $.state.set({ plugin: 'square', key: 'inbox', id }, update, version === undefined ? undefined : { ifVersion: version });
28
+ }
29
+ async function reconcile($, sessionId, operation = 'start', resume = false) {
30
+ const read = await coordinate($);
31
+ const c = sessionId ? { ...read, sessionId } : read;
32
+ const held = await $.state.get({ plugin: 'square', key: 'inbox', id: c.sessionId });
33
+ const old = held.value;
34
+ const cancelled = await cancellation($, c.sessionId);
35
+ const result = await bridge($, { ...c, operation, cancelAt: cancelled?.at, cancelledBindings: cancelled?.bindings, bindings: old?.bindings, resume });
36
+ // A concurrent abort must win over a stale reconciliation write.
37
+ await save($, c.sessionId, { coordinate: c, bindings: result.bindings }, held.version);
38
+ if (!result.available) await $.ui.log(result.diagnostic);
39
+ }
40
+
41
+ async function cancel($) {
42
+ const c = await coordinate($);
43
+ const old = await snapshot($, c.sessionId);
44
+ const cancelAt = await $.clock.now();
45
+ const captured = old?.bindings ?? [];
46
+ await $.state.set({ plugin: 'square', key: 'cancelAt', id: c.sessionId }, { at: cancelAt, bindings: captured });
47
+ const result = await bridge($, { ...c, bindings: captured, cancelAt, cancelledBindings: captured, operation: 'cancel' });
48
+ try { $.clock.after(0, () => {
49
+ (async () => {
50
+ const held = await $.state.get({ plugin: 'square', key: 'cancelAt', id: c.sessionId });
51
+ if (held.value?.at === cancelAt) await $.state.set({ plugin: 'square', key: 'cancelAt', id: c.sessionId }, { at: cancelAt, bindings: result.cancelledBindings }, { ifVersion: held.version });
52
+ })().catch(() => undefined);
53
+ }); } catch { /* captured cancellation remains engine-owned */ }
54
+ }
55
+
56
+ export function register(on) {
57
+ on('session.start', async ($, e, next) => {
58
+ await reconcile($).catch(() => undefined);
59
+ return next(e);
60
+ });
61
+ on('classic.SessionStart', async ($, e, next) => {
62
+ await reconcile($, e.session_id, 'start', e.source === 'resume').catch(() => undefined);
63
+ return next(e);
64
+ });
65
+ on('tool.call', async ($, e, next) => {
66
+ try { return await next(e); }
67
+ finally { if (!e.agentId) await reconcile($, undefined, 'reconcile').catch(() => undefined); }
68
+ });
69
+ on('turn.start', async ($, e, next) => {
70
+ if (!e.agentId) await reconcile($, undefined, 'reconcile').catch(() => undefined);
71
+ return next(e);
72
+ });
73
+ on('session.end', async ($, e, next) => {
74
+ // Pinned old ID and captured ownership epochs, never a later current coordinate.
75
+ const endedAt = await $.clock.now().catch(() => undefined);
76
+ const held = await $.state.get({ plugin: 'square', key: 'inbox', id: e.sessionId }).catch(() => undefined);
77
+ const old = held?.value;
78
+ const c = old?.coordinate ?? await coordinate($).catch(() => undefined);
79
+ if (c) {
80
+ const result = await bridge($, { ...c, sessionId: e.sessionId, bindings: old?.bindings, endedAt, operation: 'end' }, next.budget.remainingMs).catch(() => undefined);
81
+ // Retired membership lives in the host ledger; conversation state may be cleared.
82
+ if (result) await save($, e.sessionId, { coordinate: { ...c, sessionId: e.sessionId }, bindings: [] }, held?.version).catch(() => undefined);
83
+ }
84
+ return next(e);
85
+ });
86
+ on('turn.abort', async ($, e, next) => {
87
+ await cancel($).catch(() => undefined);
88
+ return next(e);
89
+ });
90
+ on('turn.complete', async ($, e, next) => {
91
+ // Terminal Esc aborts are reported here, not through the plugin's turn.abort op.
92
+ if (!e.agentId && e.reason === 'aborted') await cancel($).catch(() => undefined);
93
+ return next(e);
94
+ });
95
+ on('session.receive', async ($, e, next) => {
96
+ if (e.agentId || e.origin?.kind !== 'peer' || !hasToken(e.text)) return next(e);
97
+ const c = await coordinate($).catch(() => undefined);
98
+ if (!c) return next(e);
99
+ const old = await snapshot($, c.sessionId).catch(() => undefined);
100
+ const cancelled = await cancellation($, c.sessionId).catch(() => undefined);
101
+ const guard = await bridge($, { ...c, cancelAt: cancelled?.at, cancelledBindings: cancelled?.bindings, bindings: old?.bindings, operation: 'guard', text: e.text }).catch(() => undefined);
102
+ if (guard?.recognized && !guard.current) return { consumed: 'Square delivery is stale or cancelled' };
103
+ // Preserve native policy: hold/refuse reject this gate; never call next twice.
104
+ const result = await next(e);
105
+ if (guard?.current && result.text === e.text) confirmLater($, c, 'admitted', result.text);
106
+ return result;
107
+ });
108
+ on('session.append', async ($, e, next) => {
109
+ if (e.agentId || e.origin?.kind !== 'peer') return next(e);
110
+ const c = await coordinate($).catch(() => undefined);
111
+ const result = await next(e);
112
+ if (!c || result.message?.role !== 'user') return result;
113
+ // Match a complete payload inside one returned stored text block, not entry text.
114
+ for (const block of result.message.content ?? []) {
115
+ if (block.type === 'text' && hasToken(block.text)) confirmLater($, c, 'stored', block.text);
116
+ }
117
+ return result;
118
+ });
119
+ }
@@ -0,0 +1,7 @@
1
+ export type SquareInboxBinding = { location: string; participant: string; session: string; epoch?: number };
2
+ export type SquareInboxCoordinate = { sessionId: string; cwd: string; version: string; endpoint?: string };
3
+ export type SquareInboxCancellation = { at: number; bindings?: SquareInboxBinding[] };
4
+ export type SquareInboxState = { coordinate: SquareInboxCoordinate; bindings?: SquareInboxBinding[] };
5
+ declare module 'claude-code' {
6
+ interface PluginState { square: { inbox: StateFamily<SquareInboxState>; cancelAt: StateFamily<SquareInboxCancellation> } }
7
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.62",
3
+ "version": "0.3.64",
4
4
  "description": "Shared Square activity with reliable participant attention at Codex boundaries.",
5
5
  "author": {
6
6
  "name": "Square"
@@ -1,16 +1,7 @@
1
- import { type StoredAct, type SquareState, type PublicAct, type RoomChangeAct } from './model.js';
2
- export interface ActivityFeedFilter {
3
- participants?: string[];
4
- mention?: string;
5
- }
1
+ import { type StoredAct, type SquareState } from './model.js';
6
2
  export declare function actDelta(acts: StoredAct[], cursor: number): StoredAct[];
7
3
  /** Visible activities after the participant's derived continuous-seen prefix. */
8
4
  export declare function deliveryDelta(squareState: SquareState, name: string, delivery?: import("./delivery.js").DeliveryModel): StoredAct[];
9
- export declare function peerRoomChanges(delta: StoredAct[], name: string): RoomChangeAct[];
10
- export declare function peerPublicActs(delta: StoredAct[], name: string): PublicAct[];
11
5
  export declare function directedPeerSays(squareState: SquareState, delta: StoredAct[], name: string, delivery?: import("./delivery.js").DeliveryModel): Extract<StoredAct, {
12
6
  kind: 'say';
13
7
  }>[];
14
- export declare function matchesFeedFilter(act: StoredAct, filter: ActivityFeedFilter, recipients?: readonly string[]): boolean;
15
- export declare function filteredPeerActivities(delta: StoredAct[], name: string, filter: ActivityFeedFilter): PublicAct[];
16
- export declare function filteredRoomChanges(delta: StoredAct[], name: string, filter: ActivityFeedFilter): RoomChangeAct[];
@@ -1,5 +1,4 @@
1
- import { sameName } from './model.js';
2
- import { deriveDeliveryModel, matchesCatchFilter } from './delivery.js';
1
+ import { deriveDeliveryModel } from './delivery.js';
3
2
  export function actDelta(acts, cursor) {
4
3
  return acts.filter((act) => act.index > cursor);
5
4
  }
@@ -7,31 +6,6 @@ export function actDelta(acts, cursor) {
7
6
  export function deliveryDelta(squareState, name, delivery = deriveDeliveryModel(squareState)) {
8
7
  return actDelta(squareState.acts, delivery.cursorFor(name));
9
8
  }
10
- export function peerRoomChanges(delta, name) {
11
- return delta.filter((act) => act.actor !== undefined && !sameName(act.actor, name) && act.kind !== 'say' && act.kind !== 'read');
12
- }
13
- export function peerPublicActs(delta, name) {
14
- return delta.filter((act) => act.actor !== undefined && !sameName(act.actor, name) && (act.kind === 'say' || act.kind === 'done'));
15
- }
16
9
  export function directedPeerSays(squareState, delta, name, delivery = deriveDeliveryModel(squareState)) {
17
10
  return delta.filter((act) => delivery.directedTo(act, name));
18
11
  }
19
- function matchesParticipants(act, participants) {
20
- return participants === undefined || (act.actor !== undefined && participants.some((participant) => sameName(participant, act.actor)));
21
- }
22
- export function matchesFeedFilter(act, filter, recipients) {
23
- if (act.kind === 'say') {
24
- return matchesCatchFilter({ actor: act.actor, body: act.body, mentions: act.mentions, reach: act.reach, ...(recipients === undefined ? {} : { recipients }) }, filter);
25
- }
26
- return filter.mention === undefined && matchesParticipants(act, filter.participants);
27
- }
28
- export function filteredPeerActivities(delta, name, filter) {
29
- return peerPublicActs(delta, name)
30
- .filter((act) => act.kind === 'say')
31
- .filter((act) => matchesFeedFilter(act, filter));
32
- }
33
- export function filteredRoomChanges(delta, name, filter) {
34
- if (filter.mention !== undefined)
35
- return [];
36
- return peerRoomChanges(delta, name).filter((act) => matchesParticipants(act, filter.participants));
37
- }
package/dist/activity.js CHANGED
@@ -14,20 +14,13 @@ import { formatDuration } from './time.js';
14
14
  import { style } from './tty-style.js';
15
15
  import { hostLedgerForEnv } from './registry.js';
16
16
  import { createDefaultWakeTransport } from './notifications.js';
17
- function draftDirFor(squarePath) {
18
- return path.join(path.dirname(squarePath), 'drafts');
19
- }
20
- function draftTimestamp(at) {
21
- return new Date(at).toISOString().replace(/\.\d{3}Z$/, 'Z').replace(/[:.]/g, '-');
22
- }
23
- function draftNamePart(name) {
24
- return name.replace(/[^\p{L}\p{N}_-]+/gu, '-').replace(/^-+|-+$/g, '') || 'participant';
25
- }
26
17
  export function saveActivityDraft(squarePath, name, body) {
27
- const draftDir = draftDirFor(squarePath);
18
+ const draftDir = path.join(path.dirname(squarePath), 'drafts');
28
19
  fs.mkdirSync(draftDir, { recursive: true });
29
20
  const hash = crypto.createHash('sha256').update(body).digest('hex').slice(0, 8);
30
- const base = `${draftTimestamp(nowMs())}-${draftNamePart(name)}-${hash}`;
21
+ const stamp = new Date(nowMs()).toISOString().replace(/\.\d{3}Z$/, 'Z').replace(/[:.]/g, '-');
22
+ const namePart = name.replace(/[^\p{L}\p{N}_-]+/gu, '-').replace(/^-+|-+$/g, '') || 'participant';
23
+ const base = `${stamp}-${namePart}-${hash}`;
31
24
  for (let i = 0;; i++) {
32
25
  const filename = `${base}${i === 0 ? '' : `-${i}`}.md`;
33
26
  const target = path.join(draftDir, filename);
@@ -51,6 +44,20 @@ export function activityRetryCommand(squarePath, name, opts, force = opts.force
51
44
  const reply = opts.reply === undefined ? '' : ` --reply ${replyIndex === undefined ? quoteShell(opts.reply) : formatActivityId(replyIndex)}`;
52
45
  return `${participantCommandPrefix(squarePath, name)} express${force ? ' --force' : ''}${opts.noWait ? ' --no-wait' : ''}${reach}${reply} -`;
53
46
  }
47
+ /**
48
+ * A recipient conflict is the speaker's to edit: the printed recovery drops every
49
+ * mention flag (or keeps the bell) so the retry never repeats the rejected target.
50
+ */
51
+ function mentionRepairCommand(squarePath, name, opts, bell) {
52
+ const replyIndex = opts.reply === undefined ? undefined : parseActivityId(opts.reply);
53
+ const reply = opts.reply === undefined ? '' : ` --reply ${replyIndex === undefined ? quoteShell(opts.reply) : formatActivityId(replyIndex)}`;
54
+ const reach = bell ? ' --bell' : ' --no-mention';
55
+ return `${participantCommandPrefix(squarePath, name)} express${opts.force ? ' --force' : ''}${opts.noWait ? ' --no-wait' : ''}${reach}${reply} -`;
56
+ }
57
+ /** A body that is nothing but *gestures* reads as an action, not as speech. */
58
+ function isGestureOnly(body) {
59
+ return /\*[^*]*\*/.test(body) && body.replace(/\*[^*]*\*/g, '').trim() === '';
60
+ }
54
61
  export async function cmdActivity(squarePath, name, activity, resolveBody, opts) {
55
62
  const rawInput = String(resolveBody(activity));
56
63
  const env = { ...(opts.env ?? process.env) };
@@ -99,7 +106,6 @@ export async function cmdActivity(squarePath, name, activity, resolveBody, opts)
99
106
  const reentered = entry.kind === 'joined' && entry.activity !== null;
100
107
  const before = await presentation();
101
108
  const pendingPublic = before.pendingPublic;
102
- const pendingRoomChanges = before.pendingRoomChanges;
103
109
  expressAttempted = true;
104
110
  const landed = await application.express(rawInput.replace(/\r\n/g, '\n').trim(), {
105
111
  force: opts.force ?? false,
@@ -128,18 +134,28 @@ export async function cmdActivity(squarePath, name, activity, resolveBody, opts)
128
134
  const headerCount = fresh.participantCount;
129
135
  const held = fresh.held;
130
136
  const ownActCount = fresh.ownActivityCount;
131
- const hasPending = pendingPublic.length > 0 || pendingRoomChanges.length > 0;
132
- const pending = hasPending ? `\n\n${renderPendingFeed([...fresh.activities], [...pendingPublic], [...pendingRoomChanges], knownName, fresh.state)}` : '';
137
+ const hasPending = pendingPublic.length > 0;
138
+ const pendingSpeaker = pendingPublic[0]?.actor;
139
+ const pendingLead = pendingSpeaker === undefined
140
+ ? ''
141
+ : `▲ while you spoke, ${participantIdentity(pendingSpeaker)} said something behind you\n\n`;
142
+ const pending = hasPending ? `\n\n${pendingLead}${renderPendingFeed([...fresh.activities], [...pendingPublic], knownName, fresh.state)}` : '';
133
143
  const hint = expressHintLine(ownActCount);
134
- const reachEcho = landed.activity.reach === 'bell'
135
- ? ' · to everyone (bell)'
136
- : landed.activity.mentions.length > 0
137
- ? ` · to ${landed.activity.mentions.map((target) => participantIdentity(target)).join(', ')}`
138
- : '';
139
- const confirmation = `● your activity lands${style('dim', ` — #${ownActCount} · ${landed.activity.id}${reachEcho}`)}`;
144
+ const receiptId = landed.activity.id;
145
+ const capCount = fresh.hardCap === null ? '' : ` · ${ownActCount}/${fresh.hardCap}`;
146
+ const confirmation = landed.activity.reach === 'bell'
147
+ ? `● you ring the bell — everyone turns · ${receiptId}${style('dim', capCount)}`
148
+ : `● ${landed.activity.mentions.length > 0
149
+ ? `you walk over to ${landed.activity.mentions.map((target) => participantIdentity(target)).join(', ')}`
150
+ : isGestureOnly(landed.activity.body ?? '')
151
+ ? 'your gesture lands'
152
+ : 'your words land'} — ${receiptId}${style('dim', capCount)}`;
140
153
  const withHint = hint ? `${confirmation}\n${style('dim', hint)}` : confirmation;
154
+ const replyNotice = landed.replyAuthorNotHere === undefined
155
+ ? ''
156
+ : `\n○ ${participantIdentity(landed.replyAuthorNotHere)} isn't in the square right now — the reply landed without reaching them`;
141
157
  const reentry = reentered ? '● you stepped back into the square\n' : '';
142
- process.stdout.write(withPathOutput(squarePath, reentry + withHint + pending, { participantCount: headerCount, held }));
158
+ process.stdout.write(withPathOutput(squarePath, reentry + withHint + replyNotice + pending, { participantCount: headerCount, held }));
143
159
  }
144
160
  catch (error) {
145
161
  // Save before any recovery read: even a broken artifact must not eat the body.
@@ -159,7 +175,6 @@ export async function cmdActivity(squarePath, name, activity, resolveBody, opts)
159
175
  retryCommand,
160
176
  forceCommand,
161
177
  activitySummaries: unreadActivitySummaries(fresh.state, knownName, nowMs()),
162
- unreadRoomChanges: [],
163
178
  }));
164
179
  process.exit(1);
165
180
  return;
@@ -178,7 +193,8 @@ export async function cmdActivity(squarePath, name, activity, resolveBody, opts)
178
193
  ...output,
179
194
  reason: error.code,
180
195
  delayMs: error.facts?.retryAfterMs ?? SLEEP_MS,
181
- holdReason: fresh.holdReason,
196
+ ...(error.facts?.holder === undefined ? {} : { holder: error.facts.holder }),
197
+ ...(error.facts?.holdReason === undefined ? {} : { holdReason: error.facts.holdReason }),
182
198
  retryCommand,
183
199
  }));
184
200
  process.exit(1);
@@ -197,19 +213,35 @@ export async function cmdActivity(squarePath, name, activity, resolveBody, opts)
197
213
  }
198
214
  }
199
215
  const lines = [error instanceof Error ? error.message : String(error), `· draft kept: ${draftPath}`];
216
+ const reason = isSquareError(error) ? error.facts?.reason : undefined;
217
+ // `not_standing` is a flag mention; the body-scan and bell conflicts carry an @name in the draft.
218
+ const mentionRepair = reason === 'bell_mention_conflict' ? { bell: true, fromBody: true }
219
+ : reason === 'unmatched_mention' ? { bell: false, fromBody: true }
220
+ : reason === 'not_standing' ? { bell: false, fromBody: false }
221
+ : undefined;
200
222
  if (isSquareError(error)) {
201
223
  if (callerMissing || error.code === 'unknown_participant' || error.code === 'not_joined')
202
224
  lines.push(joinRecoveryCommand(squarePath, name));
203
225
  if (error.code === 'already_joined')
204
226
  lines.push(...takeoverRecoveryLines(squarePath, name));
205
- if (error.code === 'invalid_args' || error.code === 'invalid_name') {
227
+ if (mentionRepair !== undefined) {
228
+ // The draft stays, but the retry no longer carries the target that was refused.
229
+ lines.push(participantsRecoveryCommand(squarePath));
230
+ lines.push(mentionRepair.fromBody
231
+ ? " · edit the draft first — the body's @name must be removed or wrapped in backticks:"
232
+ : ' · pick someone standing here, or land it bare:');
233
+ lines.push(`${mentionRepairCommand(squarePath, name, opts, mentionRepair.bell)} < ${quoteShell(draftPath)}`);
234
+ }
235
+ else if (error.code === 'invalid_args' || error.code === 'invalid_name') {
206
236
  if (opts.mentions?.length)
207
237
  lines.push(participantsRecoveryCommand(squarePath));
208
238
  if (opts.reply !== undefined)
209
239
  lines.push(`${commandPrefix(squarePath)} history --limit 10`);
210
240
  }
211
241
  }
212
- lines.push(' · correct the problem above before retrying; keep your intended recipients and reply:', `${retryCommand} < ${quoteShell(draftPath)}`);
242
+ if (mentionRepair === undefined) {
243
+ lines.push(' · correct the problem above before retrying; keep your intended recipients and reply:', `${retryCommand} < ${quoteShell(draftPath)}`);
244
+ }
213
245
  process.stderr.write(formatRefusal(squarePath, lines, output));
214
246
  process.exit(2);
215
247
  }
package/dist/artifact.js CHANGED
@@ -31,14 +31,11 @@ function isNotFound(error) {
31
31
  function isNotADatabase(error) {
32
32
  return error instanceof Error && /not a database|file is not a database|SQLITE_NOTADB/i.test(error.message);
33
33
  }
34
- function existingDatabaseUri(squarePath) {
34
+ function openExistingDatabase(squarePath) {
35
35
  const uri = pathToFileURL(path.resolve(squarePath));
36
36
  uri.searchParams.set('mode', 'rw');
37
- return uri.href;
38
- }
39
- function openExistingDatabase(squarePath) {
40
37
  try {
41
- return new DatabaseSync(existingDatabaseUri(squarePath));
38
+ return new DatabaseSync(uri.href);
42
39
  }
43
40
  catch (error) {
44
41
  if (isNotFound(error))
@@ -116,9 +113,6 @@ function validateDatabase(database) {
116
113
  throw error;
117
114
  }
118
115
  }
119
- function configureDatabase(database) {
120
- database.exec('PRAGMA busy_timeout = 0; PRAGMA journal_mode = DELETE; PRAGMA synchronous = FULL;');
121
- }
122
116
  /** Opens an existing artifact without permitting SQLite to create a missing path. */
123
117
  function readExistingArtifact(squarePath, read) {
124
118
  requireSquareExtension(squarePath);
@@ -143,13 +137,6 @@ function readExistingArtifact(squarePath, read) {
143
137
  function readSquareSnapshotOnce(squarePath) {
144
138
  return readExistingArtifact(squarePath, validateDatabase);
145
139
  }
146
- function readSquareRevisionOnce(squarePath) {
147
- return readExistingArtifact(squarePath, (database) => {
148
- validateDatabaseHeader(database);
149
- const rows = database.prepare(`SELECT id, revision FROM ${SNAPSHOT_TABLE}`).all();
150
- return validateSnapshotRevision(rows);
151
- });
152
- }
153
140
  async function createTemporaryArtifact(squarePath) {
154
141
  const directory = path.dirname(squarePath);
155
142
  await fs.promises.mkdir(directory, { recursive: true });
@@ -177,12 +164,6 @@ async function syncTemporaryArtifact(temporary) {
177
164
  await handle.close();
178
165
  }
179
166
  }
180
- async function removeTemporaryArtifact(temporary) {
181
- try {
182
- await fs.promises.unlink(temporary);
183
- }
184
- catch { /* cleanup cannot invalidate a published artifact */ }
185
- }
186
167
  function initializeDatabase(squarePath, state) {
187
168
  const encoded = JSON.stringify(state);
188
169
  let database;
@@ -222,7 +203,10 @@ export async function createSquareFile(squarePath, state) {
222
203
  }
223
204
  }
224
205
  finally {
225
- await removeTemporaryArtifact(temporary);
206
+ try {
207
+ await fs.promises.unlink(temporary);
208
+ }
209
+ catch { /* cleanup cannot invalidate a published artifact */ }
226
210
  }
227
211
  }
228
212
  function isBusy(error) {
@@ -259,7 +243,11 @@ export function readSquareSnapshot(squarePath, signal) {
259
243
  }
260
244
  /** Check the artifact revision without loading or parsing the historical state. */
261
245
  export function readSquareRevision(squarePath, signal) {
262
- return readWithBusyRetry(() => readSquareRevisionOnce(squarePath), signal);
246
+ return readWithBusyRetry(() => readExistingArtifact(squarePath, (database) => {
247
+ validateDatabaseHeader(database);
248
+ const rows = database.prepare(`SELECT id, revision FROM ${SNAPSHOT_TABLE}`).all();
249
+ return validateSnapshotRevision(rows);
250
+ }), signal);
263
251
  }
264
252
  /** One reference-counted revision detector per canonical artifact in this process. */
265
253
  export function observeSquareRevision(squarePath) {
@@ -305,7 +293,7 @@ export async function transactSquareSnapshot(squarePath, fn, signal, forceCommit
305
293
  if (signal?.aborted)
306
294
  throw signal.reason ?? closedError();
307
295
  try {
308
- configureDatabase(database);
296
+ database.exec('PRAGMA busy_timeout = 0; PRAGMA journal_mode = DELETE; PRAGMA synchronous = FULL;');
309
297
  database.exec('BEGIN IMMEDIATE;');
310
298
  begun = true;
311
299
  break;
@@ -19,6 +19,7 @@ export function displayAttentionPath(squarePath) {
19
19
  export function renderAttentionDescription({ actor, recipient, route }) {
20
20
  switch (route) {
21
21
  case 'mention': return `${actor} addressed you (${recipient})`;
22
+ case 'reply': return `${actor} replied to you (${recipient})`;
22
23
  case 'attention': return `${actor} spoke · you’re listening to ${actor}`;
23
24
  case 'bell': return `${actor} rang the bell · everyone’s attention`;
24
25
  }
@@ -1,5 +1,7 @@
1
- export type AutomaticProvider = 'codex' | 'claude' | 'opencode' | 'pi';
1
+ import { type AutomaticProvider } from './participant-identity.js';
2
+ export type { AutomaticProvider } from './participant-identity.js';
2
3
  export { automaticParticipant } from './participant-identity.js';
4
+ export declare function operationEnv(provider: AutomaticProvider, sessionId: string, env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
3
5
  export declare function publicSquarePath(cwd: string): string;
4
6
  export declare function automaticSessionStart(provider: AutomaticProvider, sessionId: string, cwd: string, env?: NodeJS.ProcessEnv): Promise<string | undefined>;
5
7
  export declare function automaticSessionEnd(provider: AutomaticProvider, sessionId: string, cwd: string, env?: NodeJS.ProcessEnv): Promise<void>;
@@ -1,39 +1,26 @@
1
1
  import { promises as fs } from 'node:fs';
2
- import { hostLedgerRoot } from './host-ledger-root.js';
3
2
  import path from 'node:path';
4
3
  import { openSquare } from './square-file-adapter.js';
5
4
  import { closeOpenSquare } from './open-square.js';
6
5
  import { Square } from './square-wiring.js';
7
6
  import { entryPresentation } from './views.js';
8
- import { automaticParticipant } from './participant-identity.js';
7
+ import { automaticParticipant, harnessSessionSources } from './participant-identity.js';
9
8
  import { SquareError } from './model.js';
10
- import { createHostLedgerPort } from './host-ledger-file-adapter.js';
11
9
  import { projectSessionBindings } from './square-projections.js';
12
- import { claimSessionParticipant, readParticipantOwner, releaseSessionParticipant } from './registry.js';
10
+ import { claimSessionParticipant, hostLedgerForEnv, readParticipantOwner, releaseSessionParticipant } from './registry.js';
13
11
  import { publishWakeRoute, resolvePrimaryWakeRoute, defaultWakeRouteCapabilities } from './routes.js';
14
12
  export { automaticParticipant } from './participant-identity.js';
15
- const providerEnv = {
16
- codex: 'CODEX_THREAD_ID',
17
- claude: 'CLAUDE_CODE_SESSION_ID',
18
- opencode: 'OPENCODE_SESSION_ID',
19
- pi: 'PI_SESSION_ID',
20
- };
21
- function operationEnv(provider, sessionId, env) {
22
- return {
23
- ...env,
24
- CLAUDE_CODE_SESSION_ID: '',
25
- CLAUDE_CODE_CHILD_SESSION: '',
26
- CODEX_THREAD_ID: '',
27
- OPENCODE_SESSION_ID: '',
28
- PI_SESSION_ID: '',
29
- [providerEnv[provider]]: sessionId,
30
- };
31
- }
32
- function hostLedgerForEnv(env) {
33
- const root = hostLedgerRoot(env);
34
- return createHostLedgerPort({
35
- rootPath: root,
36
- });
13
+ export function operationEnv(provider, sessionId, env) {
14
+ const scoped = { ...env };
15
+ // Clear every native coordinate (and the Claude child flag); PASEO_AGENT_ID stays as it is.
16
+ for (const source of harnessSessionSources) {
17
+ if (source.provider === undefined)
18
+ continue;
19
+ scoped[source.variable] = source.provider === provider ? sessionId : '';
20
+ if (source.childVariable !== undefined)
21
+ scoped[source.childVariable] = '';
22
+ }
23
+ return scoped;
37
24
  }
38
25
  export function publicSquarePath(cwd) {
39
26
  return path.join(cwd, '.square', 'PUBLIC.square');
@@ -77,18 +64,18 @@ export async function automaticSessionStart(provider, sessionId, cwd, env = proc
77
64
  throw new SquareError('already_joined', `✕ ${name} already stands here — another session holds the name`);
78
65
  }
79
66
  const scopedEnv = operationEnv(provider, sessionId, env);
80
- const claim = await claimSessionParticipant(squarePath, name, scopedEnv);
81
- const square = await Square.at({ path: squarePath, hostLedger: hostLedgerForEnv(scopedEnv), env: scopedEnv });
67
+ const claim = await claimSessionParticipant(squarePath, name, hostLedger, scopedEnv);
68
+ const square = await Square.at({ path: squarePath, hostLedger, env: scopedEnv });
82
69
  try {
83
70
  const implicit = await square.implicitJoin(name);
84
71
  if (implicit.state === 'done') {
85
72
  if (claim?.status === 'acquired')
86
- await releaseSessionParticipant(squarePath, name, scopedEnv);
73
+ await releaseSessionParticipant(squarePath, name, hostLedger, scopedEnv);
87
74
  return undefined;
88
75
  }
89
- const route = resolvePrimaryWakeRoute({ location: squarePath, participant: name, sessionId, provider }, env, await defaultWakeRouteCapabilities(hostLedgerForEnv(env)));
76
+ const route = resolvePrimaryWakeRoute({ location: squarePath, participant: name, sessionId, provider }, env, await defaultWakeRouteCapabilities(hostLedger));
90
77
  if (route !== undefined) {
91
- const publisher = await openSquare(squarePath, { hostLedger: hostLedgerForEnv(scopedEnv), env: scopedEnv });
78
+ const publisher = await openSquare(squarePath, { hostLedger, env: scopedEnv });
92
79
  try {
93
80
  await publishWakeRoute(publisher.artifact, { ...route, ...(claim?.epoch === undefined || claim.epoch <= 0 ? {} : { epoch: claim.epoch }) }, { at: Date.now(), requireCurrentSession: true });
94
81
  }
@@ -96,7 +83,7 @@ export async function automaticSessionStart(provider, sessionId, cwd, env = proc
96
83
  await closeOpenSquare(publisher);
97
84
  }
98
85
  }
99
- await hostLedgerForEnv(env).ensurePresence({
86
+ await hostLedger.ensurePresence({
100
87
  location: squarePath,
101
88
  participant: name,
102
89
  session: sessionId,
@@ -108,7 +95,7 @@ export async function automaticSessionStart(provider, sessionId, cwd, env = proc
108
95
  }
109
96
  catch (error) {
110
97
  if (claim?.status === 'acquired')
111
- await releaseSessionParticipant(squarePath, name, scopedEnv).catch(() => undefined);
98
+ await releaseSessionParticipant(squarePath, name, hostLedger, scopedEnv).catch(() => undefined);
112
99
  throw error;
113
100
  }
114
101
  finally {
@@ -129,7 +116,7 @@ export async function automaticSessionEnd(provider, sessionId, cwd, env = proces
129
116
  }))[0];
130
117
  const expectedEpoch = binding === undefined
131
118
  ? undefined
132
- : (await readParticipantOwner(squarePath, binding.participant, scopedEnv))?.epoch;
119
+ : (await readParticipantOwner(squarePath, binding.participant, hostLedger))?.epoch;
133
120
  await closeOpenSquare(probe);
134
121
  const square = await Square.at({ path: squarePath, hostLedger, env: scopedEnv });
135
122
  try {
@@ -5,11 +5,11 @@ import { withFileLock } from './file-lock.js';
5
5
  import { openSquare } from './square-file-adapter.js';
6
6
  import { closeOpenSquare } from './open-square.js';
7
7
  import { presentPending } from './presentation-operations.js';
8
- import { hostLedgerForEnv, sessionInbox } from './inbox.js';
8
+ import { hostLedgerForEnv } from './registry.js';
9
+ import { sessionInbox } from './inbox.js';
9
10
  import { attentionBodyIsClipped, renderAttentionPreview } from './attention-presentation.js';
10
- import { pendingAtBoundary, presentationSuppressesWake, projectPresentationEvidence } from './square-projections.js';
11
+ import { projectBoundaryEligibility } from './square-projections.js';
11
12
  export { pendingAtBoundary } from './square-projections.js';
12
- import { formatActivityId } from './square-core.js';
13
13
  const CONTEXT_MAX = 1200;
14
14
  const presentationLocks = new Map();
15
15
  /** One settled promise loses to an abort so an in-process presentation wait stays bounded too. */
@@ -35,9 +35,6 @@ async function awaitWithSignal(promise, signal) {
35
35
  signal.removeEventListener('abort', onAbort);
36
36
  }
37
37
  }
38
- function pendingCount(inbox) {
39
- return inbox.reduce((total, membership) => total + membership.notifications.length, 0);
40
- }
41
38
  export function renderPendingAtBoundary(inbox) {
42
39
  return renderBoundary(inbox).context;
43
40
  }
@@ -84,13 +81,7 @@ async function presentPendingAtBoundaryUnlocked(sessionId, present, lookup = ses
84
81
  if (signal?.aborted)
85
82
  return undefined;
86
83
  const hostLedger = hostLedgerForEnv(env);
87
- const pending = [];
88
- for (const membership of pendingAtBoundary(inbox)) {
89
- const evidence = await projectPresentationEvidence({ hostLedger, location: membership.squarePath, participant: membership.name, sessionId });
90
- const notifications = membership.notifications.filter((notification) => !presentationSuppressesWake(evidence.filter((row) => row.activity === formatActivityId(notification.actIndex))));
91
- if (notifications.length > 0)
92
- pending.push({ ...membership, notifications });
93
- }
84
+ const pending = await projectBoundaryEligibility({ hostLedger, sessionId, inbox });
94
85
  if (pending.length === 0 || signal?.aborted)
95
86
  return undefined;
96
87
  const delivered = renderBoundary(pending);
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The one canonicalizer for square locations.
3
+ *
4
+ * Resolve, then realpath the nearest existing ancestor and rejoin the non-existing
5
+ * suffix: a square that does not exist yet under a symlinked directory still lands
6
+ * on the same identity as the directory it will appear in. Any other read error
7
+ * keeps the plain resolution.
8
+ */
9
+ export declare function canonicalPath(value: string): Promise<string>;
10
+ /** The synchronous face of {@link canonicalPath}, using the native realpath. */
11
+ export declare function canonicalPathSync(value: string): string;