@astrosheep/square 0.3.4 → 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 +23 -22
- package/dist/artifact.js +126 -202
- package/dist/claude-hook.js +45 -21
- package/dist/cli/context.js +143 -0
- package/dist/cli/harness-command.js +50 -0
- package/dist/cli/maintenance-commands.js +76 -0
- package/dist/cli/meta-commands.js +28 -0
- package/dist/cli/observation-commands.js +453 -0
- package/dist/cli/program.js +48 -0
- package/dist/cli/registry.js +40 -0
- package/dist/cli/square-commands.js +219 -0
- package/dist/cmd/notify-once.js +23 -21
- package/dist/compact.js +6 -19
- 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 +68 -0
- package/dist/harness-codex.js +119 -0
- package/dist/harness-links.js +123 -0
- package/dist/harness-stage.js +36 -0
- package/dist/harness.js +94 -576
- package/dist/help.js +44 -35
- package/dist/inbox.js +12 -11
- package/dist/index.js +30 -129
- 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 +26 -137
- package/dist/square-application.js +264 -0
- package/dist/square-core.js +3 -11
- package/dist/square.js +5 -1362
- package/dist/stream.js +27 -126
- package/dist/wake-sink.js +134 -188
- package/dist/watch.js +79 -138
- 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/terminal.js +0 -125
package/dist/delivery-health.js
CHANGED
|
@@ -1,249 +1,143 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { loadSquare
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { loadSquare } from './artifact.js';
|
|
5
|
+
import { deriveDeliveryModel } from './delivery.js';
|
|
6
|
+
import { readNotificationFailures } from './notification-failures.js';
|
|
7
|
+
import { isCurrentlyJoined } from './runtime.js';
|
|
8
|
+
import { sameName } from './model.js';
|
|
7
9
|
import { formatDuration } from './time.js';
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
function parsePositiveIntegerEnv(name, fallback) {
|
|
13
|
-
const raw = process.env[name];
|
|
10
|
+
const STALE_MS = 60_000;
|
|
11
|
+
const LOOKBACK_MS = 60 * 60 * 1000;
|
|
12
|
+
function positive(name, fallback, env) {
|
|
13
|
+
const raw = env[name];
|
|
14
14
|
if (raw === undefined)
|
|
15
15
|
return fallback;
|
|
16
|
-
const value = Number
|
|
17
|
-
if (!Number.
|
|
18
|
-
throw new
|
|
19
|
-
}
|
|
16
|
+
const value = Number(raw);
|
|
17
|
+
if (!Number.isInteger(value) || value <= 0)
|
|
18
|
+
throw new Error(`Invalid ${name}: expected a positive integer.`);
|
|
20
19
|
return value;
|
|
21
20
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return parsePositiveIntegerEnv('SQUARE_DELIVERY_STALE_MS', DEFAULT_STALE_MS);
|
|
25
|
-
}
|
|
26
|
-
/** How far back liveness scans look. Older unreceipted mentions are backlog, not hook-death. */
|
|
27
|
-
export function deliveryLookbackMs() {
|
|
28
|
-
return parsePositiveIntegerEnv('SQUARE_DELIVERY_LOOKBACK_MS', DEFAULT_LOOKBACK_MS);
|
|
29
|
-
}
|
|
30
|
-
function baselinePath(env = process.env) {
|
|
31
|
-
return env.SQUARE_DELIVERY_BASELINE || path.join(os.homedir(), '.square', 'delivery-baseline.json');
|
|
32
|
-
}
|
|
33
|
-
function readBaselines(env = process.env) {
|
|
34
|
-
const file = baselinePath(env);
|
|
35
|
-
if (!fs.existsSync(file))
|
|
36
|
-
return {};
|
|
37
|
-
try {
|
|
38
|
-
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
39
|
-
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
40
|
-
return {};
|
|
41
|
-
return parsed;
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
return {};
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
export function recordBacklogBaseline(squarePath, backlogCount, env = process.env, at = Date.now()) {
|
|
48
|
-
const file = baselinePath(env);
|
|
49
|
-
const data = readBaselines(env);
|
|
50
|
-
data[path.resolve(squarePath)] = { backlogCount, at };
|
|
51
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
52
|
-
const temp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
53
|
-
fs.writeFileSync(temp, `${JSON.stringify(data, null, 2)}\n`, { mode: 0o600 });
|
|
54
|
-
fs.renameSync(temp, file);
|
|
21
|
+
export function deliveryStaleMs(env = process.env) {
|
|
22
|
+
return positive('SQUARE_DELIVERY_STALE_MS', STALE_MS, env);
|
|
55
23
|
}
|
|
56
|
-
export function
|
|
57
|
-
return
|
|
24
|
+
export function deliveryLookbackMs(env = process.env) {
|
|
25
|
+
return positive('SQUARE_DELIVERY_LOOKBACK_MS', LOOKBACK_MS, env);
|
|
58
26
|
}
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
// presentation cache may suppress duplicate hook text, but never clears debt.
|
|
62
|
-
return isDeliveryDelivered(doc, recipient, actIndex);
|
|
27
|
+
function actedAfter(doc, recipient, actIndex) {
|
|
28
|
+
return doc.acts.some((act) => act.actor !== undefined && sameName(act.actor, recipient) && act.index > actIndex);
|
|
63
29
|
}
|
|
64
|
-
function
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const recipient = resolveRosterName(doc, planned.recipient) ?? planned.recipient;
|
|
80
|
-
if (opts.onlyJoined && !isCurrentlyJoined(doc.acts, recipient))
|
|
81
|
-
continue;
|
|
82
|
-
if (isReceipted(squarePath, doc, recipient, act.index, env))
|
|
83
|
-
continue;
|
|
84
|
-
pending.push({
|
|
85
|
-
squarePath,
|
|
86
|
-
recipient,
|
|
87
|
-
actIndex: act.index,
|
|
88
|
-
actor: act.actor,
|
|
89
|
-
at: act.at,
|
|
90
|
-
ageMs: now - act.at,
|
|
91
|
-
via: planned.via,
|
|
92
|
-
actedAfterWithoutDelivery: actedAfterMention(doc, recipient, act.at, act.index),
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return pending;
|
|
30
|
+
function pending(squarePath, now) {
|
|
31
|
+
const doc = loadSquare(squarePath);
|
|
32
|
+
const model = deriveDeliveryModel(doc);
|
|
33
|
+
const recipients = [...new Set(doc.acts.filter((act) => act.kind === 'join').map((act) => act.actor))]
|
|
34
|
+
.filter((name) => isCurrentlyJoined(doc.acts, name));
|
|
35
|
+
return recipients.flatMap((recipient) => model.pendingFor(recipient).map((note) => ({
|
|
36
|
+
squarePath,
|
|
37
|
+
recipient: note.recipient,
|
|
38
|
+
actIndex: note.item.index,
|
|
39
|
+
actor: note.item.actor,
|
|
40
|
+
at: note.item.at,
|
|
41
|
+
ageMs: now - note.item.at,
|
|
42
|
+
route: note.route,
|
|
43
|
+
actedAfterWithoutDelivery: actedAfter(doc, note.recipient, note.item.index),
|
|
44
|
+
})));
|
|
97
45
|
}
|
|
98
|
-
/**
|
|
99
|
-
* Partition unreceipted mention/bell traffic.
|
|
100
|
-
* Liveness failure requires: past stale, inside lookback, currently joined, mention after last join.
|
|
101
|
-
*/
|
|
102
46
|
export function partitionPendingDeliveries(squarePath, opts = {}) {
|
|
103
47
|
const now = opts.now ?? Date.now();
|
|
104
48
|
const staleMs = opts.staleMs ?? deliveryStaleMs();
|
|
105
49
|
const lookbackMs = Math.max(opts.lookbackMs ?? deliveryLookbackMs(), staleMs);
|
|
106
|
-
const doc = opts.doc ?? loadSquare(squarePath);
|
|
107
50
|
const recent = [];
|
|
108
51
|
const historical = [];
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
onlyJoined: false,
|
|
112
|
-
env: opts.env,
|
|
113
|
-
})) {
|
|
114
|
-
const joined = isCurrentlyJoined(doc.acts, item.recipient);
|
|
115
|
-
const joinAt = lastJoinAt(doc.acts, item.recipient);
|
|
116
|
-
const afterJoin = joinAt === undefined || item.at >= joinAt;
|
|
117
|
-
const inLookback = item.ageMs <= lookbackMs;
|
|
118
|
-
const pastStale = item.ageMs >= staleMs;
|
|
119
|
-
if (joined && pastStale && inLookback && afterJoin) {
|
|
52
|
+
for (const item of pending(squarePath, now)) {
|
|
53
|
+
if (item.ageMs >= staleMs && item.ageMs <= lookbackMs)
|
|
120
54
|
recent.push(item);
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
// Still in grace window and would otherwise be live → ignore (not historical yet).
|
|
124
|
-
if (joined && !pastStale && inLookback && afterJoin)
|
|
125
|
-
continue;
|
|
126
|
-
historical.push(item);
|
|
55
|
+
else if (item.ageMs >= staleMs)
|
|
56
|
+
historical.push(item);
|
|
127
57
|
}
|
|
128
58
|
return { recent, historical };
|
|
129
59
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
const lines = [];
|
|
142
|
-
for (const [recipient, group] of byRecipient) {
|
|
143
|
-
const oldest = group.reduce((a, b) => (a.at <= b.at ? a : b));
|
|
144
|
-
const ageSec = Math.max(1, Math.round(oldest.ageMs / 1000));
|
|
145
|
-
const adapter = group.some((item) => item.actedAfterWithoutDelivery);
|
|
146
|
-
lines.push(adapter
|
|
147
|
-
? ` · @${recipient}: ${group.length} pending — acted after the mention, but delivery was not acknowledged (oldest act_${oldest.actIndex}, ~${ageSec}s)`
|
|
148
|
-
: ` · @${recipient}: ${group.length} pending (oldest act_${oldest.actIndex} from @${oldest.actor}, ~${ageSec}s)`);
|
|
149
|
-
}
|
|
150
|
-
return lines;
|
|
60
|
+
function byRecipient(items) {
|
|
61
|
+
const groups = new Map();
|
|
62
|
+
for (const item of items)
|
|
63
|
+
groups.set(item.recipient, [...(groups.get(item.recipient) ?? []), item]);
|
|
64
|
+
return [...groups].map(([recipient, notes]) => {
|
|
65
|
+
const oldest = notes.reduce((first, item) => item.at < first.at ? item : first);
|
|
66
|
+
const adapterFault = notes.some((item) => item.actedAfterWithoutDelivery);
|
|
67
|
+
return adapterFault
|
|
68
|
+
? ` · @${recipient}: ${notes.length} pending; they acted after it without a receipt (act_${oldest.actIndex})`
|
|
69
|
+
: ` · @${recipient}: ${notes.length} pending (oldest act_${oldest.actIndex} from @${oldest.actor})`;
|
|
70
|
+
});
|
|
151
71
|
}
|
|
152
72
|
export function formatStaleDeliveryWarnings(recent, historical = [], opts = {}) {
|
|
153
|
-
const
|
|
73
|
+
const out = [];
|
|
154
74
|
if (recent.length > 0) {
|
|
155
75
|
const adapterFaults = recent.filter((item) => item.actedAfterWithoutDelivery);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
const other = recent.filter((item) => !item.actedAfterWithoutDelivery);
|
|
162
|
-
if (other.length > 0) {
|
|
163
|
-
lines.push(`⚠ ${other.length} recent mention or bell notification(s) were not acknowledged inside the delivery window.`);
|
|
164
|
-
lines.push(' The installed hook or extension may not be consuming notifications.');
|
|
165
|
-
}
|
|
166
|
-
lines.push(...summarizeByRecipient(recent));
|
|
167
|
-
if (adapterFaults.length === 0)
|
|
168
|
-
lines.push(' » square harness doctor');
|
|
76
|
+
out.push(adapterFaults.length > 0
|
|
77
|
+
? `✕ ${adapterFaults.length} pending notification(s) point to an adapter/pull dead path.`
|
|
78
|
+
: `✕ ${recent.length} recent notification(s) have no delivered receipt.`);
|
|
79
|
+
out.push(...byRecipient(recent));
|
|
169
80
|
}
|
|
170
81
|
if (historical.length > 0) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
for (const item of historical) {
|
|
174
|
-
counts.set(item.recipient, (counts.get(item.recipient) ?? 0) + 1);
|
|
175
|
-
}
|
|
176
|
-
lines.push(` · ${[...counts.entries()].map(([name, n]) => `@${name}:${n}`).join(', ')}`);
|
|
82
|
+
out.push(`○ ${historical.length} older pending notification(s) remain as historical backlog.`);
|
|
83
|
+
out.push(...byRecipient(historical));
|
|
177
84
|
if (opts.previousBacklog !== undefined) {
|
|
178
85
|
const delta = historical.length - opts.previousBacklog;
|
|
179
|
-
|
|
180
|
-
lines.push(` ⚠ backlog grew by ${delta} since last doctor (something aged out of lookback unnoticed).`);
|
|
181
|
-
}
|
|
182
|
-
else if (delta < 0) {
|
|
183
|
-
lines.push(` · backlog shrank by ${-delta} since last doctor.`);
|
|
184
|
-
}
|
|
185
|
-
else {
|
|
186
|
-
lines.push(' · backlog unchanged since last doctor.');
|
|
187
|
-
}
|
|
86
|
+
out.push(delta === 0 ? ' · backlog unchanged since last doctor.' : delta > 0 ? ` · backlog grew by ${delta} since last doctor.` : ` · backlog shrank by ${-delta} since last doctor.`);
|
|
188
87
|
}
|
|
189
88
|
}
|
|
190
|
-
else if (opts.previousBacklog
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
return lines;
|
|
89
|
+
else if ((opts.previousBacklog ?? 0) > 0)
|
|
90
|
+
out.push(`○ backlog cleared (was ${opts.previousBacklog}).`);
|
|
91
|
+
return out;
|
|
194
92
|
}
|
|
195
|
-
|
|
93
|
+
function baselineFile(env) {
|
|
94
|
+
return env.SQUARE_DELIVERY_BASELINE ?? path.join(os.homedir(), '.square', 'delivery-baseline.json');
|
|
95
|
+
}
|
|
96
|
+
function baseline(squarePath, env) {
|
|
196
97
|
try {
|
|
197
|
-
|
|
198
|
-
const { recent, historical } = partitionPendingDeliveries(squarePath, { now, doc, env });
|
|
199
|
-
const previous = previousBacklogCount(squarePath, env);
|
|
200
|
-
const lines = [
|
|
201
|
-
`· stale after ${formatDuration(deliveryStaleMs())} · scan window ${formatDuration(deliveryLookbackMs())}`,
|
|
202
|
-
`· participants ${rosterNames(doc).join(', ') || '(none)'}`,
|
|
203
|
-
];
|
|
204
|
-
if (recent.length === 0 && historical.length === 0) {
|
|
205
|
-
lines.push('✓ no stale undelivered mentions or bells');
|
|
206
|
-
}
|
|
207
|
-
else {
|
|
208
|
-
if (recent.length === 0) {
|
|
209
|
-
lines.push('✓ no current delivery failures inside the scan window');
|
|
210
|
-
}
|
|
211
|
-
lines.push(...formatStaleDeliveryWarnings(recent, historical, { previousBacklog: previous }).map((line) => line.startsWith('⚠ ') ? line.replace(/^⚠ /, '✕ ') : line));
|
|
212
|
-
}
|
|
213
|
-
recordBacklogBaseline(squarePath, historical.length, env, now);
|
|
214
|
-
return lines;
|
|
98
|
+
return JSON.parse(fs.readFileSync(baselineFile(env), 'utf8'))[path.resolve(squarePath)]?.backlogCount;
|
|
215
99
|
}
|
|
216
|
-
catch
|
|
217
|
-
return
|
|
100
|
+
catch {
|
|
101
|
+
return undefined;
|
|
218
102
|
}
|
|
219
103
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
return `square:${squarePath}#act_${actIndex}`;
|
|
226
|
-
}
|
|
227
|
-
/**
|
|
228
|
-
* Write delivered-class receipts with reason=reconciled for backlog items.
|
|
229
|
-
* Does not touch recent liveness failures.
|
|
230
|
-
*/
|
|
231
|
-
export function reconcileDeliveryBacklog(squarePath, opts = {}) {
|
|
232
|
-
const now = opts.now ?? Date.now();
|
|
233
|
-
const doc = loadSquare(squarePath);
|
|
234
|
-
const { recent, historical } = partitionPendingDeliveries(squarePath, {
|
|
235
|
-
now,
|
|
236
|
-
doc,
|
|
237
|
-
lookbackMs: opts.lookbackMs,
|
|
238
|
-
staleMs: opts.staleMs,
|
|
239
|
-
});
|
|
240
|
-
const actor = opts.actor ?? 'doctor --fix reconcile-backlog';
|
|
241
|
-
let reconciled = 0;
|
|
242
|
-
for (const item of historical) {
|
|
243
|
-
if (recordDeliveredDelivery(doc, item.recipient, item.actIndex, { at: now, reason: 'reconciled', actor }))
|
|
244
|
-
reconciled += 1;
|
|
104
|
+
function writeBaseline(squarePath, backlogCount, env, at) {
|
|
105
|
+
const file = baselineFile(env);
|
|
106
|
+
let rows = {};
|
|
107
|
+
try {
|
|
108
|
+
rows = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
245
109
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
110
|
+
catch { }
|
|
111
|
+
rows[path.resolve(squarePath)] = { backlogCount, at };
|
|
112
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
113
|
+
fs.writeFileSync(file, `${JSON.stringify(rows, null, 2)}\n`, { mode: 0o600 });
|
|
114
|
+
}
|
|
115
|
+
function formatFailures(squarePath, recent, env) {
|
|
116
|
+
const failures = readNotificationFailures(squarePath, env);
|
|
117
|
+
if (failures.length === 0)
|
|
118
|
+
return [];
|
|
119
|
+
const pendingKeys = new Set(recent.map((item) => `${item.recipient}\0${item.actIndex}`));
|
|
120
|
+
const current = failures.filter((item) => item.recipient !== undefined && pendingKeys.has(`${item.recipient}\0${item.actIndex}`));
|
|
121
|
+
const rows = current.length > 0 ? current : failures;
|
|
122
|
+
const historical = current.length === 0;
|
|
123
|
+
const latest = rows.at(-1);
|
|
124
|
+
const diagnostic = latest.diagnostic;
|
|
125
|
+
return [
|
|
126
|
+
historical ? `○ ${rows.length} historical notification failure(s) retained: ${latest.message}` : `✕ ${rows.length} notification attempt(s) failed: ${latest.message}; receipt remains pending.`,
|
|
127
|
+
...(diagnostic?.passwordPresent === false ? [' · PASEO_PASSWORD absent; pass PASEO_PASSWORD to the Codex process.'] : []),
|
|
128
|
+
` · ${notificationFailuresPathForDisplay(squarePath, env)}`,
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
function notificationFailuresPathForDisplay(squarePath, env) {
|
|
132
|
+
return env.SQUARE_NOTIFICATION_FAILURES ?? path.join(path.dirname(squarePath), 'notification-failures.ndjsonl');
|
|
133
|
+
}
|
|
134
|
+
export function doctorDeliveryHealth(squarePath, now = Date.now(), env = process.env) {
|
|
135
|
+
const { recent, historical } = partitionPendingDeliveries(squarePath, { now, staleMs: deliveryStaleMs(env), lookbackMs: deliveryLookbackMs(env) });
|
|
136
|
+
const prior = baseline(squarePath, env);
|
|
137
|
+
writeBaseline(squarePath, historical.length, env, now);
|
|
138
|
+
return [
|
|
139
|
+
`· stale after ${formatDuration(deliveryStaleMs(env))} · scan window ${formatDuration(deliveryLookbackMs(env))}`,
|
|
140
|
+
...(recent.length === 0 && historical.length === 0 ? ['✓ no stale undelivered notifications'] : formatStaleDeliveryWarnings(recent, historical, { previousBacklog: prior })),
|
|
141
|
+
...formatFailures(squarePath, recent, env),
|
|
142
|
+
];
|
|
249
143
|
}
|
package/dist/delivery.js
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
import { findParticipantName, sameName, } from './model.js';
|
|
2
|
-
import { extractMentions,
|
|
2
|
+
import { actId, extractMentions, isCurrentlyJoined, lastJoinIndex, matchesMentionTarget, resolveRosterName, rosterNames } from './runtime.js';
|
|
3
|
+
export function notificationMessageId(squarePath, actIndex) {
|
|
4
|
+
return `square:${squarePath}#act_${actIndex}`;
|
|
5
|
+
}
|
|
6
|
+
export function isPendingNotification(notification) {
|
|
7
|
+
return notification.route !== 'broadcast';
|
|
8
|
+
}
|
|
9
|
+
function canonicalRecipient(doc, name) {
|
|
10
|
+
return resolveRosterName(doc, name) ?? name;
|
|
11
|
+
}
|
|
12
|
+
/** Delivery receipts are the only durable acknowledgement of directed attention. */
|
|
13
|
+
export function deliveryReceipt(doc, name, actOrIndex) {
|
|
14
|
+
return deliveryReceiptFromRuntime(doc.runtime, canonicalRecipient(doc, name), actOrIndex);
|
|
15
|
+
}
|
|
16
|
+
export function deliveryReceiptFromRuntime(runtime, recipient, actOrIndex) {
|
|
17
|
+
return runtime.deliveryReceipts[recipient]?.[actId(actOrIndex)];
|
|
18
|
+
}
|
|
19
|
+
export function isDeliveryDelivered(doc, name, actOrIndex) {
|
|
20
|
+
return deliveryReceipt(doc, name, actOrIndex)?.status === 'delivered';
|
|
21
|
+
}
|
|
22
|
+
export function recordDeliveredDelivery(doc, name, actOrIndex, receipt) {
|
|
23
|
+
return recordDeliveredRuntime(doc.runtime, canonicalRecipient(doc, name), actOrIndex, receipt);
|
|
24
|
+
}
|
|
25
|
+
export function recordDeliveredRuntime(runtime, recipient, actOrIndex, receipt) {
|
|
26
|
+
const id = actId(actOrIndex);
|
|
27
|
+
if (deliveryReceiptFromRuntime(runtime, recipient, actOrIndex)?.status === 'delivered')
|
|
28
|
+
return false;
|
|
29
|
+
(runtime.deliveryReceipts[recipient] ??= {})[id] = { status: 'delivered', ...receipt };
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
export function markDeliveredDelivery(doc, name, actOrIndex, at = Date.now()) {
|
|
33
|
+
return recordDeliveredDelivery(doc, name, actOrIndex, { at });
|
|
34
|
+
}
|
|
3
35
|
function uniqueKnownMentions(body, roster) {
|
|
4
36
|
const recipients = [];
|
|
5
37
|
for (const mention of extractMentions(body)) {
|
|
@@ -15,30 +47,30 @@ function uniqueKnownMentions(body, roster) {
|
|
|
15
47
|
* All consumers share these targets instead of reinterpreting artifact text or cursor state.
|
|
16
48
|
*/
|
|
17
49
|
export function deriveDeliveryModel(doc) {
|
|
18
|
-
const roster = rosterNames(doc);
|
|
50
|
+
const roster = rosterNames(doc).filter((name) => isCurrentlyJoined(doc.acts, name));
|
|
19
51
|
let pendingByRecipient;
|
|
20
52
|
function plan(item) {
|
|
21
|
-
if (item.
|
|
53
|
+
if (item.kind !== 'say')
|
|
22
54
|
return [];
|
|
23
55
|
const sayItem = item;
|
|
24
|
-
const actor = sayItem.
|
|
25
|
-
if (sayItem.
|
|
56
|
+
const actor = sayItem.actor;
|
|
57
|
+
if (sayItem.reach === 'bell') {
|
|
26
58
|
return roster
|
|
27
59
|
.filter((recipient) => !sameName(recipient, actor))
|
|
28
|
-
.map((recipient) => ({ item: sayItem, recipient,
|
|
60
|
+
.map((recipient) => ({ item: sayItem, recipient, route: 'bell' }));
|
|
29
61
|
}
|
|
30
|
-
if (sayItem.
|
|
31
|
-
const recipient = findParticipantName(roster, sayItem.
|
|
62
|
+
if (sayItem.reach !== undefined) {
|
|
63
|
+
const recipient = findParticipantName(roster, sayItem.reach.beside);
|
|
32
64
|
return recipient === undefined || sameName(recipient, actor)
|
|
33
65
|
? []
|
|
34
|
-
: [{ item: sayItem, recipient,
|
|
66
|
+
: [{ item: sayItem, recipient, route: 'beside' }];
|
|
35
67
|
}
|
|
36
|
-
const mentions = uniqueKnownMentions(sayItem.
|
|
68
|
+
const mentions = uniqueKnownMentions(sayItem.body, roster).filter((recipient) => !sameName(recipient, actor));
|
|
37
69
|
const recipients = mentions.length > 0
|
|
38
70
|
? mentions
|
|
39
71
|
: roster.filter((recipient) => !sameName(recipient, actor));
|
|
40
|
-
const
|
|
41
|
-
return recipients.map((recipient) => ({ item: sayItem, recipient,
|
|
72
|
+
const route = mentions.length > 0 ? 'mention' : 'broadcast';
|
|
73
|
+
return recipients.map((recipient) => ({ item: sayItem, recipient, route }));
|
|
42
74
|
}
|
|
43
75
|
function pendingFor(requestedRecipient) {
|
|
44
76
|
const recipient = findParticipantName(roster, requestedRecipient);
|
|
@@ -54,15 +86,15 @@ export function deriveDeliveryModel(doc) {
|
|
|
54
86
|
// avoids allocating one planned notification per participant per activity.
|
|
55
87
|
if (act.reach === undefined && extractMentions(act.body).length === 0)
|
|
56
88
|
continue;
|
|
57
|
-
for (const planned of plan(
|
|
58
|
-
if (planned.
|
|
89
|
+
for (const planned of plan(act)) {
|
|
90
|
+
if (planned.route === 'broadcast')
|
|
59
91
|
continue;
|
|
60
92
|
const joinedAt = joinedAfter.get(planned.recipient);
|
|
61
93
|
if (joinedAt === undefined || act.index <= joinedAt)
|
|
62
94
|
continue;
|
|
63
95
|
if (isDeliveryDelivered(doc, planned.recipient, act.index))
|
|
64
96
|
continue;
|
|
65
|
-
pendingByRecipient.get(planned.recipient)?.push({ ...planned,
|
|
97
|
+
pendingByRecipient.get(planned.recipient)?.push({ ...planned, route: planned.route });
|
|
66
98
|
}
|
|
67
99
|
}
|
|
68
100
|
}
|
|
@@ -73,21 +105,39 @@ export function deriveDeliveryModel(doc) {
|
|
|
73
105
|
export function planActNotifications(doc, item) {
|
|
74
106
|
return deriveDeliveryModel(doc).plan(item);
|
|
75
107
|
}
|
|
108
|
+
/** Mark only the directed notifications selected by the canonical pending projection. */
|
|
109
|
+
export function markDeliveredNotifications(doc, recipient, delivered, at = Date.now()) {
|
|
110
|
+
const deliveredIndexes = new Set(delivered.map((item) => item.index));
|
|
111
|
+
let changed = false;
|
|
112
|
+
for (const notification of deriveDeliveryModel(doc).pendingFor(recipient)) {
|
|
113
|
+
if (!deliveredIndexes.has(notification.item.index))
|
|
114
|
+
continue;
|
|
115
|
+
changed = recordDeliveredDelivery(doc, notification.recipient, notification.item.index, { at }) || changed;
|
|
116
|
+
}
|
|
117
|
+
return changed;
|
|
118
|
+
}
|
|
76
119
|
/** Canonical say-activity filter shared by catch selection and hook ownership. */
|
|
77
120
|
export function matchesCatchFilter(activity, filter) {
|
|
78
|
-
if (activity.bell)
|
|
121
|
+
if (activity.reach === 'bell')
|
|
79
122
|
return true;
|
|
80
123
|
if (filter.participants !== undefined &&
|
|
81
124
|
!filter.participants.some((participant) => sameName(participant, activity.actor))) {
|
|
82
125
|
return false;
|
|
83
126
|
}
|
|
84
|
-
return filter.mention === undefined || matchesMentionTarget(
|
|
127
|
+
return filter.mention === undefined || matchesMentionTarget(activity, filter.mention);
|
|
85
128
|
}
|
|
86
129
|
/** True only when the live catch's own filters would deliver this notification. */
|
|
87
130
|
export function leaseOwnsNotification(lease, notification) {
|
|
131
|
+
const recipient = notification.recipient;
|
|
132
|
+
if (notification.route === 'beside' && recipient === undefined)
|
|
133
|
+
return false;
|
|
88
134
|
return matchesCatchFilter({
|
|
89
135
|
actor: notification.actor,
|
|
90
136
|
body: notification.body,
|
|
91
|
-
|
|
137
|
+
reach: notification.route === 'bell'
|
|
138
|
+
? 'bell'
|
|
139
|
+
: notification.route === 'beside'
|
|
140
|
+
? { beside: recipient }
|
|
141
|
+
: undefined,
|
|
92
142
|
}, lease.filter ?? {});
|
|
93
143
|
}
|
package/dist/doctor.js
CHANGED
|
@@ -4,17 +4,19 @@ export function planRepair(text) {
|
|
|
4
4
|
if (diagnosis.unfixable)
|
|
5
5
|
return { diagnosis };
|
|
6
6
|
const actions = [];
|
|
7
|
-
const
|
|
8
|
-
|
|
7
|
+
const diagnosedFirstIndex = diagnosis.acts[0]?.act.index ?? 0;
|
|
8
|
+
const preservesStableIndexes = diagnosis.acts.every(({ act }, index) => act.index === diagnosedFirstIndex + index);
|
|
9
|
+
const acts = diagnosis.acts.map(({ act }, index) => ({
|
|
10
|
+
...act,
|
|
11
|
+
index: preservesStableIndexes ? act.index : index,
|
|
12
|
+
}));
|
|
13
|
+
if (!preservesStableIndexes) {
|
|
9
14
|
actions.push({ message: 'renumbered act indexes to be contiguous' });
|
|
10
15
|
}
|
|
11
16
|
if (diagnosis.quarantined.length > 0) {
|
|
12
17
|
actions.push({ message: `quarantined ${diagnosis.quarantined.length} unparseable act block(s)` });
|
|
13
18
|
}
|
|
14
|
-
|
|
15
|
-
actions.push({ message: 'dropped frontmatter participants; roster now derives from join/done acts' });
|
|
16
|
-
}
|
|
17
|
-
const nextActIndex = acts.length;
|
|
19
|
+
const nextActIndex = acts.length > 0 ? acts[acts.length - 1].index + 1 : 0;
|
|
18
20
|
const doc = {
|
|
19
21
|
hardCap: diagnosis.hardCap,
|
|
20
22
|
throttlePerMinute: diagnosis.throttlePerMinute,
|
|
@@ -24,9 +26,8 @@ export function planRepair(text) {
|
|
|
24
26
|
runtime: {
|
|
25
27
|
version: 2,
|
|
26
28
|
nextActIndex,
|
|
27
|
-
firstActIndex: acts.length > 0 ? (acts[0].index ?? 0) : 0,
|
|
28
29
|
cursors: {},
|
|
29
|
-
|
|
30
|
+
deliveryReceipts: {},
|
|
30
31
|
leases: {},
|
|
31
32
|
},
|
|
32
33
|
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { SQUARE_IDENTITY } from './identity.js';
|
|
6
|
+
import { stageReplacement } from './harness-stage.js';
|
|
7
|
+
export const CLAUDE_PLUGIN_ID = SQUARE_IDENTITY.pluginId;
|
|
8
|
+
export const CLAUDE_MARKETPLACE_NAME = SQUARE_IDENTITY.marketplaceName;
|
|
9
|
+
export function claudeMarketplaceRoot(homeDir) {
|
|
10
|
+
return path.join(homeDir, '.square', 'claude', 'marketplaces', CLAUDE_MARKETPLACE_NAME);
|
|
11
|
+
}
|
|
12
|
+
function runClaude(homeDir, args) {
|
|
13
|
+
const result = spawnSync(process.env.SQUARE_CLAUDE_BIN || 'claude', args, {
|
|
14
|
+
encoding: 'utf8', env: { ...process.env, HOME: homeDir, CLAUDE_CONFIG_DIR: path.join(homeDir, '.claude') }, timeout: 30_000,
|
|
15
|
+
});
|
|
16
|
+
if (result.error)
|
|
17
|
+
throw result.error;
|
|
18
|
+
return { status: result.status ?? 1, stdout: result.stdout || '', stderr: result.stderr || '' };
|
|
19
|
+
}
|
|
20
|
+
function requireSuccess(result, operation, allowMissing = false) {
|
|
21
|
+
if (result.status === 0 || (allowMissing && /not configured|not installed|not found/i.test(result.stderr)))
|
|
22
|
+
return;
|
|
23
|
+
throw new Error(`Claude ${operation} failed: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.status}`}`);
|
|
24
|
+
}
|
|
25
|
+
function writeJson(file, value) {
|
|
26
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
27
|
+
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
28
|
+
}
|
|
29
|
+
export async function installClaudePlugin(homeDir, run = runClaude) {
|
|
30
|
+
const marketplaceRoot = claudeMarketplaceRoot(homeDir);
|
|
31
|
+
const staged = stageReplacement(marketplaceRoot, (stage) => {
|
|
32
|
+
const plugin = path.join(stage, 'plugins', SQUARE_IDENTITY.pluginName);
|
|
33
|
+
fs.cpSync(fileURLToPath(new URL('../skills/square/', import.meta.url)), plugin, { recursive: true });
|
|
34
|
+
writeJson(path.join(stage, '.claude-plugin', 'marketplace.json'), {
|
|
35
|
+
name: CLAUDE_MARKETPLACE_NAME,
|
|
36
|
+
plugins: [{ name: SQUARE_IDENTITY.pluginName, source: './plugins/square' }],
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
try {
|
|
40
|
+
const add = run(homeDir, ['plugin', 'marketplace', 'add', marketplaceRoot]);
|
|
41
|
+
requireSuccess(add, 'marketplace install', true);
|
|
42
|
+
requireSuccess(run(homeDir, ['plugin', 'install', CLAUDE_PLUGIN_ID]), 'plugin install');
|
|
43
|
+
requireSuccess(run(homeDir, ['plugin', 'update', CLAUDE_PLUGIN_ID]), 'plugin update');
|
|
44
|
+
staged.finalize();
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
staged.rollback();
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
return { marketplaceRoot, pluginRoot: path.join(marketplaceRoot, 'plugins', SQUARE_IDENTITY.pluginName) };
|
|
51
|
+
}
|
|
52
|
+
export async function uninstallClaudePlugin(homeDir, run = runClaude) {
|
|
53
|
+
const root = claudeMarketplaceRoot(homeDir);
|
|
54
|
+
requireSuccess(run(homeDir, ['plugin', 'remove', CLAUDE_PLUGIN_ID]), 'plugin removal', true);
|
|
55
|
+
requireSuccess(run(homeDir, ['plugin', 'marketplace', 'remove', CLAUDE_MARKETPLACE_NAME]), 'marketplace removal', true);
|
|
56
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
57
|
+
return { paths: [root], notes: [] };
|
|
58
|
+
}
|
|
59
|
+
export async function doctorClaudePlugin(homeDir, run = runClaude) {
|
|
60
|
+
const root = claudeMarketplaceRoot(homeDir);
|
|
61
|
+
const bundle = path.join(root, 'plugins', SQUARE_IDENTITY.pluginName);
|
|
62
|
+
const listed = run(homeDir, ['plugin', 'list', '--json']);
|
|
63
|
+
const installed = listed.status === 0 && listed.stdout.includes(CLAUDE_PLUGIN_ID);
|
|
64
|
+
return [
|
|
65
|
+
fs.existsSync(bundle) ? `✓ Square Claude plugin bundle ${root}` : `○ Square Claude plugin bundle missing ${root}`,
|
|
66
|
+
installed ? `✓ ${CLAUDE_PLUGIN_ID} installed` : `○ ${CLAUDE_PLUGIN_ID} unavailable`,
|
|
67
|
+
];
|
|
68
|
+
}
|