@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/wake-sink.js CHANGED
@@ -1,219 +1,165 @@
1
- /**
2
- * wake-sink.ts — shared paseo wake dispatch.
3
- *
4
- * This is the single canonical implementation of "wake a paseo agent
5
- * when they receive an undelivered @mention or --bell in a square."
6
- *
7
- * The detached one-shot notification worker uses this module after the
8
- * natural-delivery grace period. The WakeSink is stateless; the worker owns
9
- * receipt checks, timing, and failure isolation.
10
- */
11
- import { execSync, spawnSync } from 'node:child_process';
12
- import { setTimeout as sleep } from 'node:timers/promises';
1
+ import { execFileSync, spawnSync } from 'node:child_process';
13
2
  import { homedir } from 'node:os';
3
+ import { setTimeout as sleep } from 'node:timers/promises';
14
4
  import { loadSquare } from './artifact.js';
15
- import { SquareError } from './model.js';
16
- import { leaseOwnsNotification } from './delivery.js';
5
+ import { leaseOwnsNotification, isDeliveryDelivered } from './delivery.js';
17
6
  import { sessionInbox } from './inbox.js';
18
- import { waitForPaseoToolBoundary } from './paseo-timeline.js';
19
- import { hasPresentedAttention, presentOnce } from './presented.js';
7
+ import { hasPresentedForOwner, presentOnce } from './presented.js';
20
8
  import { lookupParticipant } from './registry.js';
21
9
  import { quoteShell } from './presentation.js';
22
- import { isDeliveryDelivered, resolveRosterName } from './runtime.js';
23
- async function waitForCatchDelivery(squarePath, recipient, notification, ownerId) {
24
- const deadline = Date.now() + 180_000;
25
- while (Date.now() < deadline) {
26
- const doc = loadSquare(squarePath);
27
- const known = resolveRosterName(doc, recipient) ?? recipient;
28
- if (isDeliveryDelivered(doc, known, notification.item.index))
29
- return true;
30
- const lease = lookupParticipant(squarePath, known).find((binding) => binding.ownerId === ownerId);
31
- const activeLease = lease === undefined
32
- ? undefined
33
- : sessionInbox(lease.sessionId).find((membership) => membership.name === known)?.catchLease;
34
- if (activeLease === undefined || activeLease.expiresAt <= Date.now())
35
- return false;
36
- await sleep(Math.min(250, Math.max(1, activeLease.expiresAt - Date.now())));
10
+ import { isCurrentlyJoined, resolveRosterName } from './runtime.js';
11
+ import { waitForPaseoToolBoundary } from './paseo-timeline.js';
12
+ export class PaseoWakeError extends Error {
13
+ diagnostic;
14
+ constructor(message, diagnostic) {
15
+ super(message);
16
+ this.diagnostic = diagnostic;
17
+ this.name = 'PaseoWakeError';
37
18
  }
38
- return false;
39
19
  }
40
- const DEFAULT_PASEO_WAKE_TIMEOUT_MS = 90_000;
41
- function paseoWakeTimeoutMs() {
42
- const raw = process.env['SQUARE_PASEO_WAKE_TIMEOUT_MS'];
43
- if (raw === undefined)
44
- return DEFAULT_PASEO_WAKE_TIMEOUT_MS;
45
- const value = Number.parseInt(raw, 10);
46
- if (!Number.isFinite(value) || value <= 0) {
47
- throw new SquareError('invalid_args', 'Invalid SQUARE_PASEO_WAKE_TIMEOUT_MS: expected a positive integer.');
48
- }
49
- return value;
20
+ function endpoint() {
21
+ return process.env.SQUARE_PASEO_WS_URL ?? process.env.PASEO_LISTEN ?? '127.0.0.1:6767';
50
22
  }
51
- // ── helpers ──────────────────────────────────────────────────────
52
- function previewBody(body) {
53
- return body.length > 200 ? body.slice(0, 197) + '...' : body;
23
+ function diagnostic(phase, ownership, code) {
24
+ return {
25
+ phase,
26
+ code,
27
+ command: phase === 'discovery' ? 'paseo ls --json' : 'paseo send <agent-id> --prompt <prompt> --no-wait',
28
+ endpoint: endpoint(),
29
+ paseoAgentIds: ownership.map((item) => item.agentId),
30
+ ownerIds: [...new Set(ownership.map((item) => item.ownerId))],
31
+ passwordPresent: Boolean(process.env.PASEO_PASSWORD),
32
+ };
54
33
  }
55
- function wakeNowCommand(squarePath, recipient) {
56
- return `square --as ${quoteShell(recipient)} --square-path ${quoteShell(squarePath)} catch --now`;
34
+ function native(binding) {
35
+ return ['claude-code', 'codex', 'opencode', 'pi'].includes(binding.channel);
57
36
  }
58
- function nativeWakePrompt(squarePath, recipient) {
59
- return [
60
- '<system-reminder source="square">',
61
- `Square activity is waiting for @${recipient} in ${squarePath}.`,
62
- 'The native adapter will present it at the next boundary. If it does not:',
63
- `\`${wakeNowCommand(squarePath, recipient)}\``,
64
- '</system-reminder>',
65
- ].join('\n');
37
+ export function paseoOwnershipSnapshot(bindings) {
38
+ const out = new Map();
39
+ for (const binding of bindings) {
40
+ if (!binding.paseoAgentId)
41
+ continue;
42
+ const owner = bindings.filter((item) => item.ownerId === binding.ownerId);
43
+ out.set(`${binding.ownerId}\0${binding.paseoAgentId}`, {
44
+ agentId: binding.paseoAgentId,
45
+ ownerId: binding.ownerId,
46
+ sessionId: owner.find(native)?.sessionId ?? binding.sessionId,
47
+ nativeGuarantee: owner.some(native),
48
+ });
49
+ }
50
+ return [...out.values()];
66
51
  }
67
- function fullWakePrompt(notification, squarePath) {
68
- const { item, recipient } = notification;
69
- const home = homedir();
70
- const displayPath = squarePath.startsWith(home) ? `~${squarePath.slice(home.length)}` : squarePath;
71
- return [
72
- '<system-reminder source="square">',
73
- `Mentioned by @${item.act.actor} in \`${displayPath}\``,
74
- '',
75
- `> ${previewBody(item.act.body).replace(/\n/g, '\n> ')}`,
76
- '',
77
- 'To catch up and keep presence current:',
78
- `\`${wakeNowCommand(squarePath, recipient)}\``,
79
- '',
80
- '*(Async notification from Square.)*',
81
- '</system-reminder>',
82
- ].join('\n');
52
+ export function selectPaseoWakeAgents(ownership, agents) {
53
+ const ids = new Set(ownership.map((item) => item.agentId));
54
+ return agents.filter((agent) => ids.has(agent.id) && (agent.status === 'idle' || agent.status === 'running'));
83
55
  }
84
- function hasNativeGuarantee(bindings, ownerId) {
85
- return bindings.some((binding) => binding.ownerId === ownerId &&
86
- (binding.channel === 'claude-code' ||
87
- binding.channel === 'codex' ||
88
- binding.channel === 'opencode' ||
89
- binding.channel === 'pi'));
56
+ export function discoverPaseoAgents(timeoutMs = 5000) {
57
+ try {
58
+ const raw = execFileSync(process.env.SQUARE_PASEO_BIN || 'paseo', ['ls', '--json'], { encoding: 'utf8', timeout: timeoutMs, stdio: ['ignore', 'pipe', 'pipe'] });
59
+ const parsed = JSON.parse(raw);
60
+ const agents = Array.isArray(parsed) ? parsed : parsed?.agents;
61
+ if (!Array.isArray(agents))
62
+ return { agents: [], error: 'Paseo returned malformed agent inventory.' };
63
+ return { agents: agents.filter((item) => item !== null && typeof item === 'object' && typeof item.id === 'string' && typeof item.status === 'string') };
64
+ }
65
+ catch (error) {
66
+ return { agents: [], error: error instanceof Error ? error.message : String(error) };
67
+ }
90
68
  }
91
- function sendPaseoPrompt(agentId, prompt) {
92
- const result = spawnSync('paseo', ['send', agentId, '--prompt', prompt, '--no-wait'], {
93
- stdio: 'ignore',
94
- timeout: 5000,
95
- });
69
+ function send(agentId, prompt) {
70
+ const result = spawnSync(process.env.SQUARE_PASEO_BIN || 'paseo', ['send', agentId, '--prompt', prompt, '--no-wait'], { stdio: 'ignore', timeout: 5000, env: process.env });
96
71
  if (result.error)
97
72
  throw result.error;
98
73
  if (result.status !== 0)
99
74
  throw new Error(`paseo send exited with ${result.status ?? 'no status'}`);
100
75
  }
101
- // ── paseo agent discovery ────────────────────────────────────────
102
- /** List all paseo agents. Returns empty array if paseo is unavailable. */
103
- export function listPaseoAgents() {
104
- try {
105
- return JSON.parse(execSync('paseo ls --json', {
106
- encoding: 'utf8',
107
- stdio: ['ignore', 'pipe', 'ignore'],
108
- }));
109
- }
110
- catch {
111
- return [];
112
- }
76
+ function catchCommand(squarePath, recipient) {
77
+ return `square --as ${quoteShell(recipient)} --square-path ${quoteShell(squarePath)} catch --now`;
113
78
  }
114
- /**
115
- * Wait for the tool calls that are running now to finish. Calls that start
116
- * afterward are deliberately ignored: the transitional wake policy allows
117
- * `paseo send` to replace the next call.
118
- */
119
- export async function waitForToolEnd(agentId, timeoutMs) {
120
- return waitForPaseoToolBoundary(agentId, {
121
- timeoutMs: timeoutMs ?? paseoWakeTimeoutMs(),
122
- });
79
+ function prompt(notification, squarePath, nativeWake) {
80
+ const display = squarePath.startsWith(homedir()) ? `~${squarePath.slice(homedir().length)}` : squarePath;
81
+ const body = notification.item.body.length > 200 ? `${notification.item.body.slice(0, 197)}...` : notification.item.body;
82
+ return [
83
+ '<system-reminder source="square">',
84
+ `${notification.route === 'bell' ? 'Bell' : notification.route === 'beside' ? 'Beside' : 'Mention'} from @${notification.item.actor} in \`${display}\``,
85
+ nativeWake ? 'The native adapter will present it at the next boundary.' : `> ${body.replace(/\n/g, '\n> ')}`,
86
+ `\`${catchCommand(squarePath, notification.recipient)}\``,
87
+ '</system-reminder>',
88
+ ].join('\n');
123
89
  }
124
- // ── paseo wake dispatch ──────────────────────────────────────────
125
- /**
126
- * Dispatch a paseo send wake prompt for a single notification.
127
- *
128
- * Resolves exact Paseo agent ids from the machine-local participant registry.
129
- * Idle agents receive the wake immediately. Running agents wait for the
130
- * current tool call boundary; the following `paseo send` may replace the next
131
- * call by design.
132
- */
133
- export function selectPaseoWakeAgents(squarePath, recipient, agents = listPaseoAgents()) {
134
- const ids = new Set(lookupParticipant(squarePath, recipient)
135
- .map((binding) => binding.paseoAgentId)
136
- .filter((id) => id !== undefined));
137
- return agents.filter((agent) => ids.has(agent.id) && (agent.status === 'running' || agent.status === 'idle'));
90
+ async function waitForCatch(squarePath, recipient, notification, ownerId) {
91
+ const deadline = Date.now() + 180_000;
92
+ while (Date.now() < deadline) {
93
+ const doc = loadSquare(squarePath);
94
+ if (isDeliveryDelivered(doc, recipient, notification.item.index))
95
+ return true;
96
+ const binding = lookupParticipant(squarePath, recipient).find((item) => item.ownerId === ownerId);
97
+ const lease = binding && sessionInbox(binding.sessionId).find((item) => item.name === recipient)?.catchLease;
98
+ if (!lease || lease.expiresAt <= Date.now())
99
+ return false;
100
+ await sleep(Math.min(250, lease.expiresAt - Date.now()));
101
+ }
102
+ return false;
138
103
  }
139
- export async function dispatchPaseoWake(notification, ctx) {
140
- const { item, recipient } = notification;
104
+ export async function dispatchPaseoNotification(notification, ctx) {
105
+ const initial = loadSquare(ctx.squarePath);
106
+ const recipient = resolveRosterName(initial, notification.recipient);
107
+ if (!recipient || !isCurrentlyJoined(initial.acts, recipient))
108
+ return;
141
109
  const bindings = lookupParticipant(ctx.squarePath, recipient);
142
- const activeAgents = selectPaseoWakeAgents(ctx.squarePath, recipient);
143
- if (activeAgents.length === 0)
110
+ const ownership = paseoOwnershipSnapshot(bindings);
111
+ if (ownership.length === 0)
144
112
  return;
145
- const wokenOwners = new Set();
146
- for (const active of activeAgents) {
147
- if (active.status === 'running') {
148
- const reachedBoundary = await waitForToolEnd(active.id);
149
- if (!reachedBoundary)
150
- continue;
113
+ const discovery = discoverPaseoAgents();
114
+ if (discovery.error && discovery.agents.length === 0) {
115
+ throw new PaseoWakeError(`Paseo unavailable: ${discovery.error}`, diagnostic('discovery', ownership, 'unavailable'));
116
+ }
117
+ const active = selectPaseoWakeAgents(ownership, discovery.agents);
118
+ if (active.length === 0) {
119
+ throw new PaseoWakeError('No registered Paseo agent is idle or running.', diagnostic('selection', ownership, 'not_active'));
120
+ }
121
+ let boundaryTimedOut = false;
122
+ for (const agent of active) {
123
+ const owner = ownership.find((item) => item.agentId === agent.id);
124
+ if (!owner || hasPresentedForOwner(owner.ownerId, ctx.squarePath, recipient, notification.item.index))
125
+ continue;
126
+ if (agent.status === 'running' && !(await waitForPaseoToolBoundary(agent.id))) {
127
+ boundaryTimedOut = true;
128
+ continue;
151
129
  }
152
- // Re-read after the tool wait: harness inject may have presented during the wait.
153
- try {
154
- const doc = loadSquare(ctx.squarePath);
155
- const known = resolveRosterName(doc, recipient) ?? recipient;
156
- if (isDeliveryDelivered(doc, known, item.index))
157
- return;
158
- if (hasPresentedAttention(ctx.squarePath, known, item.index))
130
+ const latest = loadSquare(ctx.squarePath);
131
+ if (!isCurrentlyJoined(latest.acts, recipient) || isDeliveryDelivered(latest, recipient, notification.item.index))
132
+ return;
133
+ const current = lookupParticipant(ctx.squarePath, recipient).find((item) => item.ownerId === owner.ownerId && item.paseoAgentId === owner.agentId);
134
+ if (!current)
135
+ continue;
136
+ const activeCatch = sessionInbox(current.sessionId).find((item) => item.name === recipient)?.catchLease;
137
+ if (activeCatch && leaseOwnsNotification(activeCatch, { actor: notification.item.actor, body: notification.item.body, route: notification.route, recipient })) {
138
+ if (await waitForCatch(ctx.squarePath, recipient, notification, owner.ownerId))
159
139
  return;
160
140
  }
161
- catch {
162
- // Receipt re-read failure fails open toward wake.
163
- }
164
- const binding = bindings.find((candidate) => candidate.paseoAgentId === active.id);
165
- if (binding === undefined || wokenOwners.has(binding.ownerId))
166
- continue;
167
- try {
168
- const catchMembership = sessionInbox(binding.sessionId).find((membership) => membership.name === (resolveRosterName(loadSquare(ctx.squarePath), recipient) ?? recipient) &&
169
- membership.catchLease !== undefined &&
170
- leaseOwnsNotification(membership.catchLease, {
171
- actor: item.act.actor,
172
- body: item.act.body,
173
- via: notification.via === 'bell' ? 'bell' : 'mention',
174
- }));
175
- if (catchMembership?.catchLease !== undefined) {
176
- if (await waitForCatchDelivery(ctx.squarePath, recipient, notification, binding.ownerId))
177
- return;
178
- const current = lookupParticipant(ctx.squarePath, recipient).find((candidate) => candidate.paseoAgentId === active.id);
179
- if (current?.ownerId !== binding.ownerId)
180
- continue;
181
- const doc = loadSquare(ctx.squarePath);
182
- const known = resolveRosterName(doc, recipient) ?? recipient;
183
- if (isDeliveryDelivered(doc, known, item.index) ||
184
- hasPresentedAttention(ctx.squarePath, known, item.index)) {
185
- return;
186
- }
141
+ if (owner.nativeGuarantee) {
142
+ try {
143
+ send(agent.id, prompt({ ...notification, recipient }, ctx.squarePath, true));
187
144
  }
188
- if (hasNativeGuarantee(bindings, binding.ownerId)) {
189
- sendPaseoPrompt(active.id, nativeWakePrompt(ctx.squarePath, recipient));
190
- wokenOwners.add(binding.ownerId);
191
- continue;
145
+ catch (error) {
146
+ throw new PaseoWakeError(error instanceof Error ? error.message : String(error), diagnostic('send', ownership, 'failed'));
192
147
  }
193
- presentOnce(binding.sessionId, (sessionId) => sessionInbox(sessionId)
194
- .map((membership) => ({
195
- ...membership,
196
- notifications: membership.notifications.filter((candidate) => candidate.actIndex === item.index),
197
- }))
198
- .filter((membership) => membership.notifications.length > 0), () => sendPaseoPrompt(active.id, fullWakePrompt(notification, ctx.squarePath)));
199
- wokenOwners.add(binding.ownerId);
200
- }
201
- catch {
202
- // Paseo unavailable: notification remains unpresented for a later retry.
148
+ return;
203
149
  }
150
+ presentOnce(current.sessionId, (id) => sessionInbox(id).map((item) => ({ ...item, notifications: item.notifications.filter((note) => note.actIndex === notification.item.index) })).filter((item) => item.notifications.length > 0), () => {
151
+ try {
152
+ send(agent.id, prompt({ ...notification, recipient }, ctx.squarePath, false));
153
+ }
154
+ catch (error) {
155
+ throw new PaseoWakeError(error instanceof Error ? error.message : String(error), diagnostic('send', ownership, 'failed'));
156
+ }
157
+ return true;
158
+ });
159
+ return;
204
160
  }
161
+ if (boundaryTimedOut)
162
+ throw new PaseoWakeError('Paseo did not reach the current tool boundary before the wake timeout.', diagnostic('boundary', ownership, 'timeout'));
205
163
  }
206
- // ── WakeSink factory ─────────────────────────────────────────────
207
- /** Create the default paseo WakeSink used by both sync and async paths. */
208
- export function paseoWakeSink() {
209
- return {
210
- name: 'paseo',
211
- dispatch: dispatchPaseoWake,
212
- };
213
- }
214
- /** Default sink list, respecting SQUARE_DISABLE_PASEO_WAKE. */
215
- export function defaultWakeSinks() {
216
- if (process.env.SQUARE_DISABLE_PASEO_WAKE === '1')
217
- return [];
218
- return [paseoWakeSink()];
219
- }
164
+ export function paseoWakeSink() { return { name: 'paseo', dispatch: dispatchPaseoNotification }; }
165
+ export function defaultWakeSinks() { return process.env.SQUARE_DISABLE_PASEO_WAKE === '1' ? [] : [paseoWakeSink()]; }