@astrosheep/square 0.3.12 → 0.3.14

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 (121) hide show
  1. package/codex-plugin/.codex-plugin/plugin.json +1 -1
  2. package/codex-plugin/hooks/hooks.json +9 -0
  3. package/dist/activity-feed.d.ts +14 -0
  4. package/dist/activity-feed.js +5 -5
  5. package/dist/activity.d.ts +9 -0
  6. package/dist/activity.js +111 -90
  7. package/dist/artifact.d.ts +20 -0
  8. package/dist/artifact.js +10 -29
  9. package/dist/automatic-session.d.ts +5 -0
  10. package/dist/automatic-session.js +86 -0
  11. package/dist/boundary-presentation.d.ts +5 -0
  12. package/dist/boundary-presentation.js +5 -4
  13. package/dist/claude-hook.d.ts +9 -0
  14. package/dist/claude-hook.js +35 -3
  15. package/dist/cli/context.d.ts +35 -0
  16. package/dist/cli/context.js +25 -30
  17. package/dist/cli/harness-command.d.ts +28 -0
  18. package/dist/cli/harness-command.js +29 -14
  19. package/dist/cli/maintenance-commands.d.ts +7 -0
  20. package/dist/cli/maintenance-commands.js +3 -3
  21. package/dist/cli/meta-commands.d.ts +5 -0
  22. package/dist/cli/observation-commands.d.ts +20 -0
  23. package/dist/cli/observation-commands.js +155 -128
  24. package/dist/cli/program.d.ts +2 -0
  25. package/dist/cli/program.js +2 -12
  26. package/dist/cli/registry.d.ts +9 -0
  27. package/dist/cli/registry.js +1 -2
  28. package/dist/cli/square-commands.d.ts +34 -0
  29. package/dist/cli/square-commands.js +81 -74
  30. package/dist/cmd/notify-once.d.ts +2 -0
  31. package/dist/codex-hook.d.ts +10 -0
  32. package/dist/codex-hook.js +35 -3
  33. package/dist/decisions.d.ts +92 -0
  34. package/dist/decisions.js +83 -77
  35. package/dist/delivery-health.d.ts +21 -0
  36. package/dist/delivery-health.js +40 -34
  37. package/dist/delivery.d.ts +81 -0
  38. package/dist/delivery.js +60 -22
  39. package/dist/file-lock.d.ts +6 -0
  40. package/dist/harness-claude.d.ts +19 -0
  41. package/dist/harness-claude.js +8 -3
  42. package/dist/harness-codex.d.ts +29 -0
  43. package/dist/harness-codex.js +2 -2
  44. package/dist/harness-links.d.ts +17 -0
  45. package/dist/harness-links.js +2 -2
  46. package/dist/harness-pi.d.ts +11 -0
  47. package/dist/harness-pi.js +2 -2
  48. package/dist/harness-stage.d.ts +6 -0
  49. package/dist/harness.d.ts +20 -0
  50. package/dist/harness.js +5 -3
  51. package/dist/help.d.ts +6 -0
  52. package/dist/help.js +6 -5
  53. package/dist/identity.d.ts +13 -0
  54. package/dist/inbox.d.ts +2 -0
  55. package/dist/inbox.js +15 -19
  56. package/dist/index.d.ts +13 -0
  57. package/dist/index.js +32 -69
  58. package/dist/landing.d.ts +10 -0
  59. package/dist/landing.js +95 -0
  60. package/dist/list.d.ts +1 -0
  61. package/dist/list.js +40 -18
  62. package/dist/model.d.ts +147 -0
  63. package/dist/model.js +12 -2
  64. package/dist/notifications.d.ts +30 -0
  65. package/dist/notifications.js +75 -91
  66. package/dist/open-square.d.ts +11 -0
  67. package/dist/open-square.js +4 -0
  68. package/dist/participant-identity.d.ts +1 -0
  69. package/dist/participant-identity.js +3 -0
  70. package/dist/paseo-connection.d.ts +15 -0
  71. package/dist/paseo-delivery.d.ts +14 -0
  72. package/dist/paseo-state.d.ts +11 -0
  73. package/dist/paseo-timeline.d.ts +16 -0
  74. package/dist/presence.d.ts +3 -0
  75. package/dist/presence.js +42 -0
  76. package/dist/presentation.d.ts +91 -0
  77. package/dist/presentation.js +33 -29
  78. package/dist/presented.d.ts +11 -0
  79. package/dist/registry.d.ts +58 -0
  80. package/dist/registry.js +36 -3
  81. package/dist/routes.d.ts +42 -0
  82. package/dist/runtime.d.ts +45 -0
  83. package/dist/runtime.js +42 -34
  84. package/dist/search.d.ts +11 -0
  85. package/dist/square-core.d.ts +111 -0
  86. package/dist/square-core.js +7 -0
  87. package/dist/square-facade.d.ts +86 -0
  88. package/dist/square-facade.js +1 -0
  89. package/dist/square-file-adapter.d.ts +18 -0
  90. package/dist/square-file-adapter.js +70 -0
  91. package/dist/square-storage.d.ts +20 -0
  92. package/dist/square-storage.js +171 -0
  93. package/dist/square-wiring.d.ts +19 -0
  94. package/dist/square-wiring.js +70 -0
  95. package/dist/square.d.ts +2 -0
  96. package/dist/state-cell.d.ts +14 -0
  97. package/dist/state-cell.js +1 -0
  98. package/dist/stream.d.ts +3 -0
  99. package/dist/stream.js +19 -23
  100. package/dist/time.d.ts +6 -0
  101. package/dist/views.d.ts +95 -0
  102. package/dist/views.js +81 -0
  103. package/dist/wake-attempts.d.ts +39 -0
  104. package/dist/wake-evidence.d.ts +12 -0
  105. package/dist/wake-evidence.js +7 -10
  106. package/dist/wake-port.d.ts +20 -0
  107. package/dist/wake-sink.d.ts +12 -0
  108. package/dist/wakes.d.ts +40 -0
  109. package/dist/wakes.js +62 -0
  110. package/dist/watch.d.ts +2 -0
  111. package/dist/watch.js +89 -210
  112. package/extensions/square-opencode.js +27 -1
  113. package/extensions/square-pi.js +13 -0
  114. package/package.json +5 -2
  115. package/skills/brainstorm/SKILL.md +26 -26
  116. package/skills/square/.claude-plugin/plugin.json +1 -1
  117. package/skills/square/SKILL.md +47 -29
  118. package/skills/square/hooks/hooks.json +9 -0
  119. package/skills/square-feedback/SKILL.md +2 -2
  120. package/dist/compact.js +0 -26
  121. package/dist/square-application.js +0 -193
package/dist/watch.js CHANGED
@@ -1,55 +1,31 @@
1
1
  import { setTimeout as sleep } from 'node:timers/promises';
2
- import { loadSquare } from './artifact.js';
3
- import { SquareError, nameKey, } from './model.js';
4
- import { markDeliveredNotifications } from './delivery.js';
5
- import { SLEEP_MS, STALE_MS, WATCH_HEARTBEAT_MS, WATCH_STALE_MS, countSays, currentHold, doneNames, freshWatchLease, hasQuorum, inSquareCount, nowMs, touchPresenceCursor, writeWatchLease, } from './runtime.js';
6
- import { execute, withSquareLock, writeSquareDoc } from './square-application.js';
2
+ import { isSquareError, } from './model.js';
3
+ import { SLEEP_MS, STALE_MS, WATCH_HEARTBEAT_MS, nowMs, } from './runtime.js';
4
+ import { openSquare } from './square-file-adapter.js';
5
+ import { closeOpenSquare } from './open-square.js';
6
+ import { openParticipant } from './square-wiring.js';
7
+ import { resolveParticipant, watchPresentation } from './views.js';
8
+ import { acquireWatchLease, ownsWatchLease, pulseWatchLease, releaseWatchLease } from './wakes.js';
7
9
  import { renderWatchForceTakeover, renderWatchAlreadyActive, renderWatchOutput, renderWatchReplaced, renderWatchStatus, participantCommandPrefix, withPathOutput, } from './presentation.js';
8
- import { ackPeerDelta, deliveryDelta, filteredPeerActivities, filteredRoomChanges, matchesFeedFilter, peerPublicActs, peerRoomChanges, } from './activity-feed.js';
9
- import { coreParticipants, resolveKnownName } from './decisions.js';
10
10
  import { hasAutomaticDeliveryIdentity, localParticipantOwner } from './registry.js';
11
- function catchDelta(doc, name) {
12
- return deliveryDelta(doc, name);
13
- }
11
+ import { parseActivityId } from './square-core.js';
14
12
  function watchStatusExitCode(status) {
15
13
  return status === 'capped' ? 1 : 0;
16
14
  }
17
15
  function leaseId() {
18
16
  return `watch_${process.pid}_${Date.now()}_${Math.random().toString(36).slice(2)}`;
19
17
  }
20
- function leaseFilter(opts) {
21
- const filter = {
22
- ...(opts.participants !== undefined ? { participants: opts.participants } : {}),
23
- ...(opts.mention !== undefined ? { mention: opts.mention } : {}),
24
- };
25
- return Object.keys(filter).length === 0 ? undefined : filter;
26
- }
27
- function setLease(doc, name, id, at, opts, ownerId) {
28
- const filter = leaseFilter(opts);
29
- writeWatchLease(doc, name, {
30
- leaseId: id,
31
- ...(ownerId === undefined ? {} : { ownerId }),
32
- heartbeatAt: at,
33
- expiresAt: at + WATCH_STALE_MS,
34
- ...(filter ? { filter } : {}),
18
+ function watchOutputResult(squarePath, presentation, name, caught, opts = {}) {
19
+ const delivered = caught.activities.flatMap((activity) => {
20
+ const index = parseActivityId(activity.id);
21
+ const stored = index === undefined ? undefined : presentation.activities.find((item) => item.index === index);
22
+ return stored === undefined ? [] : [stored];
35
23
  });
36
- }
37
- function sameLease(doc, name, id, at = nowMs()) {
38
- return freshWatchLease(doc, name, at)?.leaseId === id;
39
- }
40
- function consumeDelta(doc, name, delta, delivered, at) {
41
- const consumed = ackPeerDelta(doc, name, delta);
42
- const receipts = markDeliveredNotifications(doc, name, delivered, at);
43
- return consumed || receipts;
44
- }
45
- function watchOutputResult(squarePath, doc, name, delta, opts = {}) {
46
- const publicItems = peerPublicActs(delta, name).filter((item) => matchesFeedFilter(item, opts));
47
- const roomChanges = filteredRoomChanges(delta, name, opts);
48
- consumeDelta(doc, name, delta, publicItems, nowMs());
49
- writeSquareDoc(squarePath, doc);
24
+ const publicItems = delivered.filter((item) => item.kind === 'say' || item.kind === 'done');
25
+ const roomChanges = delivered.filter((item) => item.kind === 'join' || item.kind === 'done' || item.kind === 'hold' || item.kind === 'resume');
50
26
  return {
51
27
  type: 'output',
52
- stdout: renderWatchOutput(doc.acts, publicItems, roomChanges, {
28
+ stdout: renderWatchOutput([...presentation.activities], publicItems, roomChanges, {
53
29
  ...opts,
54
30
  squarePath,
55
31
  viewer: name,
@@ -58,30 +34,11 @@ function watchOutputResult(squarePath, doc, name, delta, opts = {}) {
58
34
  ...(opts.status ? { status: opts.status } : {}),
59
35
  };
60
36
  }
61
- function loadPresence(squarePath) {
62
- try {
63
- const doc = loadSquare(squarePath);
64
- const now = nowMs();
65
- return { participants: coreParticipants(doc, now), now };
66
- }
67
- catch {
68
- return undefined;
69
- }
70
- }
71
- function loadHeaderCount(squarePath) {
72
- try {
73
- return inSquareCount(loadSquare(squarePath));
74
- }
75
- catch {
76
- return undefined;
77
- }
78
- }
79
- function writeWatchOutput(squarePath, name, stdout, status, idleMs) {
80
- const presence = loadPresence(squarePath);
81
- const headerOpts = { participantCount: loadHeaderCount(squarePath) };
37
+ function writeWatchOutput(squarePath, name, presentation, stdout, status, idleMs) {
38
+ const headerOpts = { participantCount: presentation.participantCount };
82
39
  const showCatchHint = !hasAutomaticDeliveryIdentity();
83
40
  if (status) {
84
- process.stdout.write(withPathOutput(squarePath, [renderWatchStatus({ status, squarePath, name, idleMs, presence, showCatchHint }), stdout.trimEnd()].filter(Boolean).join('\n\n').trimEnd(), headerOpts));
41
+ process.stdout.write(withPathOutput(squarePath, [renderWatchStatus({ status, squarePath, name, idleMs, presence: presentation.presence, showCatchHint }), stdout.trimEnd()].filter(Boolean).join('\n\n').trimEnd(), headerOpts));
85
42
  return;
86
43
  }
87
44
  const fallback = showCatchHint
@@ -89,67 +46,51 @@ function writeWatchOutput(squarePath, name, stdout, status, idleMs) {
89
46
  : '';
90
47
  process.stdout.write(withPathOutput(squarePath, [stdout.trimEnd(), fallback].filter(Boolean).join('\n\n').trimEnd(), headerOpts));
91
48
  }
92
- function writeWatchTerminal(squarePath, name, status, idleMs) {
93
- const presence = loadPresence(squarePath);
49
+ function writeWatchTerminal(squarePath, name, presentation, status, idleMs) {
94
50
  process.stdout.write(withPathOutput(squarePath, renderWatchStatus({
95
51
  status,
96
52
  squarePath,
97
53
  name,
98
54
  ...(idleMs === undefined ? {} : { idleMs }),
99
- presence,
55
+ presence: presentation.presence,
100
56
  showCatchHint: !hasAutomaticDeliveryIdentity(),
101
- }), { participantCount: loadHeaderCount(squarePath) }));
57
+ }), { participantCount: presentation.participantCount }));
102
58
  }
103
- function writeWatchReplaced(squarePath, name) {
104
- process.stdout.write(withPathOutput(squarePath, renderWatchReplaced({ squarePath, name }), { participantCount: loadHeaderCount(squarePath) }));
59
+ function writeWatchReplaced(squarePath, name, presentation) {
60
+ process.stdout.write(withPathOutput(squarePath, renderWatchReplaced({ squarePath, name }), { participantCount: presentation.participantCount }));
105
61
  }
106
- async function finishWatchResult(squarePath, name, result, leaseId, idleMs) {
62
+ async function finishWatchResult(square, squarePath, name, result, leaseId, idleMs) {
107
63
  if (result.type === 'output') {
108
- await endWatch(squarePath, name, leaseId);
109
- writeWatchOutput(squarePath, name, result.stdout, result.status);
64
+ await endWatch(square, name, leaseId);
65
+ writeWatchOutput(squarePath, name, await watchPresentation(square, name), result.stdout, result.status);
110
66
  process.exitCode = watchStatusExitCode(result.status);
111
67
  return true;
112
68
  }
113
69
  if (result.type === 'terminal') {
114
- await endWatch(squarePath, name, leaseId);
115
- writeWatchTerminal(squarePath, name, result.status, idleMs);
70
+ await endWatch(square, name, leaseId);
71
+ writeWatchTerminal(squarePath, name, await watchPresentation(square, name), result.status, idleMs);
116
72
  process.exitCode = watchStatusExitCode(result.status);
117
73
  return true;
118
74
  }
119
75
  if (result.type === 'replaced') {
120
- writeWatchReplaced(squarePath, name);
76
+ writeWatchReplaced(squarePath, name, await watchPresentation(square, name));
121
77
  process.exitCode = 0;
122
78
  return true;
123
79
  }
124
80
  return false;
125
81
  }
126
- async function beginWatch(squarePath, name, opts) {
127
- const at = nowMs();
82
+ async function beginWatch(square, squarePath, name, opts) {
128
83
  const id = leaseId();
129
84
  const ownerId = localParticipantOwner(squarePath, name);
130
- const committed = await execute(squarePath, {
131
- type: 'lease',
132
- name,
133
- leaseId: id,
134
- ...(ownerId === undefined ? {} : { ownerId }),
135
- at,
136
- expiresAt: at + WATCH_STALE_MS,
137
- force: opts.replace,
138
- filter: leaseFilter(opts),
139
- });
140
- if (committed.result.type === 'active')
141
- return committed.result;
142
- return { type: 'started', leaseId: id, replaced: committed.result.replaced, heartbeatAt: at };
85
+ return acquireWatchLease(square, name, id, opts, ownerId);
143
86
  }
144
- async function endWatch(squarePath, name, id) {
145
- if (id === undefined)
146
- return;
147
- await execute(squarePath, { type: 'release-lease', name, leaseId: id });
87
+ async function endWatch(square, name, id) {
88
+ await releaseWatchLease(square, name, id);
148
89
  }
149
- function installWatchInterruptHandler(squarePath, name, currentLeaseId) {
90
+ function installWatchInterruptHandler(square, squarePath, name, currentLeaseId) {
150
91
  const onInterrupt = () => {
151
92
  void (async () => {
152
- await endWatch(squarePath, name, currentLeaseId());
93
+ await endWatch(square, name, currentLeaseId());
153
94
  process.stdout.write(withPathOutput(squarePath, '✕ catch stopped'));
154
95
  process.exit(130);
155
96
  })().catch((error) => {
@@ -162,157 +103,93 @@ function installWatchInterruptHandler(squarePath, name, currentLeaseId) {
162
103
  process.off('SIGINT', onInterrupt);
163
104
  };
164
105
  }
165
- function terminalStatus(doc, name) {
166
- const count = countSays(doc.acts, name);
167
- if (doc.hardCap !== null && count >= doc.hardCap)
168
- return 'capped';
169
- const done = doneNames(doc.acts);
170
- done.delete(nameKey(name));
171
- if (hasQuorum(doc, name, done))
172
- return 'quorum';
173
- return undefined;
174
- }
175
106
  async function cmdWatchNow(squarePath, name, opts) {
176
- const result = await withSquareLock(squarePath, () => {
177
- const doc = loadSquare(squarePath);
178
- const at = nowMs();
179
- const touched = touchPresenceCursor(doc, name, at);
180
- const delta = catchDelta(doc, name);
181
- const peerPublic = peerPublicActs(delta, name);
182
- const roomChanges = peerRoomChanges(delta, name);
183
- const filteredActivities = filteredPeerActivities(delta, name, opts);
184
- const matchingRoomChanges = filteredRoomChanges(delta, name, opts);
185
- const hasDeliverable = peerPublic.length > 0 || roomChanges.length > 0;
186
- const hasFilteredDeliverable = filteredActivities.length > 0 || matchingRoomChanges.length > 0;
187
- const status = terminalStatus(doc, name);
188
- if (hasDeliverable && hasFilteredDeliverable) {
189
- return watchOutputResult(squarePath, doc, name, delta, {
190
- participants: opts.participants,
191
- mention: opts.mention,
192
- ...(status ? { status } : {}),
193
- });
194
- }
195
- if (hasDeliverable) {
196
- const acked = ackPeerDelta(doc, name, delta);
197
- if (acked || touched)
198
- writeSquareDoc(squarePath, doc);
199
- }
200
- else if (touched) {
201
- writeSquareDoc(squarePath, doc);
202
- }
203
- if (status)
204
- return { type: 'terminal', status };
205
- return { type: 'terminal', status: 'empty-now' };
206
- });
207
- await finishWatchResult(squarePath, name, result, undefined);
107
+ const square = await openSquare(squarePath, { clock: nowMs });
108
+ const facade = await openParticipant({ path: squarePath, clock: nowMs }, name);
109
+ try {
110
+ const caught = await facade.participant.catch({
111
+ ...(opts.participants === undefined ? {} : { from: opts.participants }),
112
+ ...(opts.mention === undefined ? {} : { mention: true }),
113
+ });
114
+ const presentation = await watchPresentation(square, name);
115
+ const status = presentation.terminalStatus;
116
+ const result = caught.activities.length > 0
117
+ ? watchOutputResult(squarePath, presentation, name, caught, { mention: opts.mention, ...(status ? { status } : {}) })
118
+ : { type: 'terminal', status: status ?? 'empty-now' };
119
+ await finishWatchResult(square, squarePath, name, result, undefined);
120
+ }
121
+ finally {
122
+ await facade.close();
123
+ await closeOpenSquare(square);
124
+ }
208
125
  }
209
126
  export async function cmdWatch(squarePath, name, opts) {
210
- let initialDoc;
127
+ let square;
211
128
  try {
212
- initialDoc = loadSquare(squarePath);
213
- name = resolveKnownName(initialDoc, name);
129
+ square = await openSquare(squarePath, { clock: nowMs });
130
+ name = (await resolveParticipant(square, name)).name;
214
131
  if (opts.mention !== undefined)
215
- opts = { ...opts, mention: resolveKnownName(initialDoc, opts.mention) };
132
+ opts = { ...opts, mention: (await resolveParticipant(square, opts.mention)).name };
216
133
  if (opts.participants !== undefined && opts.participants.length > 0) {
217
- opts = { ...opts, participants: opts.participants.map((p) => resolveKnownName(initialDoc, p)) };
134
+ opts = { ...opts, participants: await Promise.all(opts.participants.map(async (participant) => (await resolveParticipant(square, participant)).name)) };
218
135
  }
219
136
  }
220
137
  catch (err) {
221
- if (err instanceof SquareError) {
138
+ if (isSquareError(err)) {
222
139
  process.stderr.write(err.message + '\n');
223
140
  process.exit(err.code === 'not_found' ? 1 : 2);
224
141
  }
225
142
  throw err;
226
143
  }
227
144
  if (opts.now) {
145
+ await closeOpenSquare(square);
228
146
  await cmdWatchNow(squarePath, name, opts);
229
147
  return;
230
148
  }
231
- const start = await beginWatch(squarePath, name, opts);
149
+ const start = await beginWatch(square, squarePath, name, opts);
232
150
  if (start.type === 'active') {
233
- process.stdout.write(withPathOutput(squarePath, renderWatchAlreadyActive({ squarePath, name }), { participantCount: loadHeaderCount(squarePath) }));
151
+ const presentation = await watchPresentation(square, name);
152
+ process.stdout.write(withPathOutput(squarePath, renderWatchAlreadyActive({ squarePath, name }), { participantCount: presentation.participantCount }));
153
+ await closeOpenSquare(square);
234
154
  process.exit(1);
235
155
  }
236
156
  let staleSince = nowMs();
237
157
  let currentLeaseId = start.leaseId;
238
158
  let nextHeartbeatAt = start.heartbeatAt + WATCH_HEARTBEAT_MS;
239
159
  if (start.replaced) {
240
- process.stdout.write(withPathOutput(squarePath, renderWatchForceTakeover({ squarePath, name }), { participantCount: loadHeaderCount(squarePath) }));
160
+ const presentation = await watchPresentation(square, name);
161
+ process.stdout.write(withPathOutput(squarePath, renderWatchForceTakeover({ squarePath, name }), { participantCount: presentation.participantCount }));
241
162
  }
242
163
  const idleMs = opts.idleMs ?? STALE_MS;
243
- const removeInterruptHandler = installWatchInterruptHandler(squarePath, name, () => currentLeaseId);
164
+ const removeInterruptHandler = installWatchInterruptHandler(square, squarePath, name, () => currentLeaseId);
165
+ const facade = await openParticipant({ path: squarePath, clock: nowMs }, name);
244
166
  try {
245
167
  while (true) {
246
- const result = await withSquareLock(squarePath, () => {
247
- const doc = loadSquare(squarePath);
248
- const at = nowMs();
249
- const lease = freshWatchLease(doc, name, at);
250
- if (lease === undefined || lease.leaseId !== currentLeaseId)
251
- return { type: 'replaced' };
252
- let mutated = false;
253
- if (at >= nextHeartbeatAt) {
254
- setLease(doc, name, currentLeaseId, at, opts, lease.ownerId);
255
- mutated = touchPresenceCursor(doc, name, at) || mutated;
256
- nextHeartbeatAt = at + WATCH_HEARTBEAT_MS;
257
- mutated = true;
258
- }
259
- const delta = catchDelta(doc, name);
260
- const peerPublic = peerPublicActs(delta, name);
261
- const roomChanges = peerRoomChanges(delta, name);
262
- const filteredActivities = filteredPeerActivities(delta, name, opts);
263
- const matchingRoomChanges = filteredRoomChanges(delta, name, opts);
264
- const hasDeliverable = peerPublic.length > 0 || roomChanges.length > 0;
265
- const hasFilteredDeliverable = filteredActivities.length > 0 || matchingRoomChanges.length > 0;
266
- const status = terminalStatus(doc, name);
267
- if (currentHold(doc.acts).active) {
268
- if (mutated)
269
- writeSquareDoc(squarePath, doc);
270
- return { type: 'held' };
271
- }
272
- if (hasDeliverable &&
273
- hasFilteredDeliverable &&
274
- (filteredActivities.length > 0 || matchingRoomChanges.length > 0 || status !== undefined)) {
275
- return watchOutputResult(squarePath, doc, name, delta, {
276
- participants: opts.participants,
277
- mention: opts.mention,
278
- ...(status ? { status } : {}),
279
- });
280
- }
281
- if (hasDeliverable && !hasFilteredDeliverable)
282
- mutated = ackPeerDelta(doc, name, delta) || mutated;
283
- if (status) {
284
- if (mutated)
285
- writeSquareDoc(squarePath, doc);
286
- return { type: 'terminal', status };
168
+ const leaseState = await pulseWatchLease(square, name, currentLeaseId, opts, nowMs() >= nextHeartbeatAt);
169
+ if (leaseState.type === 'sleep' && leaseState.heartbeatAt !== undefined) {
170
+ nextHeartbeatAt = leaseState.heartbeatAt + WATCH_HEARTBEAT_MS;
171
+ }
172
+ let result = leaseState;
173
+ if (result.type === 'sleep') {
174
+ const caught = await facade.participant.catch({
175
+ ...(opts.participants === undefined ? {} : { from: opts.participants }),
176
+ ...(opts.mention === undefined ? {} : { mention: true }),
177
+ });
178
+ if (caught.activities.length > 0) {
179
+ result = watchOutputResult(squarePath, await watchPresentation(square, name), name, caught, { mention: opts.mention });
287
180
  }
288
- if (mutated)
289
- writeSquareDoc(squarePath, doc);
290
- return { type: 'sleep' };
291
- });
292
- if (await finishWatchResult(squarePath, name, result, currentLeaseId)) {
181
+ }
182
+ if (await finishWatchResult(square, squarePath, name, result, currentLeaseId)) {
293
183
  currentLeaseId = undefined;
294
184
  return;
295
185
  }
296
186
  if (result.type === 'held')
297
187
  staleSince = nowMs();
298
188
  if (nowMs() - staleSince >= idleMs) {
299
- const result = await withSquareLock(squarePath, () => {
300
- const doc = loadSquare(squarePath);
301
- if (!sameLease(doc, name, currentLeaseId))
302
- return { type: 'replaced' };
303
- const delta = catchDelta(doc, name);
304
- const filteredActivities = filteredPeerActivities(delta, name, opts);
305
- if (filteredActivities.length > 0) {
306
- return watchOutputResult(squarePath, doc, name, delta, {
307
- stalePartial: true,
308
- participants: opts.participants,
309
- mention: opts.mention,
310
- idleMs,
311
- });
312
- }
313
- return { type: 'terminal', status: 'stale' };
314
- });
315
- if (await finishWatchResult(squarePath, name, result, currentLeaseId, idleMs)) {
189
+ const result = !await ownsWatchLease(square, name, currentLeaseId)
190
+ ? { type: 'replaced' }
191
+ : { type: 'terminal', status: 'stale' };
192
+ if (await finishWatchResult(square, squarePath, name, result, currentLeaseId, idleMs)) {
316
193
  currentLeaseId = undefined;
317
194
  return;
318
195
  }
@@ -321,6 +198,8 @@ export async function cmdWatch(squarePath, name, opts) {
321
198
  }
322
199
  }
323
200
  finally {
201
+ await facade.close();
202
+ await closeOpenSquare(square);
324
203
  removeInterruptHandler();
325
204
  }
326
205
  }
@@ -1,14 +1,40 @@
1
1
  import { presentPendingAtBoundary } from '../dist/boundary-presentation.js';
2
+ import { automaticSessionEnd, automaticSessionStart } from '../dist/automatic-session.js';
2
3
 
3
4
  export default async function squareOpenCodePlugin() {
5
+ const joining = new Map();
4
6
  return {
7
+ event: async ({ event }) => {
8
+ if (event.type === 'session.created' || event.type === 'session.updated') {
9
+ const sessionID = event.properties?.sessionID;
10
+ const cwd = event.properties?.info?.directory || process.cwd();
11
+ if (sessionID) {
12
+ try {
13
+ const context = await automaticSessionStart('opencode', sessionID, cwd);
14
+ if (context) joining.set(sessionID, context);
15
+ } catch { /* startup remains bounded */ }
16
+ }
17
+ } else if (event.type === 'session.deleted') {
18
+ const sessionID = event.properties?.sessionID;
19
+ const cwd = event.properties?.info?.directory || process.cwd();
20
+ if (sessionID) {
21
+ joining.delete(sessionID);
22
+ await automaticSessionEnd('opencode', sessionID, cwd);
23
+ }
24
+ }
25
+ },
5
26
  'shell.env': async (input, output) => {
6
27
  if (input.sessionID) output.env.OPENCODE_SESSION_ID = input.sessionID;
7
28
  },
8
29
 
9
30
  'tool.execute.after': async (input, output) => {
10
31
  try {
11
- presentPendingAtBoundary(
32
+ const joined = joining.get(input.sessionID);
33
+ if (joined) {
34
+ joining.delete(input.sessionID);
35
+ output.output = `${output.output}${output.output === '' ? '' : '\n\n'}${joined}`;
36
+ }
37
+ await presentPendingAtBoundary(
12
38
  input.sessionID,
13
39
  (context) => {
14
40
  output.output = `${output.output}${output.output === '' ? '' : '\n\n'}${context}`;
@@ -1,4 +1,5 @@
1
1
  import { presentPendingAtBoundary, renderPendingAtBoundary } from '../dist/boundary-presentation.js';
2
+ import { automaticSessionEnd, automaticSessionStart } from '../dist/automatic-session.js';
2
3
 
3
4
  export function pendingInbox(inbox) {
4
5
  return inbox.filter((item) => item.notifications?.length > 0);
@@ -16,17 +17,26 @@ export function renderPiInbox(inbox) {
16
17
 
17
18
  export default function squarePiExtension(pi) {
18
19
  let sessionId;
20
+ let sessionCwd;
21
+ let joiningContext;
19
22
  let previousSessionId;
20
23
  const present = (deliver) => sessionId === undefined ? undefined : presentPendingAtBoundary(sessionId, deliver);
21
24
 
22
25
  pi.on('session_start', async (_event, ctx) => {
23
26
  sessionId = ctx.sessionManager.getSessionId();
27
+ sessionCwd = ctx.cwd || process.cwd();
24
28
  previousSessionId = process.env.SQUARE_PI_SESSION_ID;
25
29
  process.env.SQUARE_PI_SESSION_ID = sessionId;
30
+ try { joiningContext = await automaticSessionStart('pi', sessionId, sessionCwd); } catch { joiningContext = undefined; }
26
31
  });
27
32
 
28
33
  pi.on('before_agent_start', async () => {
29
34
  try {
35
+ if (joiningContext) {
36
+ const context = joiningContext;
37
+ joiningContext = undefined;
38
+ return { message: { customType: 'square', content: context, display: true } };
39
+ }
30
40
  return present((context) => ({ message: { customType: 'square', content: context, display: true } }));
31
41
  } catch {
32
42
  return undefined;
@@ -34,10 +44,13 @@ export default function squarePiExtension(pi) {
34
44
  });
35
45
 
36
46
  pi.on('session_shutdown', async () => {
47
+ if (sessionId && sessionCwd) await automaticSessionEnd('pi', sessionId, sessionCwd);
37
48
  if (process.env.SQUARE_PI_SESSION_ID === sessionId) {
38
49
  if (previousSessionId === undefined) delete process.env.SQUARE_PI_SESSION_ID;
39
50
  else process.env.SQUARE_PI_SESSION_ID = previousSessionId;
40
51
  }
41
52
  sessionId = undefined;
53
+ sessionCwd = undefined;
54
+ joiningContext = undefined;
42
55
  });
43
56
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/square",
3
- "version": "0.3.12",
3
+ "version": "0.3.14",
4
4
  "description": "A shared public square where agents join, catch activity, express, and step out when done.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,7 +18,8 @@
18
18
  "build": "npm run clean && tsc && node --input-type=module -e \"import fs from 'node:fs'; fs.chmodSync('dist/square.js',0o755)\"",
19
19
  "verify-release": "node test/release.test.js",
20
20
  "prepublishOnly": "npm run build && npm run verify-release",
21
- "test": "npm run build && node --test test/*.test.js"
21
+ "test": "npm run build && node --test test/*.test.js",
22
+ "test:cli-process": "npm run build && node --test test/process/square-cli-render.test.js"
22
23
  },
23
24
  "keywords": [
24
25
  "brainstorm",
@@ -35,6 +36,7 @@
35
36
  "author": "astrosheep",
36
37
  "license": "MIT",
37
38
  "devDependencies": {
39
+ "@types/cross-spawn": "^6.0.6",
38
40
  "@types/node": "^22",
39
41
  "@types/ws": "^8.18.1",
40
42
  "typescript": "^5.8"
@@ -57,6 +59,7 @@
57
59
  },
58
60
  "dependencies": {
59
61
  "@getpaseo/client": "^0.3.1",
62
+ "cross-spawn": "^7.0.6",
60
63
  "ws": "^8.21.3"
61
64
  }
62
65
  }
@@ -7,7 +7,7 @@ allowed-tools: Bash(square *), Skill(square)
7
7
  # Square Brainstorm
8
8
 
9
9
  Use this skill when you are coordinating a brainstorm. Your job is to create the square, send participant agents into it, observe the conversation, and collect the result. Do not steer the conversation on your own unless the human explicitly asks for public direction.
10
- Commands default to `.square/SQUARE.square`; use `--location <path>` when you want a different file.
10
+ Commands default to `.square/SQUARE.square` for build and inspection. Manual participant commands require `--location <square>` or `SQUARE_LOCATION`; `--location` wins. Set `SQUARE_PARTICIPANT_NAME` for the participant name, overridden by `--as`. `.square/PUBLIC.square` is the public square and does not need a manual `join`.
11
11
 
12
12
  ## Build
13
13
 
@@ -31,33 +31,33 @@ square status
31
31
 
32
32
  ## Start Participants
33
33
 
34
- Send each participant agent this prompt. Replace `<name>` and `<path>`, but do not summarize or rewrite the prompt:
34
+ Send each participant agent this prompt. Replace `<name>` and `<square>`, but do not summarize or rewrite the prompt:
35
35
 
36
36
  ```text
37
- You are <name>, participating in a brainstorm. The square file is at <path>.
37
+ You are <name>, participating in a brainstorm. The square file is at <square>.
38
38
 
39
39
  First action: enter the square. Read the context, warmup, and recent activity printed by this command before expressing:
40
- square --location <path> --as <name> join
40
+ square --location <square> --as <name> join
41
41
 
42
42
  Then follow the Happy Path from the join output. Core commands:
43
- square --location <path> --as <name> express - <<'EOF'
43
+ square --location <square> --as <name> express - <<'EOF'
44
44
  ...
45
45
  EOF
46
- square --location <path> --as <name> catch --mention --idle 10m
47
- square --location <path> --as <name> catch --now
48
- square --location <path> --as <name> catch --idle 10m
49
- square --location <path> history --limit 80
50
- square --location <path> history --from <name> --limit 80
51
- square --location <path> status
52
- square --location <path> --as <name> done - <<'EOF'
46
+ square --location <square> --as <name> catch --mention --idle 10m
47
+ square --location <square> --as <name> catch --now
48
+ square --location <square> --as <name> catch --idle 10m
49
+ square --location <square> history --limit 80
50
+ square --location <square> history --from <name> --limit 80
51
+ square --location <square> status
52
+ square --location <square> --as <name> done - <<'EOF'
53
53
  ...
54
54
  EOF
55
55
 
56
- For complete history: square --location <path> history --all --full
56
+ For complete history: square --location <square> history --all --full
57
57
 
58
58
  Every activity must address at least one participant with @name. Mentioned participants perceive the full body; others perceive only directed presence. Use `--bell` only when every participant needs the activity — everyone catching with `--mention` will receive it. Precise history queries may still read original archive bodies.
59
59
 
60
- If an activity is refused because something happened while the participant was not looking, run `square --location <path> --as <name> catch --now`, take it in, then express again. `catch --now` catches up without waiting.
60
+ If an activity is refused because something happened while the participant was not looking, run `square --location <square> --as <name> catch --now`, take it in, then express again. `catch --now` catches up without waiting.
61
61
  ```
62
62
 
63
63
  Need another voice later? Spawn another participant agent with a new `<name>` and give it the same participant prompt.
@@ -67,12 +67,12 @@ Need another voice later? Spawn another participant agent with a new `<name>` an
67
67
  If you or the human want to participate, choose a participant name and use the participant loop:
68
68
 
69
69
  ```bash
70
- square --location <path> --as <name> join
71
- square --location <path> --as <name> express - <<'EOF'
70
+ square --location <square> --as <name> join
71
+ square --location <square> --as <name> express - <<'EOF'
72
72
  @<participant-name> your view
73
73
  EOF
74
- square --location <path> --as <name> catch --idle 10m
75
- square --location <path> --as <name> done - <<'EOF'
74
+ square --location <square> --as <name> catch --idle 10m
75
+ square --location <square> --as <name> done - <<'EOF'
76
76
  final note
77
77
  EOF
78
78
  ```
@@ -82,9 +82,9 @@ EOF
82
82
  Use these to check progress:
83
83
 
84
84
  ```bash
85
- square --location <path> history --limit 50
86
- square --location <path> history --from <name>
87
- square --location <path> status
85
+ square --location <square> history --limit 50
86
+ square --location <square> history --from <name>
87
+ square --location <square> status
88
88
  ```
89
89
 
90
90
  `history` reads the archive without advancing participant presence. `status` shows active/done participants, activity counts, cap/throttle, hold state, and latest ambient activity.
@@ -96,7 +96,7 @@ Every activity must contain `@name`; use `--bell` only for activity that every p
96
96
  If the human wants to refocus the square, add a constraint, ask a convergence question, or correct its direction, write that direction publicly with a participant name:
97
97
 
98
98
  ```bash
99
- square --location <path> --as <name> express --bell - <<'EOF'
99
+ square --location <square> --as <name> express --bell - <<'EOF'
100
100
  Refocus on <specific direction, constraint, question, or decision needed>.
101
101
  EOF
102
102
  ```
@@ -106,8 +106,8 @@ Do not add direction on your own. If you notice the square drifting or stuck, re
106
106
  Pause the participant loop when a human needs time to read, think, or add another voice:
107
107
 
108
108
  ```bash
109
- square --location <path> hold "human reading"
110
- square --location <path> resume
109
+ square --location <square> hold "human reading"
110
+ square --location <square> resume
111
111
  ```
112
112
 
113
113
  While held, participant expression and catch pause. Join, done, status, and history still work.
@@ -117,8 +117,8 @@ While held, participant expression and catch pause. Join, done, status, and hist
117
117
  When participants are done, collect the public activities:
118
118
 
119
119
  ```bash
120
- square --location <path> history --all --full # complete public history
121
- square --location <path> status
120
+ square --location <square> history --all --full # complete public history
121
+ square --location <square> status
122
122
  ```
123
123
 
124
124
  ## Boundaries