@astrosheep/square 0.3.6 → 0.3.8
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 +1 -1
- package/codex-plugin/hooks/hooks.json +3 -14
- package/dist/activity.js +1 -0
- package/dist/artifact.js +13 -2
- package/dist/boundary-presentation.js +77 -0
- package/dist/claude-hook.js +4 -118
- package/dist/cli/observation-commands.js +4 -1
- package/dist/cli/square-commands.js +12 -3
- package/dist/codex-hook.js +22 -0
- package/dist/decisions.js +8 -2
- package/dist/harness-claude.js +1 -0
- package/dist/harness-codex.js +36 -8
- package/dist/help.js +3 -3
- package/dist/index.js +1 -0
- package/dist/notifications.js +1 -1
- package/dist/paseo-delivery.js +160 -0
- package/dist/paseo-state.js +31 -0
- package/dist/presentation.js +2 -1
- package/dist/wake-sink.js +2 -159
- package/extensions/square-opencode.js +8 -73
- package/extensions/square-pi.js +4 -6
- package/package.json +1 -1
- package/skills/square/.claude-plugin/plugin.json +1 -1
- package/skills/square/hooks/hooks.json +2 -13
|
@@ -1,25 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"hooks": {
|
|
3
|
-
"
|
|
3
|
+
"PostToolUse": [
|
|
4
4
|
{
|
|
5
5
|
"hooks": [
|
|
6
6
|
{
|
|
7
7
|
"type": "command",
|
|
8
8
|
"command": "square codex-hook",
|
|
9
9
|
"timeout": 5,
|
|
10
|
-
"statusMessage": "square-codex-hook"
|
|
11
|
-
|
|
12
|
-
]
|
|
13
|
-
}
|
|
14
|
-
],
|
|
15
|
-
"Stop": [
|
|
16
|
-
{
|
|
17
|
-
"hooks": [
|
|
18
|
-
{
|
|
19
|
-
"type": "command",
|
|
20
|
-
"command": "square codex-hook",
|
|
21
|
-
"timeout": 5,
|
|
22
|
-
"statusMessage": "square-codex-hook"
|
|
10
|
+
"statusMessage": "square-codex-hook",
|
|
11
|
+
"additionalContextLimit": 1200
|
|
23
12
|
}
|
|
24
13
|
]
|
|
25
14
|
}
|
package/dist/activity.js
CHANGED
|
@@ -75,6 +75,7 @@ export async function cmdActivity(squarePath, name, activity, resolveBody, opts)
|
|
|
75
75
|
force,
|
|
76
76
|
now: nowMs(),
|
|
77
77
|
...(reach === undefined ? {} : { reach }),
|
|
78
|
+
...(opts.reply === undefined ? {} : { reply: opts.reply }),
|
|
78
79
|
});
|
|
79
80
|
const decision = committed.result;
|
|
80
81
|
const freshDoc = loadSquare(squarePath);
|
package/dist/artifact.js
CHANGED
|
@@ -43,6 +43,7 @@ function renderActMarker(act) {
|
|
|
43
43
|
actor: act.actor,
|
|
44
44
|
at: act.at,
|
|
45
45
|
...(act.kind === 'say' && act.reach !== undefined ? { reach: act.reach } : {}),
|
|
46
|
+
...(act.kind === 'say' && act.reply !== undefined ? { reply: act.reply } : {}),
|
|
46
47
|
};
|
|
47
48
|
return `${ACT_MARKER_PREFIX} ${JSON.stringify(marker)} -->`;
|
|
48
49
|
}
|
|
@@ -410,8 +411,15 @@ export function parseActMarker(line) {
|
|
|
410
411
|
...(typeof parsed.actor === 'string' ? { actor: parsed.actor } : {}),
|
|
411
412
|
...(typeof parsed.at === 'number' && Number.isFinite(parsed.at) ? { at: parsed.at } : {}),
|
|
412
413
|
...(parsed.reach !== undefined ? { reach: parseReach(parsed.reach) } : {}),
|
|
414
|
+
...(parsed.reply !== undefined ? { reply: parseReply(parsed.reply) } : {}),
|
|
413
415
|
};
|
|
414
416
|
}
|
|
417
|
+
function parseReply(value) {
|
|
418
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
|
|
419
|
+
throw new SquareError('invalid_args', 'Invalid square: malformed act reply metadata.');
|
|
420
|
+
}
|
|
421
|
+
return value;
|
|
422
|
+
}
|
|
415
423
|
function normalizeActMeta(marker, kind, actor, head) {
|
|
416
424
|
if (marker.kind === undefined || marker.actor === undefined) {
|
|
417
425
|
throw new SquareError('invalid_args', 'Invalid square: act marker is missing kind/actor metadata.');
|
|
@@ -428,7 +436,10 @@ function normalizeActMeta(marker, kind, actor, head) {
|
|
|
428
436
|
if (marker.at !== head.at) {
|
|
429
437
|
throw new SquareError('invalid_args', `Invalid square: act marker timestamp does not match ${kind} ${actor}.`);
|
|
430
438
|
}
|
|
431
|
-
|
|
439
|
+
if (marker.reply !== undefined && kind !== 'say') {
|
|
440
|
+
throw new SquareError('invalid_args', 'Invalid square: only say acts may reply to another activity.');
|
|
441
|
+
}
|
|
442
|
+
return { index: marker.index, ...(marker.reach !== undefined ? { reach: marker.reach } : {}), ...(marker.reply !== undefined ? { reply: marker.reply } : {}) };
|
|
432
443
|
}
|
|
433
444
|
export function activitiesSourceLines(text) {
|
|
434
445
|
const lines = text.split('\n');
|
|
@@ -479,7 +490,7 @@ function parseActBlock(blockLines) {
|
|
|
479
490
|
i++;
|
|
480
491
|
body = unquoteBody(blockLines.slice(i));
|
|
481
492
|
}
|
|
482
|
-
return { ...head, body, index: meta.index, ...(meta.reach !== undefined ? { reach: meta.reach } : {}) };
|
|
493
|
+
return { ...head, body, index: meta.index, ...(meta.reach !== undefined ? { reach: meta.reach } : {}), ...(meta.reply !== undefined ? { reply: meta.reply } : {}) };
|
|
483
494
|
}
|
|
484
495
|
function parseActs(text) {
|
|
485
496
|
const lines = activitiesSourceLines(text);
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { leaseOwnsNotification, notificationMessageId } from './delivery.js';
|
|
2
|
+
import { sessionInbox } from './inbox.js';
|
|
3
|
+
import { participantCommandPrefix } from './presentation.js';
|
|
4
|
+
import { presentOnce } from './presented.js';
|
|
5
|
+
const BODY_MAX = 200;
|
|
6
|
+
const CONTEXT_MAX = 1200;
|
|
7
|
+
function pendingCount(inbox) {
|
|
8
|
+
return inbox.reduce((total, membership) => total + membership.notifications.length, 0);
|
|
9
|
+
}
|
|
10
|
+
/** A fresh blocking catch owns only the notifications admitted by its filter. */
|
|
11
|
+
export function pendingAtBoundary(inbox) {
|
|
12
|
+
return inbox
|
|
13
|
+
.map((membership) => {
|
|
14
|
+
const lease = membership.catchLease;
|
|
15
|
+
if (lease === undefined)
|
|
16
|
+
return membership;
|
|
17
|
+
return {
|
|
18
|
+
...membership,
|
|
19
|
+
notifications: membership.notifications.filter((notification) => !leaseOwnsNotification(lease, { ...notification, recipient: membership.name })),
|
|
20
|
+
};
|
|
21
|
+
})
|
|
22
|
+
.filter((membership) => membership.notifications.length > 0);
|
|
23
|
+
}
|
|
24
|
+
function bodyPreview(body) {
|
|
25
|
+
const compact = body.replace(/\r\n/g, '\n');
|
|
26
|
+
if (compact.length <= BODY_MAX)
|
|
27
|
+
return compact;
|
|
28
|
+
return `${compact.slice(0, BODY_MAX).trimEnd()}\n… [truncated; run catch --now]`;
|
|
29
|
+
}
|
|
30
|
+
export function renderPendingAtBoundary(inbox) {
|
|
31
|
+
const count = pendingCount(inbox);
|
|
32
|
+
const noun = count === 1 ? 'notification' : 'notifications';
|
|
33
|
+
const header = `<system-reminder source="square">You have ${count} unread Square ${noun}.`;
|
|
34
|
+
const footer = [
|
|
35
|
+
'Ids are stable across boundaries. If you already acted on an id, do not repeat the action; still run catch --now to mark delivered.',
|
|
36
|
+
'Read and respond in the square when appropriate.</system-reminder>',
|
|
37
|
+
];
|
|
38
|
+
const queued = inbox.flatMap((membership) => membership.notifications.map((notification) => ({ membership, notification })));
|
|
39
|
+
const blocks = [];
|
|
40
|
+
let omitted = 0;
|
|
41
|
+
for (const [index, entry] of queued.entries()) {
|
|
42
|
+
const { membership, notification } = entry;
|
|
43
|
+
const command = `${participantCommandPrefix(membership.squarePath, membership.name)} catch --now`;
|
|
44
|
+
const id = notificationMessageId(membership.squarePath, notification.actIndex);
|
|
45
|
+
const block = [
|
|
46
|
+
`${id} · ${membership.squarePath}: @${membership.name} from @${notification.actor} (${notification.route})`,
|
|
47
|
+
bodyPreview(notification.body),
|
|
48
|
+
`Ack with: ${command}`,
|
|
49
|
+
].join('\n');
|
|
50
|
+
const omittedAfter = omitted + queued.length - index - 1;
|
|
51
|
+
const prospective = [
|
|
52
|
+
header,
|
|
53
|
+
...blocks,
|
|
54
|
+
block,
|
|
55
|
+
...(omittedAfter > 0
|
|
56
|
+
? [`… ${omittedAfter} unread ${omittedAfter === 1 ? 'notification' : 'notifications'} omitted. Run catch --now to receive them.`]
|
|
57
|
+
: []),
|
|
58
|
+
...footer,
|
|
59
|
+
].join('\n');
|
|
60
|
+
if (prospective.length > CONTEXT_MAX) {
|
|
61
|
+
omitted += 1;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
blocks.push(block);
|
|
65
|
+
}
|
|
66
|
+
return [
|
|
67
|
+
header,
|
|
68
|
+
...blocks,
|
|
69
|
+
...(omitted > 0
|
|
70
|
+
? [`… ${omitted} unread ${omitted === 1 ? 'notification' : 'notifications'} omitted. Run catch --now to receive them.`]
|
|
71
|
+
: []),
|
|
72
|
+
...footer,
|
|
73
|
+
].join('\n');
|
|
74
|
+
}
|
|
75
|
+
export function presentPendingAtBoundary(sessionId, present, lookup = sessionInbox, env = process.env) {
|
|
76
|
+
return presentOnce(sessionId, (id) => pendingAtBoundary(lookup(id)), (inbox) => present(renderPendingAtBoundary(inbox)), env);
|
|
77
|
+
}
|
package/dist/claude-hook.js
CHANGED
|
@@ -1,112 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { notificationMessageId } from './delivery.js';
|
|
1
|
+
import { presentPendingAtBoundary } from './boundary-presentation.js';
|
|
3
2
|
import { sessionInbox } from './inbox.js';
|
|
4
|
-
|
|
5
|
-
import { presentOnce } from './presented.js';
|
|
6
|
-
function pendingCount(inbox) {
|
|
7
|
-
return inbox.reduce((total, membership) => total + membership.notifications.length, 0);
|
|
8
|
-
}
|
|
9
|
-
const INJECT_BODY_MAX = 200;
|
|
10
|
-
const INJECT_TOTAL_MAX = 1200;
|
|
11
|
-
/** Let a fresh blocking catch own notifications it can deliver; hook injection remains the fallback. */
|
|
12
|
-
export function deferToActiveCatch(inbox) {
|
|
13
|
-
return inbox
|
|
14
|
-
.map((membership) => {
|
|
15
|
-
const lease = membership.catchLease;
|
|
16
|
-
if (lease === undefined)
|
|
17
|
-
return membership;
|
|
18
|
-
return {
|
|
19
|
-
...membership,
|
|
20
|
-
notifications: membership.notifications.filter((notification) => !leaseOwnsNotification(lease, { ...notification, recipient: membership.name })),
|
|
21
|
-
};
|
|
22
|
-
})
|
|
23
|
-
.filter((membership) => membership.notifications.length > 0);
|
|
24
|
-
}
|
|
25
|
-
function injectBodyPreview(body) {
|
|
26
|
-
const compact = body.replace(/\r\n/g, '\n');
|
|
27
|
-
if (compact.length <= INJECT_BODY_MAX)
|
|
28
|
-
return compact;
|
|
29
|
-
return `${compact.slice(0, INJECT_BODY_MAX).trimEnd()}\n… [truncated; run catch --now]`;
|
|
30
|
-
}
|
|
31
|
-
export function renderClaudeInboxContext(inbox) {
|
|
32
|
-
const count = pendingCount(inbox);
|
|
33
|
-
const noun = count === 1 ? 'notification' : 'notifications';
|
|
34
|
-
const header = `<system-reminder source="square">You have ${count} unread Square ${noun}.`;
|
|
35
|
-
const footer = [
|
|
36
|
-
// Body here is a cache only. Delivered is written solely by catch.
|
|
37
|
-
'Ids are stable across turns. If you already acted on an id, do not repeat the action; still run catch --now to mark delivered.',
|
|
38
|
-
'Read and respond in the square before finishing the current turn.</system-reminder>',
|
|
39
|
-
];
|
|
40
|
-
const queued = inbox.flatMap((membership) => membership.notifications.map((notification) => ({ membership, notification })));
|
|
41
|
-
const blocks = [];
|
|
42
|
-
let omitted = 0;
|
|
43
|
-
for (const [index, entry] of queued.entries()) {
|
|
44
|
-
const { membership, notification } = entry;
|
|
45
|
-
const command = `${participantCommandPrefix(membership.squarePath, membership.name)} catch --now`;
|
|
46
|
-
const id = notificationMessageId(membership.squarePath, notification.actIndex);
|
|
47
|
-
const block = [
|
|
48
|
-
`${id} · ${membership.squarePath}: @${membership.name} from @${notification.actor} (${notification.route})`,
|
|
49
|
-
injectBodyPreview(notification.body),
|
|
50
|
-
`Ack with: ${command}`,
|
|
51
|
-
].join('\n');
|
|
52
|
-
const omittedAfter = omitted + queued.length - index - 1;
|
|
53
|
-
const prospective = [
|
|
54
|
-
header,
|
|
55
|
-
...blocks,
|
|
56
|
-
block,
|
|
57
|
-
...(omittedAfter > 0
|
|
58
|
-
? [`… ${omittedAfter} unread ${omittedAfter === 1 ? 'notification' : 'notifications'} omitted. Run catch --now to receive them.`]
|
|
59
|
-
: []),
|
|
60
|
-
...footer,
|
|
61
|
-
].join('\n');
|
|
62
|
-
if (prospective.length > INJECT_TOTAL_MAX) {
|
|
63
|
-
omitted += 1;
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
blocks.push(block);
|
|
67
|
-
}
|
|
68
|
-
return [
|
|
69
|
-
header,
|
|
70
|
-
...blocks,
|
|
71
|
-
...(omitted > 0
|
|
72
|
-
? [`… ${omitted} unread ${omitted === 1 ? 'notification' : 'notifications'} omitted. Run catch --now to receive them.`]
|
|
73
|
-
: []),
|
|
74
|
-
...footer,
|
|
75
|
-
].join('\n');
|
|
76
|
-
}
|
|
77
|
-
function nativeHookResponse(input, lookup, env) {
|
|
3
|
+
export function claudeHookResponse(input, lookup = sessionInbox, env = process.env) {
|
|
78
4
|
if (typeof input.session_id !== 'string' || input.session_id === '')
|
|
79
5
|
return undefined;
|
|
80
|
-
if (input.hook_event_name !== '
|
|
6
|
+
if (input.hook_event_name !== 'PostToolBatch')
|
|
81
7
|
return undefined;
|
|
82
|
-
}
|
|
83
|
-
if (input.hook_event_name === 'Stop' && input.stop_hook_active === true)
|
|
84
|
-
return undefined;
|
|
85
|
-
// Delivery membership is only claimed by explicit participant actions (join/express/catch/...).
|
|
86
|
-
// Inherited PASEO_AGENT_ID proves process ancestry, not conversational ownership.
|
|
87
|
-
// Stop is the guaranteed "don't leave while undelivered" nudge; it does not consume presentation.
|
|
88
|
-
if (input.hook_event_name === 'Stop') {
|
|
89
|
-
const pending = lookup(input.session_id).filter((membership) => membership.notifications.length > 0);
|
|
90
|
-
if (pendingCount(pending) === 0)
|
|
91
|
-
return undefined;
|
|
92
|
-
return { decision: 'block', reason: renderClaudeInboxContext(pending) };
|
|
93
|
-
}
|
|
94
|
-
return presentOnce(input.session_id, (sessionId) => deferToActiveCatch(lookup(sessionId)), (inbox) => ({
|
|
95
|
-
hookSpecificOutput: {
|
|
96
|
-
hookEventName: 'UserPromptSubmit',
|
|
97
|
-
additionalContext: renderClaudeInboxContext(inbox),
|
|
98
|
-
},
|
|
99
|
-
}), env);
|
|
100
|
-
}
|
|
101
|
-
export function claudeHookResponse(input, lookup = sessionInbox, env = process.env) {
|
|
102
|
-
return nativeHookResponse(input, lookup, env);
|
|
103
|
-
}
|
|
104
|
-
/** Codex shares Claude's turn-boundary protocol; keep a dedicated command for churn isolation. */
|
|
105
|
-
export function codexHookResponse(input, lookup = sessionInbox, env = process.env) {
|
|
106
|
-
return nativeHookResponse(input, lookup, env);
|
|
107
|
-
}
|
|
108
|
-
export function opencodeHookResponse(input, lookup = sessionInbox, env = process.env) {
|
|
109
|
-
return nativeHookResponse(input, lookup, env);
|
|
8
|
+
return presentPendingAtBoundary(input.session_id, (context) => ({ hookSpecificOutput: { hookEventName: 'PostToolBatch', additionalContext: context } }), lookup, env);
|
|
110
9
|
}
|
|
111
10
|
export function runClaudeHook(inputText, env = process.env) {
|
|
112
11
|
let input;
|
|
@@ -121,16 +20,3 @@ export function runClaudeHook(inputText, env = process.env) {
|
|
|
121
20
|
const response = claudeHookResponse(input, sessionInbox, env);
|
|
122
21
|
return response === undefined ? '' : `${JSON.stringify(response)}\n`;
|
|
123
22
|
}
|
|
124
|
-
export function runCodexHook(inputText, env = process.env) {
|
|
125
|
-
let input;
|
|
126
|
-
try {
|
|
127
|
-
input = JSON.parse(inputText);
|
|
128
|
-
}
|
|
129
|
-
catch {
|
|
130
|
-
return '';
|
|
131
|
-
}
|
|
132
|
-
if (input === null || typeof input !== 'object')
|
|
133
|
-
return '';
|
|
134
|
-
const response = codexHookResponse(input, sessionInbox, env);
|
|
135
|
-
return response === undefined ? '' : `${JSON.stringify(response)}\n`;
|
|
136
|
-
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { loadSquare } from '../artifact.js';
|
|
2
|
-
import { runClaudeHook
|
|
2
|
+
import { runClaudeHook } from '../claude-hook.js';
|
|
3
|
+
import { runCodexHook } from '../codex-hook.js';
|
|
3
4
|
import { coreActivities, coreParticipants, coreStatus } from '../decisions.js';
|
|
4
5
|
import { sessionInbox } from '../inbox.js';
|
|
5
6
|
import { cmdListSquares } from '../list.js';
|
|
@@ -263,6 +264,7 @@ function renderFields(doc, item, fields) {
|
|
|
263
264
|
case 'kind': return item.kind;
|
|
264
265
|
case 'body': return 'body' in item && typeof item.body === 'string' ? item.body.replace(/\s+/g, ' ').trim() : '';
|
|
265
266
|
case 'number': return item.kind === 'say' ? String(sayNumberFor(doc.acts, item)) : '';
|
|
267
|
+
case 'reply': return item.kind === 'say' && item.reply !== undefined ? actId(item.reply) : '';
|
|
266
268
|
default: return '';
|
|
267
269
|
}
|
|
268
270
|
}).join('\t');
|
|
@@ -279,6 +281,7 @@ function jsonLine(doc, item) {
|
|
|
279
281
|
body: 'body' in act && typeof act.body === 'string' ? act.body : '',
|
|
280
282
|
number: act.kind === 'say' ? sayNumberFor(doc.acts, act) : null,
|
|
281
283
|
reach: act.kind === 'say' ? act.reach ?? null : null,
|
|
284
|
+
reply: act.kind === 'say' && act.reply !== undefined ? actId(act.reply) : null,
|
|
282
285
|
});
|
|
283
286
|
}
|
|
284
287
|
export const historyCommand = {
|
|
@@ -116,6 +116,7 @@ function parseActivity(argv, context) {
|
|
|
116
116
|
let noWait = false;
|
|
117
117
|
let beside;
|
|
118
118
|
let bell = false;
|
|
119
|
+
let reply;
|
|
119
120
|
const bodyArgs = [];
|
|
120
121
|
for (let index = 0; index < argv.length; index++) {
|
|
121
122
|
const argument = argv[index];
|
|
@@ -129,6 +130,13 @@ function parseActivity(argv, context) {
|
|
|
129
130
|
}
|
|
130
131
|
else if (argument === '--bell')
|
|
131
132
|
bell = true;
|
|
133
|
+
else if (argument === '--reply') {
|
|
134
|
+
const value = requireValue(argv, index, argument).trim().match(/^(?:act_)?(\d+)$/i);
|
|
135
|
+
if (!value || !Number.isSafeInteger(Number(value[1])))
|
|
136
|
+
fail('Invalid --reply: expected an activity id like act_12 or 12.');
|
|
137
|
+
reply = Number(value[1]);
|
|
138
|
+
index += 1;
|
|
139
|
+
}
|
|
132
140
|
else
|
|
133
141
|
bodyArgs.push(argument);
|
|
134
142
|
}
|
|
@@ -139,11 +147,11 @@ function parseActivity(argv, context) {
|
|
|
139
147
|
if (bodyArgs.length === 0) {
|
|
140
148
|
const piped = readPipedBodyFallback();
|
|
141
149
|
if (piped !== undefined)
|
|
142
|
-
return { name: requireParticipant(context.name), activity: piped, force, noWait, reach };
|
|
150
|
+
return { name: requireParticipant(context.name), activity: piped, force, noWait, reach, reply };
|
|
143
151
|
}
|
|
144
152
|
fail("express requires a body argument (a quoted string or '-' with piped stdin)");
|
|
145
153
|
}
|
|
146
|
-
return { name: requireParticipant(context.name), activity: bodyArgs[0], force, noWait, reach };
|
|
154
|
+
return { name: requireParticipant(context.name), activity: bodyArgs[0], force, noWait, reach, reply };
|
|
147
155
|
}
|
|
148
156
|
export const expressCommand = {
|
|
149
157
|
parse: parseActivity,
|
|
@@ -153,7 +161,8 @@ export const expressCommand = {
|
|
|
153
161
|
force: intent.force,
|
|
154
162
|
noWait: intent.noWait,
|
|
155
163
|
reach: intent.reach,
|
|
156
|
-
|
|
164
|
+
reply: intent.reply,
|
|
165
|
+
forceCommand: `${participantCommandPrefix(context.squarePath, intent.name)} express --force${reachArg}${intent.reply === undefined ? '' : ` --reply act_${intent.reply}`} -`,
|
|
157
166
|
});
|
|
158
167
|
},
|
|
159
168
|
present: () => { },
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { presentPendingAtBoundary } from './boundary-presentation.js';
|
|
2
|
+
import { sessionInbox } from './inbox.js';
|
|
3
|
+
export function codexHookResponse(input, lookup = sessionInbox, env = process.env) {
|
|
4
|
+
if (typeof input.session_id !== 'string' || input.session_id === '')
|
|
5
|
+
return undefined;
|
|
6
|
+
if (input.hook_event_name !== 'PostToolUse')
|
|
7
|
+
return undefined;
|
|
8
|
+
return presentPendingAtBoundary(input.session_id, (context) => ({ hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: context } }), lookup, env);
|
|
9
|
+
}
|
|
10
|
+
export function runCodexHook(inputText, env = process.env) {
|
|
11
|
+
let input;
|
|
12
|
+
try {
|
|
13
|
+
input = JSON.parse(inputText);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return '';
|
|
17
|
+
}
|
|
18
|
+
if (input === null || typeof input !== 'object')
|
|
19
|
+
return '';
|
|
20
|
+
const response = codexHookResponse(input, sessionInbox, env);
|
|
21
|
+
return response === undefined ? '' : `${JSON.stringify(response)}\n`;
|
|
22
|
+
}
|
package/dist/decisions.js
CHANGED
|
@@ -38,9 +38,15 @@ export function decideAct(doc, input) {
|
|
|
38
38
|
if (body.trim() === '')
|
|
39
39
|
throw new SquareError('invalid_args', 'express body cannot be empty');
|
|
40
40
|
const reach = input.reach;
|
|
41
|
+
const reply = input.reply;
|
|
42
|
+
if (reply !== undefined) {
|
|
43
|
+
if (!Number.isSafeInteger(reply) || reply < 0 || reply >= doc.runtime.nextActIndex) {
|
|
44
|
+
throw new SquareError('invalid_args', `Unknown reply activity: act_${reply}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
41
47
|
const state = foldedState(doc);
|
|
42
48
|
const current = participantState(state, name);
|
|
43
|
-
const result = validate(state, { kind: 'say', actor: name, at: now, body, ...(reach !== undefined ? { reach } : {}) }, { hardCap: doc.hardCap, throttlePerMinute: doc.throttlePerMinute, throttleWindowMs: THROTTLE_WINDOW_MS });
|
|
49
|
+
const result = validate(state, { kind: 'say', actor: name, at: now, body, ...(reach !== undefined ? { reach } : {}), ...(reply !== undefined ? { reply } : {}) }, { hardCap: doc.hardCap, throttlePerMinute: doc.throttlePerMinute, throttleWindowMs: THROTTLE_WINDOW_MS });
|
|
44
50
|
if (!result.ok) {
|
|
45
51
|
if (result.reason === 'done')
|
|
46
52
|
throw new SquareError('conflict', `${name} is done; rejoin to express again`);
|
|
@@ -95,7 +101,7 @@ export function decideAct(doc, input) {
|
|
|
95
101
|
const ownActCount = (current?.activityCount ?? 0) + 1;
|
|
96
102
|
return {
|
|
97
103
|
type: 'sent',
|
|
98
|
-
act: { kind: 'say', actor: name, at: now, body, ...(reach !== undefined ? { reach } : {}) },
|
|
104
|
+
act: { kind: 'say', actor: name, at: now, body, ...(reach !== undefined ? { reach } : {}), ...(reply !== undefined ? { reply } : {}) },
|
|
99
105
|
confirmation: `● heads turn your way — #${ownActCount}`,
|
|
100
106
|
ownActCount,
|
|
101
107
|
pendingPublic: unreadPublic,
|
package/dist/harness-claude.js
CHANGED
|
@@ -33,6 +33,7 @@ export async function installClaudePlugin(homeDir, run = runClaude) {
|
|
|
33
33
|
fs.cpSync(fileURLToPath(new URL('../skills/square/', import.meta.url)), plugin, { recursive: true });
|
|
34
34
|
writeJson(path.join(stage, '.claude-plugin', 'marketplace.json'), {
|
|
35
35
|
name: CLAUDE_MARKETPLACE_NAME,
|
|
36
|
+
owner: { name: 'Square' },
|
|
36
37
|
plugins: [{ name: SQUARE_IDENTITY.pluginName, source: './plugins/square' }],
|
|
37
38
|
});
|
|
38
39
|
});
|
package/dist/harness-codex.js
CHANGED
|
@@ -12,8 +12,33 @@ const LEGACY_MARKETPLACES = ['astrosheep-square'];
|
|
|
12
12
|
export function codexMarketplaceRoot(homeDir) {
|
|
13
13
|
return path.join(homeDir, '.square', 'codex', 'marketplaces', CODEX_MARKETPLACE_NAME);
|
|
14
14
|
}
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
function configuredMarketplaceRoot(homeDir, configText) {
|
|
16
|
+
const lines = configText.split('\n');
|
|
17
|
+
const header = `[marketplaces.${CODEX_MARKETPLACE_NAME}]`;
|
|
18
|
+
const start = lines.findIndex((line) => line.trim() === header);
|
|
19
|
+
if (start < 0)
|
|
20
|
+
return undefined;
|
|
21
|
+
const end = lines.findIndex((line, index) => index > start && /^\s*\[[^\]]+\]/.test(line));
|
|
22
|
+
const section = lines.slice(start + 1, end < 0 ? undefined : end);
|
|
23
|
+
const sourceType = section.find((line) => /^\s*source_type\s*=/.test(line));
|
|
24
|
+
if (sourceType && !/=\s*["']local["']\s*$/.test(sourceType))
|
|
25
|
+
return undefined;
|
|
26
|
+
const source = section.find((line) => /^\s*source\s*=/.test(line));
|
|
27
|
+
const match = source?.match(/^\s*source\s*=\s*("(?:\\.|[^"])*"|'[^']*')\s*$/);
|
|
28
|
+
if (!match)
|
|
29
|
+
return undefined;
|
|
30
|
+
const value = match[1].startsWith('"') ? JSON.parse(match[1]) : match[1].slice(1, -1);
|
|
31
|
+
return path.isAbsolute(value) ? value : path.resolve(codexHome(homeDir), value);
|
|
32
|
+
}
|
|
33
|
+
function activeMarketplaceRoot(homeDir, configText) {
|
|
34
|
+
return configuredMarketplaceRoot(homeDir, configText) ?? codexMarketplaceRoot(homeDir);
|
|
35
|
+
}
|
|
36
|
+
export function codexPluginRoot(homeDir, marketplaceRoot = codexMarketplaceRoot(homeDir)) {
|
|
37
|
+
return path.join(marketplaceRoot, 'plugins', SQUARE_IDENTITY.pluginName);
|
|
38
|
+
}
|
|
39
|
+
export function codexPluginHooksPath(homeDir, marketplaceRoot = codexMarketplaceRoot(homeDir)) {
|
|
40
|
+
return path.join(codexPluginRoot(homeDir, marketplaceRoot), 'hooks', 'hooks.json');
|
|
41
|
+
}
|
|
17
42
|
function codexHome(homeDir) { return path.join(homeDir, '.codex'); }
|
|
18
43
|
export function codexHomeHooksPath(homeDir) { return path.join(codexHome(homeDir), 'hooks.json'); }
|
|
19
44
|
export function codexConfigPath(homeDir) { return path.join(codexHome(homeDir), 'config.toml'); }
|
|
@@ -53,9 +78,9 @@ function writeAtomic(file, text) {
|
|
|
53
78
|
fs.renameSync(temp, file);
|
|
54
79
|
}
|
|
55
80
|
export async function installCodexPlugin(homeDir, run = runCodex) {
|
|
56
|
-
const root = codexMarketplaceRoot(homeDir);
|
|
57
81
|
const config = codexConfigPath(homeDir);
|
|
58
82
|
const current = fs.existsSync(config) ? fs.readFileSync(config, 'utf8') : '';
|
|
83
|
+
const root = activeMarketplaceRoot(homeDir, current);
|
|
59
84
|
const staged = stageReplacement(root, (stage) => {
|
|
60
85
|
const plugin = path.join(stage, 'plugins', SQUARE_IDENTITY.pluginName);
|
|
61
86
|
fs.cpSync(fileURLToPath(new URL('../codex-plugin/', import.meta.url)), plugin, { recursive: true });
|
|
@@ -88,7 +113,7 @@ export async function installCodexPlugin(homeDir, run = runCodex) {
|
|
|
88
113
|
notes.push(`retired ${pluginId}`);
|
|
89
114
|
}
|
|
90
115
|
staged.finalize();
|
|
91
|
-
return { configPath: config, marketplaceRoot: root, pluginRoot: codexPluginRoot(homeDir), ...(installedPath ? { installedPath } : {}), notes };
|
|
116
|
+
return { configPath: config, marketplaceRoot: root, pluginRoot: codexPluginRoot(homeDir, root), ...(installedPath ? { installedPath } : {}), notes };
|
|
92
117
|
}
|
|
93
118
|
catch (error) {
|
|
94
119
|
staged.rollback();
|
|
@@ -96,24 +121,27 @@ export async function installCodexPlugin(homeDir, run = runCodex) {
|
|
|
96
121
|
}
|
|
97
122
|
}
|
|
98
123
|
export async function uninstallCodexPlugin(homeDir, run = runCodex) {
|
|
124
|
+
const config = codexConfigPath(homeDir);
|
|
125
|
+
const current = fs.existsSync(config) ? fs.readFileSync(config, 'utf8') : '';
|
|
126
|
+
const root = activeMarketplaceRoot(homeDir, current);
|
|
99
127
|
requireSuccess(run(homeDir, ['plugin', 'remove', CODEX_PLUGIN_ID, '--json']), 'plugin removal', true);
|
|
100
128
|
requireSuccess(run(homeDir, ['plugin', 'marketplace', 'remove', CODEX_MARKETPLACE_NAME, '--json']), 'marketplace removal', true);
|
|
101
129
|
for (const marketplace of LEGACY_MARKETPLACES) {
|
|
102
130
|
requireSuccess(run(homeDir, ['plugin', 'remove', `${SQUARE_IDENTITY.pluginName}@${marketplace}`, '--json']), 'legacy plugin removal', true);
|
|
103
131
|
requireSuccess(run(homeDir, ['plugin', 'marketplace', 'remove', marketplace, '--json']), 'legacy marketplace removal', true);
|
|
104
132
|
}
|
|
105
|
-
const root = codexMarketplaceRoot(homeDir);
|
|
106
133
|
fs.rmSync(root, { recursive: true, force: true });
|
|
107
134
|
fs.rmSync(codexHomeHooksPath(homeDir), { force: true });
|
|
108
135
|
return { paths: [root, codexConfigPath(homeDir), codexHomeHooksPath(homeDir)], notes: [] };
|
|
109
136
|
}
|
|
110
137
|
export async function doctorCodexPlugin(homeDir, run = runCodex) {
|
|
111
|
-
const root = codexMarketplaceRoot(homeDir);
|
|
112
138
|
const config = codexConfigPath(homeDir);
|
|
139
|
+
const current = fs.existsSync(config) ? fs.readFileSync(config, 'utf8') : '';
|
|
140
|
+
const root = activeMarketplaceRoot(homeDir, current);
|
|
113
141
|
const listed = run(homeDir, ['plugin', 'list', '--json']);
|
|
114
142
|
return [
|
|
115
|
-
/^hooks\s*=\s*true$/m.test(
|
|
116
|
-
fs.existsSync(codexPluginHooksPath(homeDir)) ? `✓ Square plugin hooks ${codexPluginHooksPath(homeDir)}` : `○ Square plugin bundle missing ${root}`,
|
|
143
|
+
/^hooks\s*=\s*true$/m.test(current) ? `✓ features.hooks=true in ${config}` : `○ features.hooks missing in ${config}`,
|
|
144
|
+
fs.existsSync(codexPluginHooksPath(homeDir, root)) ? `✓ Square plugin hooks ${codexPluginHooksPath(homeDir, root)}` : `○ Square plugin bundle missing ${root}`,
|
|
117
145
|
listed.status === 0 && listed.stdout.includes(CODEX_PLUGIN_ID) ? `✓ ${CODEX_PLUGIN_ID} installed` : `○ ${CODEX_PLUGIN_ID} unavailable`,
|
|
118
146
|
];
|
|
119
147
|
}
|
package/dist/help.js
CHANGED
|
@@ -16,9 +16,9 @@ const COMMANDS = [
|
|
|
16
16
|
details: ['Options:', ' --last <N> Show the last N public activities (default 10).', ' --all Show the complete history.'],
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
|
-
names: ['express'], usage: '--as <name> express [-f|--force] [--no-wait] [--beside <name> | --bell] <activity | ->', usesSquare: true, group: 'participant',
|
|
19
|
+
names: ['express'], usage: '--as <name> express [-f|--force] [--no-wait] [--beside <name> | --bell] [--reply <act_N>] <activity | ->', usesSquare: true, group: 'participant',
|
|
20
20
|
summary: 'Speak, gesture, or do both.',
|
|
21
|
-
details: ['Options:', ' -f, --force Express without first catching unread activity.', ' --no-wait If held or throttled, save a draft and return.', ' --beside <name> Speak aside to one participant.', " --bell Call every participant's attention to this activity."],
|
|
21
|
+
details: ['Options:', ' -f, --force Express without first catching unread activity.', ' --no-wait If held or throttled, save a draft and return.', ' --beside <name> Speak aside to one participant.', " --bell Call every participant's attention to this activity.", ' --reply <act_N> Mark this activity as a reply to an earlier activity.'],
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
names: ['catch'], usage: '--as <name> catch (--now | --idle <duration>) [--from <names>] [--mention [name]] [--replace]', usesSquare: true, group: 'participant',
|
|
@@ -36,7 +36,7 @@ const COMMANDS = [
|
|
|
36
36
|
summary: 'Inspect bounded machine-local notifications for a native session.',
|
|
37
37
|
details: ['Options:', ' --for-session <id> Required harness session id.', ' --json Emit structured JSON.'],
|
|
38
38
|
},
|
|
39
|
-
{ names: ['claude-hook', 'codex-hook'], usage: '{command}', summary: '
|
|
39
|
+
{ names: ['claude-hook', 'codex-hook'], usage: '{command}', summary: 'Present pending attention at one native agent boundary.', hiddenFromIndex: true },
|
|
40
40
|
{
|
|
41
41
|
names: ['history'], usage: '[--as <name>] history [filters] [output]', usesSquare: true, group: 'participant',
|
|
42
42
|
summary: 'Read or search what happened without changing what you have caught.',
|
package/dist/index.js
CHANGED
|
@@ -58,6 +58,7 @@ export async function express(squarePath, name, body, opts = {}) {
|
|
|
58
58
|
body,
|
|
59
59
|
force: opts.force ?? false,
|
|
60
60
|
now: Date.now(),
|
|
61
|
+
...(opts.reply === undefined ? {} : { reply: actRefIndex(opts.reply) }),
|
|
61
62
|
});
|
|
62
63
|
if (committed.result.type !== 'sent')
|
|
63
64
|
throw new Error(`Activity rejected: ${committed.result.type}`);
|
package/dist/notifications.js
CHANGED
|
@@ -7,7 +7,7 @@ import { recordNotificationFailure } from './notification-failures.js';
|
|
|
7
7
|
import { hasPresentedAttention } from './presented.js';
|
|
8
8
|
import { SquareError } from './model.js';
|
|
9
9
|
import { SLEEP_MS, matchesMentionTarget, resolveRosterName, rosterNames } from './runtime.js';
|
|
10
|
-
import { defaultWakeSinks } from './
|
|
10
|
+
import { defaultWakeSinks } from './paseo-delivery.js';
|
|
11
11
|
export { planActNotifications, matchesMentionTarget };
|
|
12
12
|
function known(doc, name) {
|
|
13
13
|
const value = resolveRosterName(doc, name);
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
3
|
+
import { loadSquare } from './artifact.js';
|
|
4
|
+
import { isDeliveryDelivered, leaseOwnsNotification, } from './delivery.js';
|
|
5
|
+
import { sessionInbox } from './inbox.js';
|
|
6
|
+
import { hasPresentedForOwner, presentOnce } from './presented.js';
|
|
7
|
+
import { lookupParticipant } from './registry.js';
|
|
8
|
+
import { quoteShell } from './presentation.js';
|
|
9
|
+
import { isCurrentlyJoined, resolveRosterName } from './runtime.js';
|
|
10
|
+
import { discoverPaseoAgents, waitForPaseoWakeBoundary, } from './paseo-state.js';
|
|
11
|
+
import { sendPaseoWake } from './wake-sink.js';
|
|
12
|
+
export class PaseoWakeError extends Error {
|
|
13
|
+
diagnostic;
|
|
14
|
+
constructor(message, diagnostic) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.diagnostic = diagnostic;
|
|
17
|
+
this.name = 'PaseoWakeError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function endpoint() {
|
|
21
|
+
return process.env.SQUARE_PASEO_WS_URL ?? process.env.PASEO_LISTEN ?? '127.0.0.1:6767';
|
|
22
|
+
}
|
|
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
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function native(binding) {
|
|
35
|
+
return ['claude-code', 'codex', 'opencode', 'pi'].includes(binding.channel);
|
|
36
|
+
}
|
|
37
|
+
function ownershipSnapshot(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()];
|
|
51
|
+
}
|
|
52
|
+
function selectActiveAgents(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'));
|
|
55
|
+
}
|
|
56
|
+
function catchCommand(squarePath, recipient) {
|
|
57
|
+
return `square --as ${quoteShell(recipient)} --square-path ${quoteShell(squarePath)} catch --now`;
|
|
58
|
+
}
|
|
59
|
+
function prompt(notification, squarePath, nativeWake) {
|
|
60
|
+
const display = squarePath.startsWith(homedir()) ? `~${squarePath.slice(homedir().length)}` : squarePath;
|
|
61
|
+
const body = notification.item.body.length > 200 ? `${notification.item.body.slice(0, 197)}...` : notification.item.body;
|
|
62
|
+
return [
|
|
63
|
+
'<system-reminder source="square">',
|
|
64
|
+
`${notification.route === 'bell' ? 'Bell' : notification.route === 'beside' ? 'Beside' : 'Mention'} from @${notification.item.actor} in \`${display}\``,
|
|
65
|
+
nativeWake ? 'The native adapter will present it at the next boundary.' : `> ${body.replace(/\n/g, '\n> ')}`,
|
|
66
|
+
`\`${catchCommand(squarePath, notification.recipient)}\``,
|
|
67
|
+
'</system-reminder>',
|
|
68
|
+
].join('\n');
|
|
69
|
+
}
|
|
70
|
+
async function waitForCatch(squarePath, recipient, actIndex, ownerId) {
|
|
71
|
+
const deadline = Date.now() + 180_000;
|
|
72
|
+
while (Date.now() < deadline) {
|
|
73
|
+
const doc = loadSquare(squarePath);
|
|
74
|
+
if (isDeliveryDelivered(doc, recipient, actIndex))
|
|
75
|
+
return true;
|
|
76
|
+
const binding = lookupParticipant(squarePath, recipient).find((item) => item.ownerId === ownerId);
|
|
77
|
+
const lease = binding && sessionInbox(binding.sessionId).find((item) => item.name === recipient)?.catchLease;
|
|
78
|
+
if (!lease || lease.expiresAt <= Date.now())
|
|
79
|
+
return false;
|
|
80
|
+
await sleep(Math.min(250, lease.expiresAt - Date.now()));
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
function send(request, ownership) {
|
|
85
|
+
try {
|
|
86
|
+
sendPaseoWake(request);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
throw new PaseoWakeError(error instanceof Error ? error.message : String(error), diagnostic('send', ownership, 'failed'));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export async function dispatchPaseoNotification(notification, ctx) {
|
|
93
|
+
const initial = loadSquare(ctx.squarePath);
|
|
94
|
+
const recipient = resolveRosterName(initial, notification.recipient);
|
|
95
|
+
if (!recipient || !isCurrentlyJoined(initial.acts, recipient))
|
|
96
|
+
return;
|
|
97
|
+
const ownership = ownershipSnapshot(lookupParticipant(ctx.squarePath, recipient));
|
|
98
|
+
if (ownership.length === 0)
|
|
99
|
+
return;
|
|
100
|
+
const discovery = discoverPaseoAgents();
|
|
101
|
+
if (discovery.error && discovery.agents.length === 0) {
|
|
102
|
+
throw new PaseoWakeError(`Paseo unavailable: ${discovery.error}`, diagnostic('discovery', ownership, 'unavailable'));
|
|
103
|
+
}
|
|
104
|
+
const active = selectActiveAgents(ownership, discovery.agents);
|
|
105
|
+
if (active.length === 0) {
|
|
106
|
+
throw new PaseoWakeError('No registered Paseo agent is idle or running.', diagnostic('selection', ownership, 'not_active'));
|
|
107
|
+
}
|
|
108
|
+
let boundaryTimedOut = false;
|
|
109
|
+
for (const agent of active) {
|
|
110
|
+
const owner = ownership.find((item) => item.agentId === agent.id);
|
|
111
|
+
if (!owner || hasPresentedForOwner(owner.ownerId, ctx.squarePath, recipient, notification.item.index))
|
|
112
|
+
continue;
|
|
113
|
+
if (!(await waitForPaseoWakeBoundary(agent))) {
|
|
114
|
+
boundaryTimedOut = true;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const latest = loadSquare(ctx.squarePath);
|
|
118
|
+
if (!isCurrentlyJoined(latest.acts, recipient) || isDeliveryDelivered(latest, recipient, notification.item.index))
|
|
119
|
+
return;
|
|
120
|
+
const current = lookupParticipant(ctx.squarePath, recipient).find((item) => item.ownerId === owner.ownerId && item.paseoAgentId === owner.agentId);
|
|
121
|
+
if (!current)
|
|
122
|
+
continue;
|
|
123
|
+
const activeCatch = sessionInbox(current.sessionId).find((item) => item.name === recipient)?.catchLease;
|
|
124
|
+
if (activeCatch &&
|
|
125
|
+
leaseOwnsNotification(activeCatch, {
|
|
126
|
+
actor: notification.item.actor,
|
|
127
|
+
body: notification.item.body,
|
|
128
|
+
route: notification.route,
|
|
129
|
+
recipient,
|
|
130
|
+
}) &&
|
|
131
|
+
(await waitForCatch(ctx.squarePath, recipient, notification.item.index, owner.ownerId))) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const nativeWake = owner.nativeGuarantee;
|
|
135
|
+
const request = {
|
|
136
|
+
agentId: agent.id,
|
|
137
|
+
prompt: prompt({ ...notification, recipient }, ctx.squarePath, nativeWake),
|
|
138
|
+
};
|
|
139
|
+
if (nativeWake) {
|
|
140
|
+
send(request, ownership);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
presentOnce(current.sessionId, (id) => sessionInbox(id)
|
|
144
|
+
.map((item) => ({ ...item, notifications: item.notifications.filter((note) => note.actIndex === notification.item.index) }))
|
|
145
|
+
.filter((item) => item.notifications.length > 0), () => {
|
|
146
|
+
send(request, ownership);
|
|
147
|
+
return true;
|
|
148
|
+
});
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (boundaryTimedOut) {
|
|
152
|
+
throw new PaseoWakeError('Paseo did not reach the current tool boundary before the wake timeout.', diagnostic('boundary', ownership, 'timeout'));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
export function paseoWakeSink() {
|
|
156
|
+
return { name: 'paseo', dispatch: dispatchPaseoNotification };
|
|
157
|
+
}
|
|
158
|
+
export function defaultWakeSinks() {
|
|
159
|
+
return process.env.SQUARE_DISABLE_PASEO_WAKE === '1' ? [] : [paseoWakeSink()];
|
|
160
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { waitForPaseoToolBoundary } from './paseo-timeline.js';
|
|
3
|
+
export function discoverPaseoAgents(timeoutMs = 5000) {
|
|
4
|
+
try {
|
|
5
|
+
const raw = execFileSync(process.env.SQUARE_PASEO_BIN || 'paseo', ['ls', '--json'], {
|
|
6
|
+
encoding: 'utf8',
|
|
7
|
+
timeout: timeoutMs,
|
|
8
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
9
|
+
});
|
|
10
|
+
const parsed = JSON.parse(raw);
|
|
11
|
+
const agents = Array.isArray(parsed) ? parsed : parsed?.agents;
|
|
12
|
+
if (!Array.isArray(agents))
|
|
13
|
+
return { agents: [], error: 'Paseo returned malformed agent inventory.' };
|
|
14
|
+
return {
|
|
15
|
+
agents: agents.filter((item) => item !== null &&
|
|
16
|
+
typeof item === 'object' &&
|
|
17
|
+
typeof item.id === 'string' &&
|
|
18
|
+
typeof item.status === 'string'),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
return { agents: [], error: error instanceof Error ? error.message : String(error) };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export async function waitForPaseoWakeBoundary(agent) {
|
|
26
|
+
if (agent.status === 'idle')
|
|
27
|
+
return true;
|
|
28
|
+
if (agent.status !== 'running')
|
|
29
|
+
return false;
|
|
30
|
+
return waitForPaseoToolBoundary(agent.id);
|
|
31
|
+
}
|
package/dist/presentation.js
CHANGED
|
@@ -153,7 +153,8 @@ export function renderEventCli(event, opts = {}) {
|
|
|
153
153
|
const mentionSuffix = mention !== undefined && extractMentions(event.body).some((name) => sameName(name, mention))
|
|
154
154
|
? ` · calls your name across the square — @${mention}`
|
|
155
155
|
: '';
|
|
156
|
-
|
|
156
|
+
const replySuffix = event.reply === undefined ? '' : ` · replies to ${actId(event.reply)}`;
|
|
157
|
+
return `● ${event.actor} #${opts.actNumber ?? 1} · ${actId(event)} · ${formatRelativeTime(event.at, now)}${mentionSuffix}${replySuffix}${bodySuffix(body)}`;
|
|
157
158
|
}
|
|
158
159
|
case 'done': {
|
|
159
160
|
const body = renderedBody(event.body, maxBody);
|
package/dist/wake-sink.js
CHANGED
|
@@ -1,165 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import { setTimeout as sleep } from 'node:timers/promises';
|
|
4
|
-
import { loadSquare } from './artifact.js';
|
|
5
|
-
import { leaseOwnsNotification, isDeliveryDelivered } from './delivery.js';
|
|
6
|
-
import { sessionInbox } from './inbox.js';
|
|
7
|
-
import { hasPresentedForOwner, presentOnce } from './presented.js';
|
|
8
|
-
import { lookupParticipant } from './registry.js';
|
|
9
|
-
import { quoteShell } from './presentation.js';
|
|
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';
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
function endpoint() {
|
|
21
|
-
return process.env.SQUARE_PASEO_WS_URL ?? process.env.PASEO_LISTEN ?? '127.0.0.1:6767';
|
|
22
|
-
}
|
|
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
|
-
};
|
|
33
|
-
}
|
|
34
|
-
function native(binding) {
|
|
35
|
-
return ['claude-code', 'codex', 'opencode', 'pi'].includes(binding.channel);
|
|
36
|
-
}
|
|
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()];
|
|
51
|
-
}
|
|
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'));
|
|
55
|
-
}
|
|
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
|
-
}
|
|
68
|
-
}
|
|
69
|
-
function send(agentId, prompt) {
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
export function sendPaseoWake({ agentId, prompt }) {
|
|
70
3
|
const result = spawnSync(process.env.SQUARE_PASEO_BIN || 'paseo', ['send', agentId, '--prompt', prompt, '--no-wait'], { stdio: 'ignore', timeout: 5000, env: process.env });
|
|
71
4
|
if (result.error)
|
|
72
5
|
throw result.error;
|
|
73
6
|
if (result.status !== 0)
|
|
74
7
|
throw new Error(`paseo send exited with ${result.status ?? 'no status'}`);
|
|
75
8
|
}
|
|
76
|
-
function catchCommand(squarePath, recipient) {
|
|
77
|
-
return `square --as ${quoteShell(recipient)} --square-path ${quoteShell(squarePath)} catch --now`;
|
|
78
|
-
}
|
|
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');
|
|
89
|
-
}
|
|
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;
|
|
103
|
-
}
|
|
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;
|
|
109
|
-
const bindings = lookupParticipant(ctx.squarePath, recipient);
|
|
110
|
-
const ownership = paseoOwnershipSnapshot(bindings);
|
|
111
|
-
if (ownership.length === 0)
|
|
112
|
-
return;
|
|
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;
|
|
129
|
-
}
|
|
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))
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
if (owner.nativeGuarantee) {
|
|
142
|
-
try {
|
|
143
|
-
send(agent.id, prompt({ ...notification, recipient }, ctx.squarePath, true));
|
|
144
|
-
}
|
|
145
|
-
catch (error) {
|
|
146
|
-
throw new PaseoWakeError(error instanceof Error ? error.message : String(error), diagnostic('send', ownership, 'failed'));
|
|
147
|
-
}
|
|
148
|
-
return;
|
|
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;
|
|
160
|
-
}
|
|
161
|
-
if (boundaryTimedOut)
|
|
162
|
-
throw new PaseoWakeError('Paseo did not reach the current tool boundary before the wake timeout.', diagnostic('boundary', ownership, 'timeout'));
|
|
163
|
-
}
|
|
164
|
-
export function paseoWakeSink() { return { name: 'paseo', dispatch: dispatchPaseoNotification }; }
|
|
165
|
-
export function defaultWakeSinks() { return process.env.SQUARE_DISABLE_PASEO_WAKE === '1' ? [] : [paseoWakeSink()]; }
|
|
@@ -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
|
}
|
package/extensions/square-pi.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { sessionInbox } from '../dist/inbox.js';
|
|
3
|
-
import { presentOnce } from '../dist/presented.js';
|
|
1
|
+
import { presentPendingAtBoundary, renderPendingAtBoundary } from '../dist/boundary-presentation.js';
|
|
4
2
|
|
|
5
3
|
export function pendingInbox(inbox) {
|
|
6
4
|
return inbox.filter((item) => item.notifications?.length > 0);
|
|
@@ -13,13 +11,13 @@ export function inboxKeys(inbox) {
|
|
|
13
11
|
}
|
|
14
12
|
|
|
15
13
|
export function renderPiInbox(inbox) {
|
|
16
|
-
return
|
|
14
|
+
return renderPendingAtBoundary(pendingInbox(inbox));
|
|
17
15
|
}
|
|
18
16
|
|
|
19
17
|
export default function squarePiExtension(pi) {
|
|
20
18
|
let sessionId;
|
|
21
19
|
let previousSessionId;
|
|
22
|
-
const present = (deliver) => sessionId === undefined ? undefined :
|
|
20
|
+
const present = (deliver) => sessionId === undefined ? undefined : presentPendingAtBoundary(sessionId, deliver);
|
|
23
21
|
|
|
24
22
|
pi.on('session_start', async (_event, ctx) => {
|
|
25
23
|
sessionId = ctx.sessionManager.getSessionId();
|
|
@@ -29,7 +27,7 @@ export default function squarePiExtension(pi) {
|
|
|
29
27
|
|
|
30
28
|
pi.on('before_agent_start', async () => {
|
|
31
29
|
try {
|
|
32
|
-
return present((
|
|
30
|
+
return present((context) => ({ message: { customType: 'square', content: context, display: true } }));
|
|
33
31
|
} catch {
|
|
34
32
|
return undefined;
|
|
35
33
|
}
|
package/package.json
CHANGED
|
@@ -1,18 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "Bounded Square inbox
|
|
2
|
+
"description": "Bounded Square inbox admission between Claude Code agent steps",
|
|
3
3
|
"hooks": {
|
|
4
|
-
"
|
|
5
|
-
{
|
|
6
|
-
"hooks": [
|
|
7
|
-
{
|
|
8
|
-
"type": "command",
|
|
9
|
-
"command": "square claude-hook",
|
|
10
|
-
"timeout": 5
|
|
11
|
-
}
|
|
12
|
-
]
|
|
13
|
-
}
|
|
14
|
-
],
|
|
15
|
-
"Stop": [
|
|
4
|
+
"PostToolBatch": [
|
|
16
5
|
{
|
|
17
6
|
"hooks": [
|
|
18
7
|
{
|