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