@astrosheep/square 0.3.5 → 0.3.6

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 (51) hide show
  1. package/codex-plugin/.codex-plugin/plugin.json +3 -2
  2. package/dist/activity-feed.js +26 -18
  3. package/dist/activity.js +9 -10
  4. package/dist/artifact.js +126 -202
  5. package/dist/claude-hook.js +45 -21
  6. package/dist/cli/context.js +7 -7
  7. package/dist/cli/maintenance-commands.js +10 -26
  8. package/dist/cli/meta-commands.js +3 -6
  9. package/dist/cli/observation-commands.js +44 -52
  10. package/dist/cli/program.js +4 -4
  11. package/dist/cli/registry.js +5 -5
  12. package/dist/cli/square-commands.js +16 -18
  13. package/dist/cmd/notify-once.js +23 -21
  14. package/dist/compact.js +1 -1
  15. package/dist/decisions.js +53 -86
  16. package/dist/delivery-health.js +104 -210
  17. package/dist/delivery.js +68 -18
  18. package/dist/doctor.js +9 -8
  19. package/dist/harness-claude.js +38 -245
  20. package/dist/harness-codex.js +82 -616
  21. package/dist/harness-stage.js +36 -0
  22. package/dist/harness.js +3 -5
  23. package/dist/help.js +43 -35
  24. package/dist/inbox.js +12 -11
  25. package/dist/index.js +9 -121
  26. package/dist/list.js +1 -1
  27. package/dist/model.js +0 -6
  28. package/dist/notification-failures.js +54 -0
  29. package/dist/notifications.js +47 -62
  30. package/dist/paseo-timeline.js +58 -188
  31. package/dist/presentation.js +55 -63
  32. package/dist/presented.js +9 -8
  33. package/dist/registry.js +55 -45
  34. package/dist/runtime.js +27 -84
  35. package/dist/square-application.js +135 -130
  36. package/dist/square-core.js +3 -11
  37. package/dist/stream.js +27 -126
  38. package/dist/wake-sink.js +134 -188
  39. package/dist/watch.js +65 -122
  40. package/extensions/square-opencode.js +1 -1
  41. package/extensions/square-pi.js +8 -130
  42. package/guides/architect.md +3 -3
  43. package/guides/participant.md +25 -16
  44. package/package.json +2 -2
  45. package/skills/brainstorm/SKILL.md +25 -32
  46. package/skills/square/.claude-plugin/plugin.json +1 -1
  47. package/skills/square/SKILL.md +39 -107
  48. package/skills/square-feedback/SKILL.md +4 -4
  49. package/dist/harness-lifecycle.js +0 -102
  50. package/dist/square-store.js +0 -111
  51. package/dist/terminal.js +0 -125
package/dist/registry.js CHANGED
@@ -9,11 +9,19 @@ import fs from 'node:fs';
9
9
  import path from 'node:path';
10
10
  import { homedir } from 'node:os';
11
11
  import { randomUUID } from 'node:crypto';
12
+ import { loadSquare } from './artifact.js';
12
13
  import { nameKey, sameName } from './model.js';
14
+ import { isCurrentlyJoined } from './runtime.js';
13
15
  const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
14
16
  const COMPACT_BYTES = 64 * 1024;
15
17
  const COMPACT_LINES = 1000;
16
18
  const VALID_CHANNELS = new Set(['claude-code', 'codex', 'opencode', 'pi', 'paseo', 'unknown']);
19
+ const LOCAL_SESSION_SOURCES = [
20
+ { variable: 'CLAUDE_CODE_SESSION_ID', channel: 'claude-code', child: 'CLAUDE_CODE_CHILD_SESSION' },
21
+ { variable: 'CODEX_THREAD_ID', channel: 'codex' },
22
+ { variable: 'OPENCODE_SESSION_ID', channel: 'opencode' },
23
+ { variable: 'SQUARE_PI_SESSION_ID', channel: 'pi' },
24
+ ];
17
25
  export function registryPath() {
18
26
  if (process.env['SQUARE_REGISTRY'])
19
27
  return process.env['SQUARE_REGISTRY'];
@@ -113,9 +121,8 @@ function foldRegistry(raw, now) {
113
121
  }
114
122
  return active.sort((a, b) => b.updatedAt - a.updatedAt);
115
123
  }
116
- function compactRegistry(filePath, raw, now) {
117
- const active = foldRegistry(raw, now);
118
- const compacted = active
124
+ function writeRegistryBindings(filePath, bindings) {
125
+ const compacted = bindings
119
126
  .slice()
120
127
  .reverse()
121
128
  .map((binding) => JSON.stringify({
@@ -135,6 +142,9 @@ function compactRegistry(filePath, raw, now) {
135
142
  fs.writeFileSync(temporary, compacted === '' ? '' : `${compacted}\n`, { mode: 0o600 });
136
143
  fs.renameSync(temporary, filePath);
137
144
  }
145
+ function compactRegistry(filePath, raw, now) {
146
+ writeRegistryBindings(filePath, foldRegistry(raw, now));
147
+ }
138
148
  function maybeCompactRegistry(filePath, now) {
139
149
  let stat;
140
150
  try {
@@ -214,53 +224,53 @@ export function lookupParticipant(squarePath, name, now = Date.now()) {
214
224
  const canonicalPath = canonicalSquarePath(squarePath);
215
225
  return readActiveBindings(now).filter((binding) => binding.squarePath === canonicalPath && sameName(binding.name, name));
216
226
  }
217
- export function localSessionIdentities(env = process.env) {
218
- const paseoAgentId = env['PASEO_AGENT_ID']?.trim() || undefined;
219
- const identities = [];
220
- const claudeSessionId = env['CLAUDE_CODE_SESSION_ID']?.trim();
221
- if (claudeSessionId) {
222
- identities.push({
223
- sessionId: claudeSessionId,
224
- channel: 'claude-code',
225
- child: env['CLAUDE_CODE_CHILD_SESSION'] === '1',
226
- ...(paseoAgentId ? { paseoAgentId } : {}),
227
- });
227
+ /** Resolve the current local harness owner for a participant, if one is registered. */
228
+ export function localParticipantOwner(squarePath, name, env = process.env, now = Date.now()) {
229
+ const sessionIds = new Set(localSessionIdentities(env).map((identity) => identity.sessionId));
230
+ if (sessionIds.size === 0)
231
+ return undefined;
232
+ return lookupParticipant(squarePath, name, now).find((binding) => sessionIds.has(binding.sessionId))?.ownerId;
233
+ }
234
+ function bindingIsProvablyObsolete(binding) {
235
+ if (!fs.existsSync(binding.squarePath))
236
+ return true;
237
+ try {
238
+ return !isCurrentlyJoined(loadSquare(binding.squarePath).acts, binding.name);
228
239
  }
229
- const codexThreadId = env['CODEX_THREAD_ID']?.trim();
230
- if (codexThreadId && !identities.some((identity) => identity.sessionId === codexThreadId)) {
231
- identities.push({
232
- sessionId: codexThreadId,
233
- channel: 'codex',
234
- child: false,
235
- ...(paseoAgentId ? { paseoAgentId } : {}),
236
- });
240
+ catch {
241
+ // A temporarily unreadable artifact is uncertain, so preserve its binding.
242
+ return false;
237
243
  }
238
- const openCodeSessionId = env['OPENCODE_SESSION_ID']?.trim();
239
- if (openCodeSessionId && !identities.some((identity) => identity.sessionId === openCodeSessionId)) {
240
- identities.push({
241
- sessionId: openCodeSessionId,
242
- channel: 'opencode',
243
- child: false,
244
- ...(paseoAgentId ? { paseoAgentId } : {}),
245
- });
244
+ }
245
+ /** Compact the registry and remove only bindings disproved by their authoritative artifact. */
246
+ export function pruneRegistry(now = Date.now()) {
247
+ const filePath = registryPath();
248
+ let raw;
249
+ try {
250
+ raw = fs.readFileSync(filePath, 'utf8');
246
251
  }
247
- const piSessionId = env['SQUARE_PI_SESSION_ID']?.trim();
248
- if (piSessionId && !identities.some((identity) => identity.sessionId === piSessionId)) {
249
- identities.push({
250
- sessionId: piSessionId,
251
- channel: 'pi',
252
- child: false,
253
- ...(paseoAgentId ? { paseoAgentId } : {}),
254
- });
252
+ catch (error) {
253
+ if (error.code === 'ENOENT')
254
+ return { removed: 0, kept: 0 };
255
+ throw error;
255
256
  }
256
- if (paseoAgentId && !identities.some((identity) => identity.sessionId === paseoAgentId)) {
257
- identities.push({
258
- sessionId: paseoAgentId,
259
- channel: 'paseo',
260
- child: false,
261
- paseoAgentId,
262
- });
257
+ const active = foldRegistry(raw, now);
258
+ const kept = active.filter((binding) => !bindingIsProvablyObsolete(binding));
259
+ writeRegistryBindings(filePath, kept);
260
+ return { removed: active.length - kept.length, kept: kept.length };
261
+ }
262
+ function addLocalSession(identities, sessionId, channel, child, paseoAgentId) {
263
+ if (!sessionId || identities.some((identity) => identity.sessionId === sessionId))
264
+ return;
265
+ identities.push({ sessionId, channel, child, ...(paseoAgentId ? { paseoAgentId } : {}) });
266
+ }
267
+ export function localSessionIdentities(env = process.env) {
268
+ const paseoAgentId = env['PASEO_AGENT_ID']?.trim() || undefined;
269
+ const identities = [];
270
+ for (const source of LOCAL_SESSION_SOURCES) {
271
+ addLocalSession(identities, env[source.variable]?.trim(), source.channel, source.child !== undefined && env[source.child] === '1', paseoAgentId);
263
272
  }
273
+ addLocalSession(identities, paseoAgentId, 'paseo', false, paseoAgentId);
264
274
  return identities;
265
275
  }
266
276
  /** True when this process belongs to a harness that can deliver Square attention without a foreground catch. */
package/dist/runtime.js CHANGED
@@ -59,8 +59,13 @@ export function extractMentions(body) {
59
59
  matches.push(match[1]);
60
60
  return matches;
61
61
  }
62
- /** Pure mention filter for say acts. Broadcast bodies (no @) match any named viewer. */
62
+ /** Pure directed-activity filter. Broadcast bodies (no @) match any named viewer. */
63
63
  export function matchesMentionTarget(act, mention) {
64
+ if (act.reach === 'bell')
65
+ return true;
66
+ if (act.reach !== undefined) {
67
+ return mention === true || sameName(act.reach.beside, mention);
68
+ }
64
69
  const mentions = extractMentions(act.body);
65
70
  if (mention === true)
66
71
  return mentions.length > 0;
@@ -104,24 +109,12 @@ export function sayNumberFor(acts, target) {
104
109
  export function doneNames(acts) {
105
110
  return new Set(fold(acts).done.map((participant) => nameKey(participant)));
106
111
  }
107
- export function hasJoined(acts, name) {
108
- return fold(acts).participants.some((participant) => sameName(participant.name, name) && participant.joined);
109
- }
110
112
  export function joinedNames(acts) {
111
113
  return new Set(acts.filter((act) => act.kind === 'join').map((act) => nameKey(act.actor)));
112
114
  }
113
115
  export function isCurrentlyJoined(acts, name) {
114
116
  return fold(acts).participants.some((participant) => sameName(participant.name, name) && participant.joined);
115
117
  }
116
- /** Timestamp of the recipient's most recent join act, if any. */
117
- export function lastJoinAt(acts, name) {
118
- let last;
119
- for (const act of acts) {
120
- if (act.kind === 'join' && sameName(act.actor, name))
121
- last = act.at;
122
- }
123
- return last;
124
- }
125
118
  /** Stable index of the recipient's most recent join act, if any. */
126
119
  export function lastJoinIndex(acts, name) {
127
120
  let last;
@@ -131,11 +124,6 @@ export function lastJoinIndex(acts, name) {
131
124
  }
132
125
  return last;
133
126
  }
134
- /** Notifications are live only when they land after the recipient joined. */
135
- export function isPostJoinActivity(acts, name, actIndex) {
136
- const joinIndex = lastJoinIndex(acts, name);
137
- return joinIndex !== undefined && actIndex > joinIndex;
138
- }
139
127
  export function actStableIndex(act) {
140
128
  if (act.index === undefined)
141
129
  throw new Error(`act ${act.kind} is missing a stable index`);
@@ -158,26 +146,13 @@ export function currentHold(acts) {
158
146
  export function publicActs(acts) {
159
147
  return acts.filter((act) => act.kind === 'say' || act.kind === 'done');
160
148
  }
161
- export function roomChangeActs(acts) {
162
- return acts.filter((act) => act.kind !== 'say' && act.kind !== 'read');
163
- }
164
- export function throttleDelayMs(doc, at) {
165
- const limit = doc.throttlePerMinute;
166
- if (limit === undefined)
167
- return 0;
168
- const recent = foldedState(doc).throttleActivityAts.filter((eventAt) => at - eventAt < THROTTLE_WINDOW_MS).sort((a, b) => a - b);
169
- if (recent.length < limit)
170
- return 0;
171
- const releaseAt = recent[recent.length - limit] + THROTTLE_WINDOW_MS;
172
- return Math.max(1, releaseAt - at);
173
- }
174
149
  function canonicalRuntimeName(doc, name) {
175
150
  return resolveRosterName(doc, name) ?? name;
176
151
  }
177
- export function advanceCursor(doc, name, index, source = 'watch', updatedAt = Date.now()) {
178
- return touchPresenceCursor(doc, name, updatedAt, source, index);
152
+ export function advanceCursor(doc, name, index, updatedAt = Date.now()) {
153
+ return touchPresenceCursor(doc, name, updatedAt, index);
179
154
  }
180
- export function touchPresenceCursor(doc, name, at, source, consumedThroughIndex) {
155
+ export function touchPresenceCursor(doc, name, at, consumedThroughIndex) {
181
156
  if (!Number.isFinite(at))
182
157
  return false;
183
158
  if (consumedThroughIndex !== undefined && (!Number.isInteger(consumedThroughIndex) || consumedThroughIndex < 0))
@@ -188,63 +163,31 @@ export function touchPresenceCursor(doc, name, at, source, consumedThroughIndex)
188
163
  ? (current?.consumedThroughIndex ?? readCursor(doc, key))
189
164
  : Math.max(current?.consumedThroughIndex ?? -1, consumedThroughIndex);
190
165
  const updatedAt = current === undefined ? at : Math.max(current.updatedAt, at);
191
- if (current?.consumedThroughIndex === nextIndex && current.updatedAt === updatedAt && current.source === source)
166
+ if (current?.consumedThroughIndex === nextIndex && current.updatedAt === updatedAt)
192
167
  return false;
193
- doc.runtime.cursors[key] = { consumedThroughIndex: nextIndex, updatedAt, source };
168
+ doc.runtime.cursors[key] = { consumedThroughIndex: nextIndex, updatedAt };
194
169
  return true;
195
170
  }
196
- /** The canonical delivery ledger is keyed by (recipient, stable act id). */
197
- export function deliveryReceipt(doc, name, actOrIndex) {
198
- const id = actId(actOrIndex);
199
- // Delivery planners and runtime writers pass roster-canonical names. Keeping
200
- // this a direct lookup is important: pending/status scans must stay linear in
201
- // activities rather than folding the whole square once per notification.
202
- return doc.runtime.mentionReceipts[name]?.[id];
203
- }
204
- export function isDeliveryDelivered(doc, name, actOrIndex) {
205
- return deliveryReceipt(doc, name, actOrIndex)?.status === 'delivered';
206
- }
207
- export function recordDeliveredDelivery(doc, name, actOrIndex, receipt) {
208
- const key = canonicalRuntimeName(doc, name);
209
- const id = actId(actOrIndex);
210
- const current = deliveryReceipt(doc, key, actOrIndex);
211
- if (current?.status === 'delivered')
212
- return false;
213
- const receipts = doc.runtime.mentionReceipts[key] ?? {};
214
- receipts[id] = { status: 'delivered', ...receipt };
215
- doc.runtime.mentionReceipts[key] = receipts;
216
- return true;
217
- }
218
- export function markDeliveredMention(doc, name, actOrIndex, at = Date.now()) {
219
- return recordDeliveredDelivery(doc, name, actOrIndex, { at });
220
- }
221
- export function mentionDeliveredStatus(doc, name, actOrIndex) {
222
- return isDeliveryDelivered(doc, name, actOrIndex) ? 'delivered' : undefined;
223
- }
224
- export function markDeliveredMentions(doc, name, delivered, at = Date.now()) {
225
- let changed = false;
226
- for (const item of delivered) {
227
- const act = item.act;
228
- if (act.kind !== 'say')
229
- continue;
230
- // A cursor says only where feed reading reached. The recipient/act receipt is
231
- // the delivery fact, so never create one for a broadcast or unrelated say.
232
- const directed = act.reach === 'bell' ||
233
- (act.reach !== undefined && sameName(act.reach.beside, name)) ||
234
- extractMentions(act.body).some((mention) => sameName(mention, name));
235
- if (!directed)
236
- continue;
237
- changed = markDeliveredMention(doc, name, actStableIndex(act), at) || changed;
238
- }
239
- return changed;
240
- }
241
- export function latestIndexedActIndex(items) {
242
- return items.reduce((max, item) => Math.max(max, item.index), -1);
171
+ export function latestActIndex(acts) {
172
+ return acts.reduce((max, act) => Math.max(max, act.index), -1);
243
173
  }
244
174
  export function freshWatchLease(doc, name, at = Date.now()) {
245
175
  const key = canonicalRuntimeName(doc, name);
246
- const lease = doc.runtime.leases[key];
176
+ const lease = watchLease(doc, key);
247
177
  if (lease === undefined || lease.expiresAt <= at || at - lease.heartbeatAt > WATCH_STALE_MS)
248
178
  return undefined;
249
179
  return lease;
250
180
  }
181
+ export function watchLease(doc, name) {
182
+ return doc.runtime.leases[canonicalRuntimeName(doc, name)];
183
+ }
184
+ export function writeWatchLease(doc, name, lease) {
185
+ doc.runtime.leases[canonicalRuntimeName(doc, name)] = lease;
186
+ }
187
+ export function removeWatchLease(doc, name, leaseId) {
188
+ const key = canonicalRuntimeName(doc, name);
189
+ if (doc.runtime.leases[key]?.leaseId !== leaseId)
190
+ return false;
191
+ delete doc.runtime.leases[key];
192
+ return true;
193
+ }