@astrosheep/square 0.3.2

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 (47) hide show
  1. package/codex-plugin/.codex-plugin/plugin.json +25 -0
  2. package/codex-plugin/hooks/hooks.json +28 -0
  3. package/dist/activity-feed.js +36 -0
  4. package/dist/activity.js +151 -0
  5. package/dist/artifact.js +739 -0
  6. package/dist/claude-hook.js +112 -0
  7. package/dist/cmd/notify-once.js +37 -0
  8. package/dist/compact.js +39 -0
  9. package/dist/decisions.js +286 -0
  10. package/dist/delivery-health.js +249 -0
  11. package/dist/delivery.js +93 -0
  12. package/dist/doctor.js +34 -0
  13. package/dist/harness.js +584 -0
  14. package/dist/help.js +131 -0
  15. package/dist/inbox.js +33 -0
  16. package/dist/index.js +163 -0
  17. package/dist/list.js +126 -0
  18. package/dist/model.js +44 -0
  19. package/dist/notifications.js +97 -0
  20. package/dist/paseo-timeline.js +206 -0
  21. package/dist/presentation.js +468 -0
  22. package/dist/presented.js +211 -0
  23. package/dist/registry.js +299 -0
  24. package/dist/runtime.js +304 -0
  25. package/dist/search.js +54 -0
  26. package/dist/square-core.js +183 -0
  27. package/dist/square.js +1366 -0
  28. package/dist/stream.js +149 -0
  29. package/dist/terminal.js +125 -0
  30. package/dist/time.js +81 -0
  31. package/dist/wake-sink.js +219 -0
  32. package/dist/watch.js +386 -0
  33. package/extensions/square-opencode.js +87 -0
  34. package/extensions/square-pi.js +167 -0
  35. package/guides/architect.md +165 -0
  36. package/guides/brainstorm.md +404 -0
  37. package/guides/participant.md +171 -0
  38. package/package.json +57 -0
  39. package/skills/brainstorm/SKILL.md +136 -0
  40. package/skills/square/.claude-plugin/plugin.json +8 -0
  41. package/skills/square/SKILL.md +154 -0
  42. package/skills/square/hooks/hooks.json +27 -0
  43. package/skills/square-feedback/SKILL.md +55 -0
  44. package/skills/square-feedback/agents/openai.yaml +4 -0
  45. package/template.md +4 -0
  46. package/templates/architect.md +4 -0
  47. package/templates/brainstorm.md +4 -0
package/dist/watch.js ADDED
@@ -0,0 +1,386 @@
1
+ import { setTimeout as sleep } from 'node:timers/promises';
2
+ import { loadSquare } from './artifact.js';
3
+ import { SquareError, nameKey, } from './model.js';
4
+ import { deriveDeliveryModel } from './delivery.js';
5
+ import { SLEEP_MS, STALE_MS, WATCH_HEARTBEAT_MS, WATCH_STALE_MS, countSays, currentHold, doneNames, freshWatchLease, hasQuorum, inSquareCount, markDeliveredMentions, nowMs, readCursor, touchPresenceCursor, withSquareLock, writeSquareDoc, } from './runtime.js';
6
+ import { renderWatchForceTakeover, renderWatchAlreadyActive, renderWatchInterrupted, renderWatchOutput, renderWatchReplaced, renderWatchStatus, participantCommandPrefix, withPathOutput, withWatchNextOutput, } from './presentation.js';
7
+ import { ackPeerDelta, filteredPeerActivities, filteredRoomChanges, indexedDelta, matchesFeedFilter, peerPublicActs, peerRoomChanges, } from './activity-feed.js';
8
+ import { coreParticipants, resolveKnownName } from './decisions.js';
9
+ import { hasAutomaticDeliveryIdentity } from './registry.js';
10
+ /** Notification receipts are stronger than the public-feed cursor, which self activity may advance. */
11
+ function catchDelta(doc, name) {
12
+ const items = indexedDelta(doc.acts, readCursor(doc, name));
13
+ const seen = new Set(items.map((item) => item.index));
14
+ for (const notification of deriveDeliveryModel(doc).pendingFor(name)) {
15
+ if (!seen.has(notification.item.index))
16
+ items.push(notification.item);
17
+ }
18
+ return items.sort((a, b) => a.index - b.index);
19
+ }
20
+ function watchStatusExitCode(status) {
21
+ return status === 'capped' ? 1 : 0;
22
+ }
23
+ function leaseId() {
24
+ return `watch_${process.pid}_${Date.now()}_${Math.random().toString(36).slice(2)}`;
25
+ }
26
+ function leaseFilter(opts) {
27
+ const filter = {
28
+ ...(opts.participants !== undefined ? { participants: opts.participants } : {}),
29
+ ...(opts.mention !== undefined ? { mention: opts.mention } : {}),
30
+ };
31
+ return Object.keys(filter).length === 0 ? undefined : filter;
32
+ }
33
+ function setLease(doc, name, id, at, opts) {
34
+ const filter = leaseFilter(opts);
35
+ doc.runtime.leases[name] = {
36
+ leaseId: id,
37
+ heartbeatAt: at,
38
+ expiresAt: at + WATCH_STALE_MS,
39
+ ...(filter ? { filter } : {}),
40
+ };
41
+ }
42
+ function sameLease(doc, name, id, at = nowMs()) {
43
+ return freshWatchLease(doc, name, at)?.leaseId === id;
44
+ }
45
+ function consumeDelta(doc, name, delta, delivered, at) {
46
+ const consumed = ackPeerDelta(doc, name, delta);
47
+ const receipts = markDeliveredMentions(doc, name, delivered, at);
48
+ return consumed || receipts;
49
+ }
50
+ function watchOutputResult(squarePath, doc, name, delta, opts = {}) {
51
+ const publicItems = peerPublicActs(delta, name).filter((item) => matchesFeedFilter(item, opts));
52
+ const roomChanges = filteredRoomChanges(delta, name, opts);
53
+ consumeDelta(doc, name, delta, publicItems, nowMs());
54
+ writeSquareDoc(squarePath, doc);
55
+ return {
56
+ type: 'output',
57
+ stdout: renderWatchOutput(doc.acts, publicItems, roomChanges, {
58
+ ...opts,
59
+ squarePath,
60
+ viewer: name,
61
+ showCatchHint: !hasAutomaticDeliveryIdentity(),
62
+ }),
63
+ ...(opts.status ? { status: opts.status } : {}),
64
+ };
65
+ }
66
+ function loadPresence(squarePath) {
67
+ try {
68
+ const doc = loadSquare(squarePath);
69
+ const now = nowMs();
70
+ return { participants: coreParticipants(doc, now).participants, now };
71
+ }
72
+ catch {
73
+ return undefined;
74
+ }
75
+ }
76
+ function loadHeaderCount(squarePath) {
77
+ try {
78
+ return inSquareCount(loadSquare(squarePath));
79
+ }
80
+ catch {
81
+ return undefined;
82
+ }
83
+ }
84
+ function writeWatchOutput(squarePath, name, stdout, status, idleMs) {
85
+ const presence = loadPresence(squarePath);
86
+ const headerOpts = { participantCount: loadHeaderCount(squarePath) };
87
+ const showCatchHint = !hasAutomaticDeliveryIdentity();
88
+ if (status) {
89
+ process.stdout.write(withPathOutput(squarePath, [renderWatchStatus({ status, squarePath, name, idleMs, presence, showCatchHint }), stdout.trimEnd()].filter(Boolean).join('\n\n').trimEnd(), headerOpts));
90
+ return;
91
+ }
92
+ const fallback = showCatchHint
93
+ ? `» ${participantCommandPrefix(squarePath, name)} catch --idle 30m\n stay available for new activity`
94
+ : '';
95
+ process.stdout.write(withWatchNextOutput(squarePath, [stdout.trimEnd(), fallback].filter(Boolean).join('\n\n'), headerOpts));
96
+ }
97
+ async function beginWatch(squarePath, name, opts) {
98
+ return withSquareLock(squarePath, () => {
99
+ const doc = loadSquare(squarePath);
100
+ const at = nowMs();
101
+ const active = freshWatchLease(doc, name, at);
102
+ if (active !== undefined && !(opts.force ?? false))
103
+ return { type: 'active', lease: active };
104
+ const id = leaseId();
105
+ setLease(doc, name, id, at, opts);
106
+ touchPresenceCursor(doc, name, at, 'watch');
107
+ writeSquareDoc(squarePath, doc);
108
+ return { type: 'started', leaseId: id, replaced: active !== undefined, heartbeatAt: at };
109
+ });
110
+ }
111
+ async function endWatch(squarePath, name, id) {
112
+ if (id === undefined)
113
+ return;
114
+ await withSquareLock(squarePath, () => {
115
+ const doc = loadSquare(squarePath);
116
+ if (doc.runtime.leases[name]?.leaseId !== id)
117
+ return;
118
+ delete doc.runtime.leases[name];
119
+ writeSquareDoc(squarePath, doc);
120
+ });
121
+ }
122
+ function installWatchInterruptHandler(squarePath, name, currentLeaseId) {
123
+ const onInterrupt = () => {
124
+ void (async () => {
125
+ await endWatch(squarePath, name, currentLeaseId());
126
+ process.stdout.write(withPathOutput(squarePath, renderWatchInterrupted({ squarePath, name })));
127
+ process.exit(130);
128
+ })().catch((error) => {
129
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
130
+ process.exit(130);
131
+ });
132
+ };
133
+ process.once('SIGINT', onInterrupt);
134
+ return () => {
135
+ process.off('SIGINT', onInterrupt);
136
+ };
137
+ }
138
+ function terminalStatus(doc, name) {
139
+ const count = countSays(doc.acts, name);
140
+ if (doc.hardCap !== null && count >= doc.hardCap)
141
+ return 'capped';
142
+ const done = doneNames(doc.acts);
143
+ done.delete(nameKey(name));
144
+ if (hasQuorum(doc, name, done))
145
+ return 'quorum';
146
+ return undefined;
147
+ }
148
+ async function cmdWatchNow(squarePath, name, opts) {
149
+ const result = await withSquareLock(squarePath, () => {
150
+ const doc = loadSquare(squarePath);
151
+ const at = nowMs();
152
+ const touched = touchPresenceCursor(doc, name, at, 'watch');
153
+ const delta = catchDelta(doc, name);
154
+ const peerPublic = peerPublicActs(delta, name);
155
+ const roomChanges = peerRoomChanges(delta, name);
156
+ const filteredActivities = filteredPeerActivities(delta, name, opts);
157
+ const matchingRoomChanges = filteredRoomChanges(delta, name, opts);
158
+ const hasDeliverable = peerPublic.length > 0 || roomChanges.length > 0;
159
+ const hasFilteredDeliverable = filteredActivities.length > 0 || matchingRoomChanges.length > 0;
160
+ const status = terminalStatus(doc, name);
161
+ if (hasDeliverable && hasFilteredDeliverable) {
162
+ return watchOutputResult(squarePath, doc, name, delta, {
163
+ participants: opts.participants,
164
+ mention: opts.mention,
165
+ ...(status ? { status } : {}),
166
+ });
167
+ }
168
+ if (hasDeliverable) {
169
+ const acked = ackPeerDelta(doc, name, delta);
170
+ if (acked || touched)
171
+ writeSquareDoc(squarePath, doc);
172
+ }
173
+ else if (touched) {
174
+ writeSquareDoc(squarePath, doc);
175
+ }
176
+ if (status)
177
+ return { type: 'terminal', status };
178
+ return { type: 'terminal', status: 'empty-now' };
179
+ });
180
+ if (result.type === 'output') {
181
+ writeWatchOutput(squarePath, name, result.stdout, result.status);
182
+ process.exitCode = watchStatusExitCode(result.status);
183
+ return;
184
+ }
185
+ if (result.type === 'terminal') {
186
+ const presence = loadPresence(squarePath);
187
+ process.stdout.write(withPathOutput(squarePath, renderWatchStatus({
188
+ status: result.status,
189
+ squarePath,
190
+ name,
191
+ presence,
192
+ showCatchHint: !hasAutomaticDeliveryIdentity(),
193
+ }), {
194
+ participantCount: loadHeaderCount(squarePath),
195
+ }));
196
+ process.exitCode = watchStatusExitCode(result.status);
197
+ return;
198
+ }
199
+ }
200
+ export async function cmdWatch(squarePath, name, opts) {
201
+ let initialDoc;
202
+ try {
203
+ initialDoc = loadSquare(squarePath);
204
+ name = resolveKnownName(initialDoc, name);
205
+ if (opts.mention !== undefined)
206
+ opts = { ...opts, mention: resolveKnownName(initialDoc, opts.mention) };
207
+ if (opts.participants !== undefined && opts.participants.length > 0) {
208
+ opts = { ...opts, participants: opts.participants.map((p) => resolveKnownName(initialDoc, p)) };
209
+ }
210
+ }
211
+ catch (err) {
212
+ if (err instanceof SquareError) {
213
+ process.stderr.write(err.message + '\n');
214
+ process.exit(err.code === 'not_found' ? 1 : 2);
215
+ }
216
+ throw err;
217
+ }
218
+ if (opts.now) {
219
+ if (opts.activityCount !== 1)
220
+ process.stderr.write('--count is ignored with --now\n');
221
+ await cmdWatchNow(squarePath, name, opts);
222
+ return;
223
+ }
224
+ const start = await beginWatch(squarePath, name, opts);
225
+ if (start.type === 'active') {
226
+ const presence = loadPresence(squarePath);
227
+ process.stdout.write(withPathOutput(squarePath, renderWatchAlreadyActive({ squarePath, name }), { participantCount: loadHeaderCount(squarePath) }));
228
+ process.exit(1);
229
+ }
230
+ let staleSince = nowMs();
231
+ let currentLeaseId = start.leaseId;
232
+ let nextHeartbeatAt = start.heartbeatAt + WATCH_HEARTBEAT_MS;
233
+ if (start.replaced) {
234
+ const presence = loadPresence(squarePath);
235
+ process.stdout.write(withPathOutput(squarePath, renderWatchForceTakeover({ squarePath, name }), { participantCount: loadHeaderCount(squarePath) }));
236
+ }
237
+ const idleMs = opts.idleMs ?? STALE_MS;
238
+ const removeInterruptHandler = installWatchInterruptHandler(squarePath, name, () => currentLeaseId);
239
+ try {
240
+ while (true) {
241
+ const result = await withSquareLock(squarePath, () => {
242
+ const doc = loadSquare(squarePath);
243
+ const at = nowMs();
244
+ if (!sameLease(doc, name, currentLeaseId, at))
245
+ return { type: 'replaced' };
246
+ let mutated = false;
247
+ if (at >= nextHeartbeatAt) {
248
+ setLease(doc, name, currentLeaseId, at, opts);
249
+ mutated = touchPresenceCursor(doc, name, at, 'watch') || mutated;
250
+ nextHeartbeatAt = at + WATCH_HEARTBEAT_MS;
251
+ mutated = true;
252
+ }
253
+ const delta = catchDelta(doc, name);
254
+ const peerPublic = peerPublicActs(delta, name);
255
+ const roomChanges = peerRoomChanges(delta, name);
256
+ const filteredActivities = filteredPeerActivities(delta, name, opts);
257
+ const matchingRoomChanges = filteredRoomChanges(delta, name, opts);
258
+ const hasDeliverable = peerPublic.length > 0 || roomChanges.length > 0;
259
+ const hasFilteredDeliverable = filteredActivities.length > 0 || matchingRoomChanges.length > 0;
260
+ const status = terminalStatus(doc, name);
261
+ if (currentHold(doc.acts).active) {
262
+ if (mutated)
263
+ writeSquareDoc(squarePath, doc);
264
+ return { type: 'held' };
265
+ }
266
+ if (hasDeliverable &&
267
+ hasFilteredDeliverable &&
268
+ (filteredActivities.length >= opts.activityCount || matchingRoomChanges.length > 0 || status !== undefined)) {
269
+ return watchOutputResult(squarePath, doc, name, delta, {
270
+ participants: opts.participants,
271
+ mention: opts.mention,
272
+ ...(status ? { status } : {}),
273
+ });
274
+ }
275
+ if (hasDeliverable && !hasFilteredDeliverable)
276
+ mutated = ackPeerDelta(doc, name, delta) || mutated;
277
+ if (status) {
278
+ if (mutated)
279
+ writeSquareDoc(squarePath, doc);
280
+ return { type: 'terminal', status };
281
+ }
282
+ if (mutated)
283
+ writeSquareDoc(squarePath, doc);
284
+ return { type: 'sleep' };
285
+ });
286
+ switch (result.type) {
287
+ case 'output': {
288
+ const status = result.status;
289
+ if (opts.follow === true && status === undefined) {
290
+ writeWatchOutput(squarePath, name, result.stdout);
291
+ staleSince = nowMs();
292
+ break;
293
+ }
294
+ await endWatch(squarePath, name, currentLeaseId);
295
+ currentLeaseId = undefined;
296
+ writeWatchOutput(squarePath, name, result.stdout, status);
297
+ process.exitCode = watchStatusExitCode(status);
298
+ return;
299
+ }
300
+ case 'terminal':
301
+ await endWatch(squarePath, name, currentLeaseId);
302
+ currentLeaseId = undefined;
303
+ const presence = loadPresence(squarePath);
304
+ process.stdout.write(withPathOutput(squarePath, renderWatchStatus({
305
+ status: result.status,
306
+ squarePath,
307
+ name,
308
+ presence,
309
+ showCatchHint: !hasAutomaticDeliveryIdentity(),
310
+ }), {
311
+ participantCount: loadHeaderCount(squarePath),
312
+ }));
313
+ process.exitCode = watchStatusExitCode(result.status);
314
+ return;
315
+ case 'replaced':
316
+ currentLeaseId = undefined;
317
+ {
318
+ const presence = loadPresence(squarePath);
319
+ process.stdout.write(withPathOutput(squarePath, renderWatchReplaced({ squarePath, name }), { participantCount: loadHeaderCount(squarePath) }));
320
+ }
321
+ process.exitCode = 0;
322
+ return;
323
+ case 'sleep':
324
+ break;
325
+ case 'held':
326
+ staleSince = nowMs();
327
+ break;
328
+ }
329
+ if (nowMs() - staleSince >= idleMs) {
330
+ const result = await withSquareLock(squarePath, () => {
331
+ const doc = loadSquare(squarePath);
332
+ if (!sameLease(doc, name, currentLeaseId))
333
+ return { type: 'replaced' };
334
+ const delta = catchDelta(doc, name);
335
+ const filteredActivities = filteredPeerActivities(delta, name, opts);
336
+ if (filteredActivities.length > 0) {
337
+ return watchOutputResult(squarePath, doc, name, delta, {
338
+ stalePartial: true,
339
+ participants: opts.participants,
340
+ mention: opts.mention,
341
+ idleMs,
342
+ });
343
+ }
344
+ return { type: 'terminal', status: 'stale' };
345
+ });
346
+ if (result.type === 'replaced') {
347
+ currentLeaseId = undefined;
348
+ {
349
+ const presence = loadPresence(squarePath);
350
+ process.stdout.write(withPathOutput(squarePath, renderWatchReplaced({ squarePath, name }), { participantCount: loadHeaderCount(squarePath) }));
351
+ }
352
+ process.exitCode = 0;
353
+ return;
354
+ }
355
+ if (result.type === 'output') {
356
+ await endWatch(squarePath, name, currentLeaseId);
357
+ currentLeaseId = undefined;
358
+ writeWatchOutput(squarePath, name, result.stdout, result.status);
359
+ process.exitCode = 0;
360
+ return;
361
+ }
362
+ if (result.type === 'terminal') {
363
+ await endWatch(squarePath, name, currentLeaseId);
364
+ currentLeaseId = undefined;
365
+ const presence = loadPresence(squarePath);
366
+ process.stdout.write(withPathOutput(squarePath, renderWatchStatus({
367
+ status: result.status,
368
+ squarePath,
369
+ name,
370
+ idleMs,
371
+ presence,
372
+ showCatchHint: !hasAutomaticDeliveryIdentity(),
373
+ }), {
374
+ participantCount: loadHeaderCount(squarePath),
375
+ }));
376
+ process.exitCode = watchStatusExitCode(result.status);
377
+ return;
378
+ }
379
+ }
380
+ await sleep(SLEEP_MS);
381
+ }
382
+ }
383
+ finally {
384
+ removeInterruptHandler();
385
+ }
386
+ }
@@ -0,0 +1,87 @@
1
+ import {
2
+ deferToActiveCatch,
3
+ opencodeHookResponse,
4
+ renderClaudeInboxContext,
5
+ } from '../dist/claude-hook.js';
6
+ import { sessionInbox } from '../dist/inbox.js';
7
+ import { presentOnce } from '../dist/presented.js';
8
+
9
+ function pendingSignature(sessionId) {
10
+ const keys = sessionInbox(sessionId).flatMap((membership) =>
11
+ membership.notifications.map(
12
+ (notification) =>
13
+ `${membership.squarePath}\u0000${membership.name.toLocaleLowerCase()}\u0000${notification.actIndex}`
14
+ )
15
+ );
16
+ return keys.sort().join('\n');
17
+ }
18
+
19
+ const IDLE_WAKE = [
20
+ '<system-reminder source="square">',
21
+ 'Square activity is waiting. Process the Square context injected into this turn, then run its catch command.',
22
+ '</system-reminder>',
23
+ ].join('\n');
24
+
25
+ export default async function squareOpenCodePlugin({ client }) {
26
+ const handledAtIdle = new Map();
27
+
28
+ return {
29
+ 'shell.env': async (input, output) => {
30
+ if (input.sessionID) output.env.OPENCODE_SESSION_ID = input.sessionID;
31
+ },
32
+
33
+ 'experimental.chat.system.transform': async (input, output) => {
34
+ if (!input.sessionID) return;
35
+ try {
36
+ // Membership comes only from explicit join/act/catch claims, never env inheritance.
37
+ presentOnce(
38
+ input.sessionID,
39
+ (sessionId) => deferToActiveCatch(sessionInbox(sessionId)),
40
+ (inbox) => output.system.push(renderClaudeInboxContext(inbox))
41
+ );
42
+
43
+ const signature = pendingSignature(input.sessionID);
44
+ if (signature === '') handledAtIdle.delete(input.sessionID);
45
+ else handledAtIdle.set(input.sessionID, signature);
46
+ } catch {
47
+ // Adapter failures leave attention unpresented for a later boundary.
48
+ }
49
+ },
50
+
51
+ event: async ({ event }) => {
52
+ if (event.type !== 'session.idle') return;
53
+ const sessionId = event.properties.sessionID;
54
+ try {
55
+ const signature = pendingSignature(sessionId);
56
+ if (signature === '') {
57
+ handledAtIdle.delete(sessionId);
58
+ return;
59
+ }
60
+ if (handledAtIdle.get(sessionId) === signature) return;
61
+
62
+ const response = opencodeHookResponse({
63
+ session_id: sessionId,
64
+ hook_event_name: 'Stop',
65
+ stop_hook_active: false,
66
+ });
67
+ if (response?.decision !== 'block') return;
68
+
69
+ handledAtIdle.set(sessionId, signature);
70
+ try {
71
+ await client.session.promptAsync({
72
+ path: { id: sessionId },
73
+ body: { parts: [{ type: 'text', text: IDLE_WAKE }] },
74
+ });
75
+ } catch {
76
+ handledAtIdle.delete(sessionId);
77
+ }
78
+ } catch {
79
+ // Idle acceleration is best-effort and must not break the host session.
80
+ }
81
+ },
82
+
83
+ dispose: async () => {
84
+ handledAtIdle.clear();
85
+ },
86
+ };
87
+ }
@@ -0,0 +1,167 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ import { sessionInbox } from '../dist/inbox.js';
6
+ import { presentOnce } from '../dist/presented.js';
7
+
8
+ function quoteShell(value) {
9
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
10
+ }
11
+
12
+ export function pendingInbox(inbox) {
13
+ return inbox.filter((membership) => Array.isArray(membership.notifications) && membership.notifications.length > 0);
14
+ }
15
+
16
+ export function inboxKeys(inbox) {
17
+ return pendingInbox(inbox).flatMap((membership) =>
18
+ membership.notifications.map((notification) =>
19
+ `${membership.squarePath}\u0000${membership.name.toLocaleLowerCase()}\u0000${notification.actIndex}`
20
+ )
21
+ );
22
+ }
23
+
24
+ export function notificationMessageId(squarePath, actIndex) {
25
+ return `square:${squarePath}#act_${actIndex}`;
26
+ }
27
+
28
+ const INJECT_BODY_MAX = 2048;
29
+
30
+ function injectBodyPreview(body, squarePath, name, actIndex) {
31
+ const compact = String(body ?? '').replace(/\r\n/g, '\n');
32
+ if (compact.length <= INJECT_BODY_MAX) return compact;
33
+ const pointer = `square --square-path ${quoteShell(squarePath)} --as ${quoteShell(name)} echo --ids act_${actIndex} --full`;
34
+ return `${compact.slice(0, INJECT_BODY_MAX).trimEnd()}\n… [truncated] full echo: ${pointer}`;
35
+ }
36
+
37
+ export function renderPiInbox(inbox) {
38
+ const pending = pendingInbox(inbox);
39
+ const count = pending.reduce((total, membership) => total + membership.notifications.length, 0);
40
+ const noun = count === 1 ? 'notification' : 'notifications';
41
+ return [
42
+ `<system-reminder source="square">You have ${count} unread Square ${noun}.`,
43
+ ...pending.flatMap((membership) => {
44
+ const command = `square --square-path ${quoteShell(membership.squarePath)} --as ${quoteShell(membership.name)} catch --now`;
45
+ return membership.notifications.map((item) => {
46
+ const id = notificationMessageId(membership.squarePath, item.actIndex);
47
+ const body = injectBodyPreview(item.body, membership.squarePath, membership.name, item.actIndex);
48
+ return [
49
+ `${id} · ${membership.squarePath}: @${membership.name} from @${item.actor} (${item.via})`,
50
+ body,
51
+ `Ack with: ${command}`,
52
+ ].join('\n');
53
+ });
54
+ }),
55
+ 'Ids are stable across turns. If you already acted on an id, do not repeat the action; still run catch --now to mark delivered.',
56
+ 'Read and respond in the square before finishing the current task.</system-reminder>',
57
+ ].join('\n');
58
+ }
59
+
60
+ export default function squarePiExtension(pi) {
61
+ let sessionId;
62
+ let previousSessionId;
63
+ let sessionContext;
64
+ let checkRunning = false;
65
+ let debounceTimer;
66
+ const watchers = new Map();
67
+
68
+ function present(deliver) {
69
+ if (!sessionId) return undefined;
70
+ return presentOnce(
71
+ sessionId,
72
+ (currentSessionId) => {
73
+ const inbox = sessionInbox(currentSessionId);
74
+ updateWatchers(inbox);
75
+ return inbox;
76
+ },
77
+ deliver
78
+ );
79
+ }
80
+
81
+ function scheduleAccelerate() {
82
+ if (debounceTimer) clearTimeout(debounceTimer);
83
+ debounceTimer = setTimeout(() => {
84
+ debounceTimer = undefined;
85
+ void accelerateWake();
86
+ }, 75);
87
+ }
88
+
89
+ function watchDirectory(directory) {
90
+ const resolved = path.resolve(directory);
91
+ if (watchers.has(resolved)) return;
92
+ try {
93
+ const watcher = fs.watch(resolved, { persistent: false }, scheduleAccelerate);
94
+ watchers.set(resolved, watcher);
95
+ } catch {
96
+ // Accelerate-layer discovery is best-effort only.
97
+ }
98
+ }
99
+
100
+ function updateWatchers(inbox) {
101
+ const registry = process.env.SQUARE_REGISTRY || path.join(os.homedir(), '.square', 'sessions.ndjsonl');
102
+ try {
103
+ fs.mkdirSync(path.dirname(registry), { recursive: true });
104
+ } catch {}
105
+ watchDirectory(path.dirname(registry));
106
+ for (const membership of inbox) watchDirectory(path.dirname(membership.squarePath));
107
+ }
108
+
109
+ /** Accelerate tier: best-effort mid-turn wake. Failures only cost latency. */
110
+ async function accelerateWake() {
111
+ if (checkRunning || !sessionContext) return;
112
+ checkRunning = true;
113
+ try {
114
+ present((inbox) => {
115
+ const pending = pendingInbox(inbox);
116
+ const content = renderPiInbox(pending);
117
+ const options = sessionContext.isIdle()
118
+ ? { triggerTurn: true }
119
+ : { triggerTurn: true, deliverAs: 'steer' };
120
+ pi.sendMessage(
121
+ { customType: 'square', content, display: true, details: { keys: inboxKeys(pending) } },
122
+ options
123
+ );
124
+ });
125
+ } catch {
126
+ // Accelerate-layer failures must never break the session.
127
+ } finally {
128
+ checkRunning = false;
129
+ }
130
+ }
131
+
132
+ pi.on('session_start', async (_event, ctx) => {
133
+ sessionContext = ctx;
134
+ sessionId = ctx.sessionManager.getSessionId();
135
+ previousSessionId = process.env.SQUARE_PI_SESSION_ID;
136
+ process.env.SQUARE_PI_SESSION_ID = sessionId;
137
+ // Optional early accelerate wake if something is already pending.
138
+ await accelerateWake();
139
+ });
140
+
141
+ pi.on('before_agent_start', async () => {
142
+ try {
143
+ return present((inbox) => ({
144
+ message: {
145
+ customType: 'square',
146
+ content: renderPiInbox(pendingInbox(inbox)),
147
+ display: true,
148
+ },
149
+ }));
150
+ } catch {
151
+ return undefined;
152
+ }
153
+ });
154
+
155
+ pi.on('session_shutdown', async () => {
156
+ if (debounceTimer) clearTimeout(debounceTimer);
157
+ debounceTimer = undefined;
158
+ for (const watcher of watchers.values()) watcher.close();
159
+ watchers.clear();
160
+ if (process.env.SQUARE_PI_SESSION_ID === sessionId) {
161
+ if (previousSessionId === undefined) delete process.env.SQUARE_PI_SESSION_ID;
162
+ else process.env.SQUARE_PI_SESSION_ID = previousSessionId;
163
+ }
164
+ sessionContext = undefined;
165
+ sessionId = undefined;
166
+ });
167
+ }