@astrosheep/square 0.3.5 → 0.3.7
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.
- package/codex-plugin/.codex-plugin/plugin.json +3 -2
- package/codex-plugin/hooks/hooks.json +3 -14
- package/dist/activity-feed.js +26 -18
- package/dist/activity.js +10 -10
- package/dist/artifact.js +138 -203
- package/dist/boundary-presentation.js +77 -0
- package/dist/claude-hook.js +4 -94
- package/dist/cli/context.js +7 -7
- package/dist/cli/maintenance-commands.js +10 -26
- package/dist/cli/meta-commands.js +3 -6
- package/dist/cli/observation-commands.js +48 -53
- package/dist/cli/program.js +4 -4
- package/dist/cli/registry.js +5 -5
- package/dist/cli/square-commands.js +27 -20
- package/dist/cmd/notify-once.js +23 -21
- package/dist/codex-hook.js +22 -0
- package/dist/compact.js +1 -1
- package/dist/decisions.js +61 -88
- package/dist/delivery-health.js +104 -210
- package/dist/delivery.js +68 -18
- package/dist/doctor.js +9 -8
- package/dist/harness-claude.js +38 -245
- package/dist/harness-codex.js +82 -616
- package/dist/harness-stage.js +36 -0
- package/dist/harness.js +3 -5
- package/dist/help.js +43 -35
- package/dist/inbox.js +12 -11
- package/dist/index.js +10 -121
- package/dist/list.js +1 -1
- package/dist/model.js +0 -6
- package/dist/notification-failures.js +54 -0
- package/dist/notifications.js +47 -62
- package/dist/paseo-delivery.js +160 -0
- package/dist/paseo-state.js +31 -0
- package/dist/paseo-timeline.js +58 -188
- package/dist/presentation.js +57 -64
- package/dist/presented.js +9 -8
- package/dist/registry.js +55 -45
- package/dist/runtime.js +27 -84
- package/dist/square-application.js +135 -130
- package/dist/square-core.js +3 -11
- package/dist/stream.js +27 -126
- package/dist/wake-sink.js +3 -214
- package/dist/watch.js +65 -122
- package/extensions/square-opencode.js +8 -73
- package/extensions/square-pi.js +8 -132
- package/guides/architect.md +3 -3
- package/guides/participant.md +25 -16
- package/package.json +2 -2
- package/skills/brainstorm/SKILL.md +25 -32
- package/skills/square/.claude-plugin/plugin.json +1 -1
- package/skills/square/SKILL.md +39 -107
- package/skills/square/hooks/hooks.json +2 -13
- package/skills/square-feedback/SKILL.md +4 -4
- package/dist/harness-lifecycle.js +0 -102
- package/dist/square-store.js +0 -111
- package/dist/terminal.js +0 -125
package/dist/wake-sink.js
CHANGED
|
@@ -1,219 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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';
|
|
13
|
-
import { homedir } from 'node:os';
|
|
14
|
-
import { loadSquare } from './artifact.js';
|
|
15
|
-
import { SquareError } from './model.js';
|
|
16
|
-
import { leaseOwnsNotification } from './delivery.js';
|
|
17
|
-
import { sessionInbox } from './inbox.js';
|
|
18
|
-
import { waitForPaseoToolBoundary } from './paseo-timeline.js';
|
|
19
|
-
import { hasPresentedAttention, presentOnce } from './presented.js';
|
|
20
|
-
import { lookupParticipant } from './registry.js';
|
|
21
|
-
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())));
|
|
37
|
-
}
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
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;
|
|
50
|
-
}
|
|
51
|
-
// ── helpers ──────────────────────────────────────────────────────
|
|
52
|
-
function previewBody(body) {
|
|
53
|
-
return body.length > 200 ? body.slice(0, 197) + '...' : body;
|
|
54
|
-
}
|
|
55
|
-
function wakeNowCommand(squarePath, recipient) {
|
|
56
|
-
return `square --as ${quoteShell(recipient)} --square-path ${quoteShell(squarePath)} catch --now`;
|
|
57
|
-
}
|
|
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');
|
|
66
|
-
}
|
|
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');
|
|
83
|
-
}
|
|
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'));
|
|
90
|
-
}
|
|
91
|
-
function sendPaseoPrompt(agentId, prompt) {
|
|
92
|
-
const result = spawnSync('paseo', ['send', agentId, '--prompt', prompt, '--no-wait'], {
|
|
93
|
-
stdio: 'ignore',
|
|
94
|
-
timeout: 5000,
|
|
95
|
-
});
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
export function sendPaseoWake({ agentId, prompt }) {
|
|
3
|
+
const result = spawnSync(process.env.SQUARE_PASEO_BIN || 'paseo', ['send', agentId, '--prompt', prompt, '--no-wait'], { stdio: 'ignore', timeout: 5000, env: process.env });
|
|
96
4
|
if (result.error)
|
|
97
5
|
throw result.error;
|
|
98
6
|
if (result.status !== 0)
|
|
99
7
|
throw new Error(`paseo send exited with ${result.status ?? 'no status'}`);
|
|
100
8
|
}
|
|
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
|
-
}
|
|
113
|
-
}
|
|
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
|
-
});
|
|
123
|
-
}
|
|
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'));
|
|
138
|
-
}
|
|
139
|
-
export async function dispatchPaseoWake(notification, ctx) {
|
|
140
|
-
const { item, recipient } = notification;
|
|
141
|
-
const bindings = lookupParticipant(ctx.squarePath, recipient);
|
|
142
|
-
const activeAgents = selectPaseoWakeAgents(ctx.squarePath, recipient);
|
|
143
|
-
if (activeAgents.length === 0)
|
|
144
|
-
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;
|
|
151
|
-
}
|
|
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))
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
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
|
-
}
|
|
187
|
-
}
|
|
188
|
-
if (hasNativeGuarantee(bindings, binding.ownerId)) {
|
|
189
|
-
sendPaseoPrompt(active.id, nativeWakePrompt(ctx.squarePath, recipient));
|
|
190
|
-
wokenOwners.add(binding.ownerId);
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
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.
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
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
|
-
}
|
package/dist/watch.js
CHANGED
|
@@ -1,23 +1,16 @@
|
|
|
1
1
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
2
2
|
import { loadSquare } from './artifact.js';
|
|
3
3
|
import { SquareError, nameKey, } from './model.js';
|
|
4
|
-
import {
|
|
5
|
-
import { SLEEP_MS, STALE_MS, WATCH_HEARTBEAT_MS, WATCH_STALE_MS, countSays, currentHold, doneNames, freshWatchLease, hasQuorum, inSquareCount,
|
|
6
|
-
import { withSquareLock, writeSquareDoc } from './square-
|
|
7
|
-
import { renderWatchForceTakeover, renderWatchAlreadyActive,
|
|
8
|
-
import { ackPeerDelta, filteredPeerActivities, filteredRoomChanges,
|
|
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 { withSquareLock, writeSquareDoc } from './square-application.js';
|
|
7
|
+
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
9
|
import { coreParticipants, resolveKnownName } from './decisions.js';
|
|
10
|
-
import { hasAutomaticDeliveryIdentity } from './registry.js';
|
|
10
|
+
import { hasAutomaticDeliveryIdentity, localParticipantOwner } from './registry.js';
|
|
11
11
|
import { execute } from './square-application.js';
|
|
12
|
-
/** Notification receipts are stronger than the public-feed cursor, which self activity may advance. */
|
|
13
12
|
function catchDelta(doc, name) {
|
|
14
|
-
|
|
15
|
-
const seen = new Set(items.map((item) => item.index));
|
|
16
|
-
for (const notification of deriveDeliveryModel(doc).pendingFor(name)) {
|
|
17
|
-
if (!seen.has(notification.item.index))
|
|
18
|
-
items.push(notification.item);
|
|
19
|
-
}
|
|
20
|
-
return items.sort((a, b) => a.index - b.index);
|
|
13
|
+
return deliveryDelta(doc, name);
|
|
21
14
|
}
|
|
22
15
|
function watchStatusExitCode(status) {
|
|
23
16
|
return status === 'capped' ? 1 : 0;
|
|
@@ -32,21 +25,22 @@ function leaseFilter(opts) {
|
|
|
32
25
|
};
|
|
33
26
|
return Object.keys(filter).length === 0 ? undefined : filter;
|
|
34
27
|
}
|
|
35
|
-
function setLease(doc, name, id, at, opts) {
|
|
28
|
+
function setLease(doc, name, id, at, opts, ownerId) {
|
|
36
29
|
const filter = leaseFilter(opts);
|
|
37
|
-
doc
|
|
30
|
+
writeWatchLease(doc, name, {
|
|
38
31
|
leaseId: id,
|
|
32
|
+
...(ownerId === undefined ? {} : { ownerId }),
|
|
39
33
|
heartbeatAt: at,
|
|
40
34
|
expiresAt: at + WATCH_STALE_MS,
|
|
41
35
|
...(filter ? { filter } : {}),
|
|
42
|
-
};
|
|
36
|
+
});
|
|
43
37
|
}
|
|
44
38
|
function sameLease(doc, name, id, at = nowMs()) {
|
|
45
39
|
return freshWatchLease(doc, name, at)?.leaseId === id;
|
|
46
40
|
}
|
|
47
41
|
function consumeDelta(doc, name, delta, delivered, at) {
|
|
48
42
|
const consumed = ackPeerDelta(doc, name, delta);
|
|
49
|
-
const receipts =
|
|
43
|
+
const receipts = markDeliveredNotifications(doc, name, delivered, at);
|
|
50
44
|
return consumed || receipts;
|
|
51
45
|
}
|
|
52
46
|
function watchOutputResult(squarePath, doc, name, delta, opts = {}) {
|
|
@@ -69,7 +63,7 @@ function loadPresence(squarePath) {
|
|
|
69
63
|
try {
|
|
70
64
|
const doc = loadSquare(squarePath);
|
|
71
65
|
const now = nowMs();
|
|
72
|
-
return { participants: coreParticipants(doc, now)
|
|
66
|
+
return { participants: coreParticipants(doc, now), now };
|
|
73
67
|
}
|
|
74
68
|
catch {
|
|
75
69
|
return undefined;
|
|
@@ -94,18 +88,54 @@ function writeWatchOutput(squarePath, name, stdout, status, idleMs) {
|
|
|
94
88
|
const fallback = showCatchHint
|
|
95
89
|
? `» ${participantCommandPrefix(squarePath, name)} catch --idle 30m\n stay available for new activity`
|
|
96
90
|
: '';
|
|
97
|
-
process.stdout.write(
|
|
91
|
+
process.stdout.write(withPathOutput(squarePath, [stdout.trimEnd(), fallback].filter(Boolean).join('\n\n').trimEnd(), headerOpts));
|
|
92
|
+
}
|
|
93
|
+
function writeWatchTerminal(squarePath, name, status, idleMs) {
|
|
94
|
+
const presence = loadPresence(squarePath);
|
|
95
|
+
process.stdout.write(withPathOutput(squarePath, renderWatchStatus({
|
|
96
|
+
status,
|
|
97
|
+
squarePath,
|
|
98
|
+
name,
|
|
99
|
+
...(idleMs === undefined ? {} : { idleMs }),
|
|
100
|
+
presence,
|
|
101
|
+
showCatchHint: !hasAutomaticDeliveryIdentity(),
|
|
102
|
+
}), { participantCount: loadHeaderCount(squarePath) }));
|
|
103
|
+
}
|
|
104
|
+
function writeWatchReplaced(squarePath, name) {
|
|
105
|
+
process.stdout.write(withPathOutput(squarePath, renderWatchReplaced({ squarePath, name }), { participantCount: loadHeaderCount(squarePath) }));
|
|
106
|
+
}
|
|
107
|
+
async function finishWatchResult(squarePath, name, result, leaseId, idleMs) {
|
|
108
|
+
if (result.type === 'output') {
|
|
109
|
+
await endWatch(squarePath, name, leaseId);
|
|
110
|
+
writeWatchOutput(squarePath, name, result.stdout, result.status);
|
|
111
|
+
process.exitCode = watchStatusExitCode(result.status);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
if (result.type === 'terminal') {
|
|
115
|
+
await endWatch(squarePath, name, leaseId);
|
|
116
|
+
writeWatchTerminal(squarePath, name, result.status, idleMs);
|
|
117
|
+
process.exitCode = watchStatusExitCode(result.status);
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
if (result.type === 'replaced') {
|
|
121
|
+
writeWatchReplaced(squarePath, name);
|
|
122
|
+
process.exitCode = 0;
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
98
126
|
}
|
|
99
127
|
async function beginWatch(squarePath, name, opts) {
|
|
100
128
|
const at = nowMs();
|
|
101
129
|
const id = leaseId();
|
|
130
|
+
const ownerId = localParticipantOwner(squarePath, name);
|
|
102
131
|
const committed = await execute(squarePath, {
|
|
103
132
|
type: 'lease',
|
|
104
133
|
name,
|
|
105
134
|
leaseId: id,
|
|
135
|
+
...(ownerId === undefined ? {} : { ownerId }),
|
|
106
136
|
at,
|
|
107
137
|
expiresAt: at + WATCH_STALE_MS,
|
|
108
|
-
force: opts.
|
|
138
|
+
force: opts.replace,
|
|
109
139
|
filter: leaseFilter(opts),
|
|
110
140
|
});
|
|
111
141
|
if (committed.result.type === 'active')
|
|
@@ -121,7 +151,7 @@ function installWatchInterruptHandler(squarePath, name, currentLeaseId) {
|
|
|
121
151
|
const onInterrupt = () => {
|
|
122
152
|
void (async () => {
|
|
123
153
|
await endWatch(squarePath, name, currentLeaseId());
|
|
124
|
-
process.stdout.write(withPathOutput(squarePath,
|
|
154
|
+
process.stdout.write(withPathOutput(squarePath, '✕ catch stopped'));
|
|
125
155
|
process.exit(130);
|
|
126
156
|
})().catch((error) => {
|
|
127
157
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
@@ -147,7 +177,7 @@ async function cmdWatchNow(squarePath, name, opts) {
|
|
|
147
177
|
const result = await withSquareLock(squarePath, () => {
|
|
148
178
|
const doc = loadSquare(squarePath);
|
|
149
179
|
const at = nowMs();
|
|
150
|
-
const touched = touchPresenceCursor(doc, name, at
|
|
180
|
+
const touched = touchPresenceCursor(doc, name, at);
|
|
151
181
|
const delta = catchDelta(doc, name);
|
|
152
182
|
const peerPublic = peerPublicActs(delta, name);
|
|
153
183
|
const roomChanges = peerRoomChanges(delta, name);
|
|
@@ -175,25 +205,7 @@ async function cmdWatchNow(squarePath, name, opts) {
|
|
|
175
205
|
return { type: 'terminal', status };
|
|
176
206
|
return { type: 'terminal', status: 'empty-now' };
|
|
177
207
|
});
|
|
178
|
-
|
|
179
|
-
writeWatchOutput(squarePath, name, result.stdout, result.status);
|
|
180
|
-
process.exitCode = watchStatusExitCode(result.status);
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
if (result.type === 'terminal') {
|
|
184
|
-
const presence = loadPresence(squarePath);
|
|
185
|
-
process.stdout.write(withPathOutput(squarePath, renderWatchStatus({
|
|
186
|
-
status: result.status,
|
|
187
|
-
squarePath,
|
|
188
|
-
name,
|
|
189
|
-
presence,
|
|
190
|
-
showCatchHint: !hasAutomaticDeliveryIdentity(),
|
|
191
|
-
}), {
|
|
192
|
-
participantCount: loadHeaderCount(squarePath),
|
|
193
|
-
}));
|
|
194
|
-
process.exitCode = watchStatusExitCode(result.status);
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
208
|
+
await finishWatchResult(squarePath, name, result, undefined);
|
|
197
209
|
}
|
|
198
210
|
export async function cmdWatch(squarePath, name, opts) {
|
|
199
211
|
let initialDoc;
|
|
@@ -214,14 +226,11 @@ export async function cmdWatch(squarePath, name, opts) {
|
|
|
214
226
|
throw err;
|
|
215
227
|
}
|
|
216
228
|
if (opts.now) {
|
|
217
|
-
if (opts.activityCount !== 1)
|
|
218
|
-
process.stderr.write('--count is ignored with --now\n');
|
|
219
229
|
await cmdWatchNow(squarePath, name, opts);
|
|
220
230
|
return;
|
|
221
231
|
}
|
|
222
232
|
const start = await beginWatch(squarePath, name, opts);
|
|
223
233
|
if (start.type === 'active') {
|
|
224
|
-
const presence = loadPresence(squarePath);
|
|
225
234
|
process.stdout.write(withPathOutput(squarePath, renderWatchAlreadyActive({ squarePath, name }), { participantCount: loadHeaderCount(squarePath) }));
|
|
226
235
|
process.exit(1);
|
|
227
236
|
}
|
|
@@ -229,7 +238,6 @@ export async function cmdWatch(squarePath, name, opts) {
|
|
|
229
238
|
let currentLeaseId = start.leaseId;
|
|
230
239
|
let nextHeartbeatAt = start.heartbeatAt + WATCH_HEARTBEAT_MS;
|
|
231
240
|
if (start.replaced) {
|
|
232
|
-
const presence = loadPresence(squarePath);
|
|
233
241
|
process.stdout.write(withPathOutput(squarePath, renderWatchForceTakeover({ squarePath, name }), { participantCount: loadHeaderCount(squarePath) }));
|
|
234
242
|
}
|
|
235
243
|
const idleMs = opts.idleMs ?? STALE_MS;
|
|
@@ -239,12 +247,13 @@ export async function cmdWatch(squarePath, name, opts) {
|
|
|
239
247
|
const result = await withSquareLock(squarePath, () => {
|
|
240
248
|
const doc = loadSquare(squarePath);
|
|
241
249
|
const at = nowMs();
|
|
242
|
-
|
|
250
|
+
const lease = freshWatchLease(doc, name, at);
|
|
251
|
+
if (lease === undefined || lease.leaseId !== currentLeaseId)
|
|
243
252
|
return { type: 'replaced' };
|
|
244
253
|
let mutated = false;
|
|
245
254
|
if (at >= nextHeartbeatAt) {
|
|
246
|
-
setLease(doc, name, currentLeaseId, at, opts);
|
|
247
|
-
mutated = touchPresenceCursor(doc, name, at
|
|
255
|
+
setLease(doc, name, currentLeaseId, at, opts, lease.ownerId);
|
|
256
|
+
mutated = touchPresenceCursor(doc, name, at) || mutated;
|
|
248
257
|
nextHeartbeatAt = at + WATCH_HEARTBEAT_MS;
|
|
249
258
|
mutated = true;
|
|
250
259
|
}
|
|
@@ -263,7 +272,7 @@ export async function cmdWatch(squarePath, name, opts) {
|
|
|
263
272
|
}
|
|
264
273
|
if (hasDeliverable &&
|
|
265
274
|
hasFilteredDeliverable &&
|
|
266
|
-
(filteredActivities.length
|
|
275
|
+
(filteredActivities.length > 0 || matchingRoomChanges.length > 0 || status !== undefined)) {
|
|
267
276
|
return watchOutputResult(squarePath, doc, name, delta, {
|
|
268
277
|
participants: opts.participants,
|
|
269
278
|
mention: opts.mention,
|
|
@@ -281,49 +290,12 @@ export async function cmdWatch(squarePath, name, opts) {
|
|
|
281
290
|
writeSquareDoc(squarePath, doc);
|
|
282
291
|
return { type: 'sleep' };
|
|
283
292
|
});
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (opts.follow === true && status === undefined) {
|
|
288
|
-
writeWatchOutput(squarePath, name, result.stdout);
|
|
289
|
-
staleSince = nowMs();
|
|
290
|
-
break;
|
|
291
|
-
}
|
|
292
|
-
await endWatch(squarePath, name, currentLeaseId);
|
|
293
|
-
currentLeaseId = undefined;
|
|
294
|
-
writeWatchOutput(squarePath, name, result.stdout, status);
|
|
295
|
-
process.exitCode = watchStatusExitCode(status);
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
case 'terminal':
|
|
299
|
-
await endWatch(squarePath, name, currentLeaseId);
|
|
300
|
-
currentLeaseId = undefined;
|
|
301
|
-
const presence = loadPresence(squarePath);
|
|
302
|
-
process.stdout.write(withPathOutput(squarePath, renderWatchStatus({
|
|
303
|
-
status: result.status,
|
|
304
|
-
squarePath,
|
|
305
|
-
name,
|
|
306
|
-
presence,
|
|
307
|
-
showCatchHint: !hasAutomaticDeliveryIdentity(),
|
|
308
|
-
}), {
|
|
309
|
-
participantCount: loadHeaderCount(squarePath),
|
|
310
|
-
}));
|
|
311
|
-
process.exitCode = watchStatusExitCode(result.status);
|
|
312
|
-
return;
|
|
313
|
-
case 'replaced':
|
|
314
|
-
currentLeaseId = undefined;
|
|
315
|
-
{
|
|
316
|
-
const presence = loadPresence(squarePath);
|
|
317
|
-
process.stdout.write(withPathOutput(squarePath, renderWatchReplaced({ squarePath, name }), { participantCount: loadHeaderCount(squarePath) }));
|
|
318
|
-
}
|
|
319
|
-
process.exitCode = 0;
|
|
320
|
-
return;
|
|
321
|
-
case 'sleep':
|
|
322
|
-
break;
|
|
323
|
-
case 'held':
|
|
324
|
-
staleSince = nowMs();
|
|
325
|
-
break;
|
|
293
|
+
if (await finishWatchResult(squarePath, name, result, currentLeaseId)) {
|
|
294
|
+
currentLeaseId = undefined;
|
|
295
|
+
return;
|
|
326
296
|
}
|
|
297
|
+
if (result.type === 'held')
|
|
298
|
+
staleSince = nowMs();
|
|
327
299
|
if (nowMs() - staleSince >= idleMs) {
|
|
328
300
|
const result = await withSquareLock(squarePath, () => {
|
|
329
301
|
const doc = loadSquare(squarePath);
|
|
@@ -341,37 +313,8 @@ export async function cmdWatch(squarePath, name, opts) {
|
|
|
341
313
|
}
|
|
342
314
|
return { type: 'terminal', status: 'stale' };
|
|
343
315
|
});
|
|
344
|
-
if (result
|
|
345
|
-
currentLeaseId = undefined;
|
|
346
|
-
{
|
|
347
|
-
const presence = loadPresence(squarePath);
|
|
348
|
-
process.stdout.write(withPathOutput(squarePath, renderWatchReplaced({ squarePath, name }), { participantCount: loadHeaderCount(squarePath) }));
|
|
349
|
-
}
|
|
350
|
-
process.exitCode = 0;
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
if (result.type === 'output') {
|
|
354
|
-
await endWatch(squarePath, name, currentLeaseId);
|
|
355
|
-
currentLeaseId = undefined;
|
|
356
|
-
writeWatchOutput(squarePath, name, result.stdout, result.status);
|
|
357
|
-
process.exitCode = 0;
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
if (result.type === 'terminal') {
|
|
361
|
-
await endWatch(squarePath, name, currentLeaseId);
|
|
316
|
+
if (await finishWatchResult(squarePath, name, result, currentLeaseId, idleMs)) {
|
|
362
317
|
currentLeaseId = undefined;
|
|
363
|
-
const presence = loadPresence(squarePath);
|
|
364
|
-
process.stdout.write(withPathOutput(squarePath, renderWatchStatus({
|
|
365
|
-
status: result.status,
|
|
366
|
-
squarePath,
|
|
367
|
-
name,
|
|
368
|
-
idleMs,
|
|
369
|
-
presence,
|
|
370
|
-
showCatchHint: !hasAutomaticDeliveryIdentity(),
|
|
371
|
-
}), {
|
|
372
|
-
participantCount: loadHeaderCount(squarePath),
|
|
373
|
-
}));
|
|
374
|
-
process.exitCode = watchStatusExitCode(result.status);
|
|
375
318
|
return;
|
|
376
319
|
}
|
|
377
320
|
}
|
|
@@ -1,87 +1,22 @@
|
|
|
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();
|
|
1
|
+
import { presentPendingAtBoundary } from '../dist/boundary-presentation.js';
|
|
27
2
|
|
|
3
|
+
export default async function squareOpenCodePlugin() {
|
|
28
4
|
return {
|
|
29
5
|
'shell.env': async (input, output) => {
|
|
30
6
|
if (input.sessionID) output.env.OPENCODE_SESSION_ID = input.sessionID;
|
|
31
7
|
},
|
|
32
8
|
|
|
33
|
-
'
|
|
34
|
-
if (!input.sessionID) return;
|
|
9
|
+
'tool.execute.after': async (input, output) => {
|
|
35
10
|
try {
|
|
36
|
-
|
|
37
|
-
presentOnce(
|
|
11
|
+
presentPendingAtBoundary(
|
|
38
12
|
input.sessionID,
|
|
39
|
-
(
|
|
40
|
-
|
|
13
|
+
(context) => {
|
|
14
|
+
output.output = `${output.output}${output.output === '' ? '' : '\n\n'}${context}`;
|
|
15
|
+
}
|
|
41
16
|
);
|
|
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
17
|
} catch {
|
|
79
|
-
//
|
|
18
|
+
// A failed admission remains available at a later boundary.
|
|
80
19
|
}
|
|
81
20
|
},
|
|
82
|
-
|
|
83
|
-
dispose: async () => {
|
|
84
|
-
handledAtIdle.clear();
|
|
85
|
-
},
|
|
86
21
|
};
|
|
87
22
|
}
|