@astrosheep/square 0.3.11 → 0.3.12
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/dist/activity.js +2 -7
- package/dist/artifact.js +335 -647
- package/dist/boundary-presentation.js +1 -1
- package/dist/cli/context.js +2 -2
- package/dist/cli/harness-command.js +1 -1
- package/dist/cli/maintenance-commands.js +12 -69
- package/dist/cli/observation-commands.js +11 -18
- package/dist/cli/program.js +3 -2
- package/dist/cli/registry.js +1 -2
- package/dist/cli/square-commands.js +17 -18
- package/dist/compact.js +4 -4
- package/dist/decisions.js +11 -5
- package/dist/delivery-health.js +2 -1
- package/dist/delivery.js +11 -47
- package/dist/help.js +8 -10
- package/dist/index.js +6 -4
- package/dist/list.js +3 -47
- package/dist/model.js +0 -6
- package/dist/notifications.js +16 -8
- package/dist/presentation.js +22 -37
- package/dist/runtime.js +6 -21
- package/dist/square-application.js +9 -78
- package/dist/square-core.js +56 -9
- package/dist/wake-attempts.js +8 -4
- package/guides/participant.md +7 -174
- package/package.json +1 -3
- package/skills/brainstorm/SKILL.md +6 -6
- package/skills/square/.claude-plugin/plugin.json +1 -1
- package/skills/square/SKILL.md +23 -14
- package/skills/square-feedback/SKILL.md +7 -7
- package/dist/doctor.js +0 -36
- package/template.md +0 -4
- package/templates/architect.md +0 -4
- package/templates/brainstorm.md +0 -4
package/dist/list.js
CHANGED
|
@@ -1,37 +1,11 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { probeSquare } from './artifact.js';
|
|
4
4
|
import { inSquareCount, publicActs } from './runtime.js';
|
|
5
5
|
import { formatRelativeTime } from './time.js';
|
|
6
6
|
const DEFAULT_LIST_DEPTH = 4;
|
|
7
7
|
const LIST_SKIP_DIRS = new Set(['.git', 'node_modules', 'dist']);
|
|
8
|
-
function frontmatterOf(text) {
|
|
9
|
-
const match = text.match(/^---\n([\s\S]*?)\n---(?:\n|$)/);
|
|
10
|
-
return match ? match[1] : null;
|
|
11
|
-
}
|
|
12
|
-
function candidateFrontmatter(filePath) {
|
|
13
|
-
let fd;
|
|
14
|
-
try {
|
|
15
|
-
fd = fs.openSync(filePath, 'r');
|
|
16
|
-
const buffer = Buffer.allocUnsafe(4096);
|
|
17
|
-
const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, 0);
|
|
18
|
-
const prefix = buffer.toString('utf8', 0, bytesRead);
|
|
19
|
-
if (!prefix.startsWith('---\n'))
|
|
20
|
-
return null;
|
|
21
|
-
const frontmatter = frontmatterOf(prefix);
|
|
22
|
-
return frontmatter ?? frontmatterOf(fs.readFileSync(filePath, 'utf8'));
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
finally {
|
|
28
|
-
if (fd !== undefined)
|
|
29
|
-
fs.closeSync(fd);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
8
|
function readSquareListItem(filePath, root) {
|
|
33
|
-
let text;
|
|
34
|
-
let doc;
|
|
35
9
|
let stat;
|
|
36
10
|
try {
|
|
37
11
|
stat = fs.statSync(filePath);
|
|
@@ -39,27 +13,9 @@ function readSquareListItem(filePath, root) {
|
|
|
39
13
|
catch {
|
|
40
14
|
return null;
|
|
41
15
|
}
|
|
42
|
-
const
|
|
43
|
-
if (
|
|
44
|
-
return null;
|
|
45
|
-
if (!/^hard_cap:\s*(-1|\d+)\s*$/m.test(frontmatter))
|
|
16
|
+
const doc = probeSquare(filePath);
|
|
17
|
+
if (doc === undefined)
|
|
46
18
|
return null;
|
|
47
|
-
if (!/^format_version:\s*3\s*$/m.test(frontmatter))
|
|
48
|
-
return null;
|
|
49
|
-
try {
|
|
50
|
-
text = fs.readFileSync(filePath, 'utf8');
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
if (!text.includes('<!-- square:warmup -->') || !text.includes('<!-- square:activities -->'))
|
|
56
|
-
return null;
|
|
57
|
-
try {
|
|
58
|
-
doc = parseSquare(text);
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
19
|
const relative = path.relative(root, filePath) || path.basename(filePath);
|
|
64
20
|
return {
|
|
65
21
|
path: relative,
|
package/dist/model.js
CHANGED
|
@@ -11,12 +11,6 @@ export class SquareError extends Error {
|
|
|
11
11
|
this.name = 'SquareError';
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
-
export const WARMUP_HEADING = '## Warmup';
|
|
15
|
-
export const WARMUP_MARKER = '<!-- square:warmup -->';
|
|
16
|
-
export const ACTIVITIES_HEADING = '## Activities';
|
|
17
|
-
export const ACTIVITIES_MARKER = '<!-- square:activities -->';
|
|
18
|
-
export const ACT_MARKER_PREFIX = '<!-- square:act';
|
|
19
|
-
export const CURRENT_FORMAT_VERSION = 3;
|
|
20
14
|
export function formatHardCap(hardCap) {
|
|
21
15
|
return hardCap === null ? '-1' : String(hardCap);
|
|
22
16
|
}
|
package/dist/notifications.js
CHANGED
|
@@ -4,11 +4,12 @@ import { homedir } from 'node:os';
|
|
|
4
4
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { loadSquare } from './artifact.js';
|
|
7
|
-
import { deriveDeliveryModel, isDeliveryDelivered,
|
|
7
|
+
import { deriveDeliveryModel, isDeliveryDelivered, leaseOwnsNotification, planActNotifications, } from './delivery.js';
|
|
8
8
|
import { sessionInbox } from './inbox.js';
|
|
9
9
|
import { hasPresentedAttention } from './presented.js';
|
|
10
10
|
import { nameKey, SquareError } from './model.js';
|
|
11
11
|
import { SLEEP_MS, matchesMentionTarget, resolveRosterName, rosterNames } from './runtime.js';
|
|
12
|
+
import { formatActivityId, parseActivityId } from './square-core.js';
|
|
12
13
|
import { PaseoAdapter } from './paseo-delivery.js';
|
|
13
14
|
import { quoteShell } from './presentation.js';
|
|
14
15
|
import { lookupParticipant } from './registry.js';
|
|
@@ -42,7 +43,7 @@ function renderWakePayload(request) {
|
|
|
42
43
|
: request.squarePath;
|
|
43
44
|
return [
|
|
44
45
|
'<system-reminder source="square">',
|
|
45
|
-
`${request.route === 'bell' ? 'Bell' :
|
|
46
|
+
`${request.route === 'bell' ? 'Bell' : 'Mention'} from @${request.actor} in \`${display}\``,
|
|
46
47
|
'The native adapter will present it at the next boundary. If no native wake is available, pull from the square yourself.',
|
|
47
48
|
`\`${catchCommand(request.squarePath, request.recipient)}\``,
|
|
48
49
|
'</system-reminder>',
|
|
@@ -57,7 +58,6 @@ async function waitForCatch(route, request, body) {
|
|
|
57
58
|
actor: request.actor,
|
|
58
59
|
body,
|
|
59
60
|
route: request.route,
|
|
60
|
-
recipient: request.recipient,
|
|
61
61
|
}))
|
|
62
62
|
return false;
|
|
63
63
|
const deadline = Date.now() + 180_000;
|
|
@@ -75,14 +75,22 @@ async function waitForCatch(route, request, body) {
|
|
|
75
75
|
}
|
|
76
76
|
return false;
|
|
77
77
|
}
|
|
78
|
+
function notificationIndex(ref) {
|
|
79
|
+
if (typeof ref === 'number')
|
|
80
|
+
return ref;
|
|
81
|
+
const index = parseActivityId(ref);
|
|
82
|
+
if (index === undefined)
|
|
83
|
+
throw new Error(`Invalid act ref: ${ref}`);
|
|
84
|
+
return index;
|
|
85
|
+
}
|
|
78
86
|
export function hasDeliveredNotification(squarePath, name, ref) {
|
|
79
87
|
const doc = loadSquare(squarePath);
|
|
80
|
-
return isDeliveryDelivered(doc, known(doc, name),
|
|
88
|
+
return isDeliveryDelivered(doc, known(doc, name), notificationIndex(ref));
|
|
81
89
|
}
|
|
82
90
|
export function hasAttentionNotification(squarePath, name, ref, env = process.env) {
|
|
83
91
|
const doc = loadSquare(squarePath);
|
|
84
92
|
const recipient = known(doc, name);
|
|
85
|
-
const index =
|
|
93
|
+
const index = notificationIndex(ref);
|
|
86
94
|
return isDeliveryDelivered(doc, recipient, index) || hasPresentedAttention(squarePath, recipient, index, env);
|
|
87
95
|
}
|
|
88
96
|
export async function waitForDeliveredNotification(squarePath, name, ref, opts = {}) {
|
|
@@ -95,7 +103,7 @@ export async function waitForDeliveredNotification(squarePath, name, ref, opts =
|
|
|
95
103
|
return false;
|
|
96
104
|
}
|
|
97
105
|
function notifyLeaseKey(recipient, actIndex) {
|
|
98
|
-
return JSON.stringify([
|
|
106
|
+
return JSON.stringify([formatActivityId(actIndex), nameKey(recipient)]);
|
|
99
107
|
}
|
|
100
108
|
async function claimNotifyLease(squarePath, recipient, actIndex) {
|
|
101
109
|
const at = Date.now();
|
|
@@ -214,7 +222,7 @@ export async function processActNotificationsOnce(squarePath, actIndex, opts = {
|
|
|
214
222
|
const item = doc.acts.find((candidate) => candidate.index === actIndex);
|
|
215
223
|
if (item === undefined)
|
|
216
224
|
return;
|
|
217
|
-
const notifications = planActNotifications(doc, item)
|
|
225
|
+
const notifications = planActNotifications(doc, item);
|
|
218
226
|
await Promise.all(notifications.map((notification) => processNotification(squarePath, notification, opts)));
|
|
219
227
|
}
|
|
220
228
|
function launchWorker(workerPath, args) {
|
|
@@ -227,7 +235,7 @@ export async function dispatchActNotifications(squarePath, item, opts = {}) {
|
|
|
227
235
|
if (env.SQUARE_DISABLE_PASEO_WAKE === '1')
|
|
228
236
|
return;
|
|
229
237
|
const doc = loadSquare(squarePath);
|
|
230
|
-
if (
|
|
238
|
+
if (planActNotifications(doc, item).length === 0)
|
|
231
239
|
return;
|
|
232
240
|
(opts.launchWorker ?? launchWorker)(fileURLToPath(new URL('./cmd/notify-once.js', import.meta.url)), ['--location', squarePath, '--act-index', String(item.index)]);
|
|
233
241
|
}
|
package/dist/presentation.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { sameName } from './model.js';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
import { actId,
|
|
4
|
+
import { audienceIncludes, audienceOf, perceive } from './square-core.js';
|
|
5
|
+
import { actId, publicActs, readCursor, rosterNames, sayNumberFor } from './runtime.js';
|
|
6
6
|
import { formatDuration, formatRelativeTime, formatTimestamp } from './time.js';
|
|
7
7
|
import { grepSnippet } from './search.js';
|
|
8
8
|
function headerLine(squarePath, opts = {}) {
|
|
@@ -150,7 +150,7 @@ export function renderEventCli(event, opts = {}) {
|
|
|
150
150
|
case 'say': {
|
|
151
151
|
const body = renderedBody(event.body, maxBody);
|
|
152
152
|
const mention = opts.mention;
|
|
153
|
-
const mentionSuffix = mention !== undefined &&
|
|
153
|
+
const mentionSuffix = mention !== undefined && audienceIncludes(audienceOf(event), mention)
|
|
154
154
|
? ` · calls your name across the square — @${mention}`
|
|
155
155
|
: '';
|
|
156
156
|
const replySuffix = event.reply === undefined ? '' : ` · replies to ${actId(event.reply)}`;
|
|
@@ -165,21 +165,15 @@ export function renderEventCli(event, opts = {}) {
|
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
function renderPresenceOnlySay(event) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
function perceptionFor(history, event, viewer) {
|
|
173
|
-
const cutoff = history.findIndex((item) => actStableIndex(item) === actStableIndex(event));
|
|
174
|
-
const acts = cutoff >= 0 ? history.slice(0, cutoff) : history;
|
|
175
|
-
return perceive(fold(acts), event, viewer);
|
|
168
|
+
const audience = audienceOf(event);
|
|
169
|
+
const targets = audience.kind === 'bell' ? [] : audience.names;
|
|
170
|
+
const dest = targets.length === 0 ? '' : ` to ${targets.map((name) => `@${name}`).join(' and ')}`;
|
|
171
|
+
return `*${event.actor} walks over${dest}*`;
|
|
176
172
|
}
|
|
177
|
-
export function
|
|
173
|
+
export function renderAmbientEvent(event, viewer, opts = {}) {
|
|
178
174
|
if (event.kind !== 'say')
|
|
179
175
|
return renderEventCli(event, opts);
|
|
180
|
-
const seen =
|
|
181
|
-
if (seen === 'none')
|
|
182
|
-
return '';
|
|
176
|
+
const seen = perceive(event, viewer);
|
|
183
177
|
if (seen === 'presence')
|
|
184
178
|
return renderPresenceOnlySay(event);
|
|
185
179
|
return renderEventCli(event, opts);
|
|
@@ -194,7 +188,7 @@ function renderUnreadSummary(opts) {
|
|
|
194
188
|
return [
|
|
195
189
|
...opts.activitySummaries.flatMap((item) => [
|
|
196
190
|
...item.previews.slice(-1).map((preview) => {
|
|
197
|
-
const rendered =
|
|
191
|
+
const rendered = renderAmbientEvent(preview.act, opts.viewer, { actNumber: preview.number });
|
|
198
192
|
if (rendered === '')
|
|
199
193
|
return ` · ${item.name} spoke — ${formatAge(item.latestActivityAgeMs)} ago`;
|
|
200
194
|
if (rendered.startsWith('*'))
|
|
@@ -208,7 +202,7 @@ function renderUnreadSummary(opts) {
|
|
|
208
202
|
export function renderPendingFeed(history, publicItems, roomChanges, viewer = '') {
|
|
209
203
|
const lines = [];
|
|
210
204
|
for (const act of publicItems) {
|
|
211
|
-
const rendered =
|
|
205
|
+
const rendered = renderAmbientEvent(act, viewer, {
|
|
212
206
|
actNumber: act.kind === 'say' ? sayNumberFor(history, act) : undefined,
|
|
213
207
|
});
|
|
214
208
|
if (rendered !== '')
|
|
@@ -262,7 +256,7 @@ export function renderPublicTail(events, lastN, now, viewer = '') {
|
|
|
262
256
|
const selected = lastN == null ? publicItems : publicItems.slice(-lastN);
|
|
263
257
|
const preview = lastN == null ? undefined : BODY_PREVIEW_LENGTH;
|
|
264
258
|
return selected
|
|
265
|
-
.map((event) =>
|
|
259
|
+
.map((event) => renderAmbientEvent(event, viewer, { now, preview, actNumber: event.kind === 'say' ? sayNumberFor(events, event) : undefined }))
|
|
266
260
|
.filter(Boolean)
|
|
267
261
|
.join('\n\n');
|
|
268
262
|
}
|
|
@@ -270,7 +264,7 @@ function lastPresenceAnchor(doc, name) {
|
|
|
270
264
|
const cursor = readCursor(doc, name);
|
|
271
265
|
for (let i = doc.acts.length - 1; i >= 0; i--) {
|
|
272
266
|
const event = doc.acts[i];
|
|
273
|
-
const index =
|
|
267
|
+
const index = event.index;
|
|
274
268
|
if (index > cursor)
|
|
275
269
|
continue;
|
|
276
270
|
if (event.kind === 'say' || event.kind === 'done')
|
|
@@ -281,7 +275,7 @@ function lastPresenceAnchor(doc, name) {
|
|
|
281
275
|
function renderLastPresenceMarker(name) {
|
|
282
276
|
return `· ${name}'s footprints reach here`;
|
|
283
277
|
}
|
|
284
|
-
export function renderActivitiesView(doc, visible, lastN, full, squarePath, viewer = '') {
|
|
278
|
+
export function renderActivitiesView(doc, visible, lastN, full, squarePath, viewer = '', mode = 'ambient') {
|
|
285
279
|
const publicVisible = visible.filter((act) => act.kind === 'say' || act.kind === 'done');
|
|
286
280
|
const shown = lastN == null ? publicVisible : publicVisible.slice(-lastN);
|
|
287
281
|
const previewLen = full ? undefined : BODY_PREVIEW_LENGTH;
|
|
@@ -293,10 +287,13 @@ export function renderActivitiesView(doc, visible, lastN, full, squarePath, view
|
|
|
293
287
|
}
|
|
294
288
|
const chunks = [];
|
|
295
289
|
for (const act of shown) {
|
|
296
|
-
const
|
|
290
|
+
const opts = {
|
|
297
291
|
preview: previewLen,
|
|
298
292
|
actNumber: act.kind === 'say' ? sayNumberFor(doc.acts, act) : undefined,
|
|
299
|
-
}
|
|
293
|
+
};
|
|
294
|
+
const rendered = mode === 'archive'
|
|
295
|
+
? renderEventCli(act, opts)
|
|
296
|
+
: renderAmbientEvent(act, viewer, opts);
|
|
300
297
|
if (rendered !== '')
|
|
301
298
|
chunks.push(rendered);
|
|
302
299
|
for (const participant of markers.get(act.index) ?? []) {
|
|
@@ -306,7 +303,7 @@ export function renderActivitiesView(doc, visible, lastN, full, squarePath, view
|
|
|
306
303
|
if (chunks.length === 0)
|
|
307
304
|
return 'latest\n ○ no public activity in this view';
|
|
308
305
|
if (previewLen !== undefined) {
|
|
309
|
-
const truncated = shown.some((act) => act.kind === 'say' && act.body.length > previewLen);
|
|
306
|
+
const truncated = shown.some((act) => act.kind === 'say' && act.body.length > previewLen && (mode === 'archive' || perceive(act, viewer) === 'full'));
|
|
310
307
|
if (truncated)
|
|
311
308
|
chunks.push(`» ${commandPrefix(squarePath)} history --full`);
|
|
312
309
|
}
|
|
@@ -412,20 +409,8 @@ function renderRoomChanges(changes) {
|
|
|
412
409
|
export function renderDoctorClean() {
|
|
413
410
|
return '✓ no problems found';
|
|
414
411
|
}
|
|
415
|
-
export function renderDoctorProblems(problems) {
|
|
416
|
-
return [`✕ ${problems.length} ${pluralize(problems.length, 'problem')} found`, ...problems.map((problem) => ` · ${problem.kind}: ${problem.message}`)].join('\n');
|
|
417
|
-
}
|
|
418
412
|
export function renderDoctorUnfixable(reason) {
|
|
419
|
-
return ['✕
|
|
420
|
-
}
|
|
421
|
-
export function renderDoctorRepaired(actions, quarantinedCount, sidecarPath) {
|
|
422
|
-
if (actions.length === 0)
|
|
423
|
-
return '✓ no problems found';
|
|
424
|
-
return [
|
|
425
|
-
'✓ repaired',
|
|
426
|
-
...actions.map((action) => ` · ${action.message}`),
|
|
427
|
-
...(quarantinedCount > 0 && sidecarPath !== undefined ? [` · quarantined ${quarantinedCount} act block(s)`, ` · sidecar ${sidecarPath}`] : []),
|
|
428
|
-
].join('\n');
|
|
413
|
+
return ['✕ unreadable artifact', ` · ${reason}`].join('\n');
|
|
429
414
|
}
|
|
430
415
|
export function renderWatchOutput(history, publicItems, roomChanges, opts) {
|
|
431
416
|
const sections = [];
|
|
@@ -448,7 +433,7 @@ export function renderWatchOutput(history, publicItems, roomChanges, opts) {
|
|
|
448
433
|
sections.push(room);
|
|
449
434
|
if (publicItems.length > 0) {
|
|
450
435
|
const rendered = publicItems
|
|
451
|
-
.map((act) =>
|
|
436
|
+
.map((act) => renderAmbientEvent(act, opts.viewer, {
|
|
452
437
|
actNumber: act.kind === 'say' ? sayNumberFor(history, act) : undefined,
|
|
453
438
|
mention: opts.mention,
|
|
454
439
|
}))
|
package/dist/runtime.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fold } from './square-core.js';
|
|
1
|
+
import { audienceIncludes, audienceOf, fold, formatActivityId } from './square-core.js';
|
|
2
2
|
import { SquareError, findParticipantName, nameKey, sameName, } from './model.js';
|
|
3
3
|
function parseIntegerEnvValue(name, raw, fallback) {
|
|
4
4
|
if (raw === undefined)
|
|
@@ -51,27 +51,12 @@ export function nowMs() {
|
|
|
51
51
|
}
|
|
52
52
|
return value;
|
|
53
53
|
}
|
|
54
|
-
|
|
55
|
-
const matches = [];
|
|
56
|
-
const re = /@([\p{L}\p{N}_-]+)/gu;
|
|
57
|
-
let match;
|
|
58
|
-
while ((match = re.exec(body)) !== null)
|
|
59
|
-
matches.push(match[1]);
|
|
60
|
-
return matches;
|
|
61
|
-
}
|
|
62
|
-
/** Pure directed-activity filter. Broadcast bodies (no @) match any named viewer. */
|
|
54
|
+
/** Pure directed-activity filter. Bell matches every viewer; mentions match by audience. */
|
|
63
55
|
export function matchesMentionTarget(act, mention) {
|
|
64
|
-
|
|
65
|
-
return true;
|
|
66
|
-
if (act.reach !== undefined) {
|
|
67
|
-
return mention === true || sameName(act.reach.beside, mention);
|
|
68
|
-
}
|
|
69
|
-
const mentions = extractMentions(act.body);
|
|
56
|
+
const audience = audienceOf(act);
|
|
70
57
|
if (mention === true)
|
|
71
|
-
return
|
|
72
|
-
|
|
73
|
-
return true;
|
|
74
|
-
return mentions.some((name) => sameName(name, mention));
|
|
58
|
+
return audience.kind === 'bell' || audience.names.length > 0;
|
|
59
|
+
return audienceIncludes(audience, mention);
|
|
75
60
|
}
|
|
76
61
|
export function foldedState(doc) {
|
|
77
62
|
return fold(doc.acts);
|
|
@@ -131,7 +116,7 @@ export function actStableIndex(act) {
|
|
|
131
116
|
}
|
|
132
117
|
export function actId(actOrIndex) {
|
|
133
118
|
const index = typeof actOrIndex === 'number' ? actOrIndex : actStableIndex(actOrIndex);
|
|
134
|
-
return
|
|
119
|
+
return formatActivityId(index);
|
|
135
120
|
}
|
|
136
121
|
export function getReadState(doc, name) {
|
|
137
122
|
return doc.runtime.cursors[name] ?? Object.entries(doc.runtime.cursors).find(([participant]) => sameName(participant, name))?.[1];
|
|
@@ -1,21 +1,16 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import
|
|
3
|
-
import { emptyRuntimeState, loadRuntimeSidecar, loadSquare, mergeRuntimeState, renderArtifactAct, renderSquare, renderSquareDoc, saveRuntimeSidecar } from './artifact.js';
|
|
2
|
+
import { createSquareDoc, loadArchive, loadSquare, writeArchiveFile, writeSquareFile } from './artifact.js';
|
|
4
3
|
import { coreCompact, coreDone, coreHold, coreResume, decideAct, decideJoin, resolveKnownName } from './decisions.js';
|
|
5
|
-
import { planRepair } from './doctor.js';
|
|
6
4
|
import { withFileLock } from './file-lock.js';
|
|
7
5
|
import { stageReplacement } from './harness-stage.js';
|
|
8
6
|
import { SquareError } from './model.js';
|
|
9
7
|
import { advanceCursor, freshWatchLease, LOCK_RETRY_MS, LOCK_STALE_MS, removeWatchLease, touchPresenceCursor, watchLease, writeWatchLease } from './runtime.js';
|
|
10
|
-
/** The only
|
|
8
|
+
/** The only mutation boundary: one per-square lock around one complete snapshot commit. */
|
|
11
9
|
export async function withSquareLock(squarePath, fn) {
|
|
12
10
|
return withFileLock(`${squarePath}.lock`, { retryMs: LOCK_RETRY_MS, staleMs: LOCK_STALE_MS }, fn);
|
|
13
11
|
}
|
|
14
12
|
export function writeSquareDoc(squarePath, doc) {
|
|
15
|
-
|
|
16
|
-
fs.writeFileSync(temporary, renderSquareDoc(doc));
|
|
17
|
-
fs.renameSync(temporary, squarePath);
|
|
18
|
-
saveRuntimeSidecar(squarePath, doc.runtime);
|
|
13
|
+
writeSquareFile(squarePath, doc);
|
|
19
14
|
}
|
|
20
15
|
export function appendAct(squarePath, doc, act) {
|
|
21
16
|
const stored = applyActs(doc, [act])[0];
|
|
@@ -35,13 +30,11 @@ function applyActs(doc, acts, mutateRuntime) {
|
|
|
35
30
|
mutateRuntime?.(doc);
|
|
36
31
|
return stored;
|
|
37
32
|
}
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
*/
|
|
42
|
-
function prepareAppend(filePath, block, existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '') {
|
|
33
|
+
/** Publish a dependent archive before the main snapshot, retaining rollback evidence. */
|
|
34
|
+
function prepareArchive(filePath, acts) {
|
|
35
|
+
const existing = fs.existsSync(filePath) ? loadArchive(filePath) : [];
|
|
43
36
|
return stageReplacement(filePath, (stage) => {
|
|
44
|
-
|
|
37
|
+
writeArchiveFile(stage, [...existing, ...acts]);
|
|
45
38
|
});
|
|
46
39
|
}
|
|
47
40
|
function plan(doc, intent) {
|
|
@@ -156,27 +149,9 @@ function plan(doc, intent) {
|
|
|
156
149
|
replaceDoc: result.doc,
|
|
157
150
|
preparePersistence: archive.length === 0
|
|
158
151
|
? undefined
|
|
159
|
-
: () =>
|
|
160
|
-
const existing = fs.existsSync(intent.archivePath) ? fs.readFileSync(intent.archivePath, 'utf8') : '';
|
|
161
|
-
const block = archive
|
|
162
|
-
.map((act, index) => renderArtifactAct(act, { first: existing === '' && index === 0 }))
|
|
163
|
-
.join('\n');
|
|
164
|
-
return prepareAppend(intent.archivePath, block, existing);
|
|
165
|
-
},
|
|
152
|
+
: () => prepareArchive(intent.archivePath, archive),
|
|
166
153
|
};
|
|
167
154
|
}
|
|
168
|
-
case 'repair':
|
|
169
|
-
return {
|
|
170
|
-
result: undefined,
|
|
171
|
-
acts: [],
|
|
172
|
-
replaceDoc: intent.doc,
|
|
173
|
-
preparePersistence: intent.quarantine === undefined || intent.quarantine.blocks.length === 0
|
|
174
|
-
? undefined
|
|
175
|
-
: () => {
|
|
176
|
-
const block = intent.quarantine.blocks.join('\n\n');
|
|
177
|
-
return prepareAppend(intent.quarantine.path, block);
|
|
178
|
-
},
|
|
179
|
-
};
|
|
180
155
|
}
|
|
181
156
|
}
|
|
182
157
|
function commitPlan(squarePath, doc, planned) {
|
|
@@ -213,50 +188,6 @@ export async function createSquare(squarePath, options, snippet) {
|
|
|
213
188
|
if (fs.existsSync(squarePath) && !options.force) {
|
|
214
189
|
throw new SquareError('conflict', `Refusing to overwrite existing square: ${squarePath}\nPass -f to overwrite.`);
|
|
215
190
|
}
|
|
216
|
-
|
|
217
|
-
fs.mkdirSync(path.dirname(squarePath), { recursive: true });
|
|
218
|
-
fs.writeFileSync(temporary, renderSquare(options, snippet));
|
|
219
|
-
fs.renameSync(temporary, squarePath);
|
|
220
|
-
saveRuntimeSidecar(squarePath, emptyRuntimeState(0));
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
/** Keep artifact repair planning and dependent quarantine persistence inside the application boundary. */
|
|
224
|
-
export async function repairSquare(squarePath) {
|
|
225
|
-
const result = await withSquareLock(squarePath, () => {
|
|
226
|
-
let text;
|
|
227
|
-
try {
|
|
228
|
-
text = fs.readFileSync(squarePath, 'utf8');
|
|
229
|
-
}
|
|
230
|
-
catch (error) {
|
|
231
|
-
if (error.code === 'ENOENT')
|
|
232
|
-
throw new SquareError('not_found', `square file not found: ${squarePath}`);
|
|
233
|
-
throw error;
|
|
234
|
-
}
|
|
235
|
-
const repair = planRepair(text);
|
|
236
|
-
if (repair.diagnosis.unfixable || repair.repaired === undefined)
|
|
237
|
-
return { repair };
|
|
238
|
-
// Repair changes Markdown only. Keep the sidecar's runtime metadata and
|
|
239
|
-
// merge history boundaries so a doctor run cannot erase delivery state or
|
|
240
|
-
// reuse a stable activity index.
|
|
241
|
-
const sidecarRuntime = loadRuntimeSidecar(squarePath, repair.repaired.doc.runtime);
|
|
242
|
-
const indexesPreserved = repair.diagnosis.acts.every(({ act }, index) => repair.repaired.doc.acts[index]?.index === act.index);
|
|
243
|
-
if (indexesPreserved) {
|
|
244
|
-
repair.repaired.doc.runtime = mergeRuntimeState(repair.repaired.doc.runtime, sidecarRuntime);
|
|
245
|
-
}
|
|
246
|
-
else {
|
|
247
|
-
repair.repaired.doc.runtime = emptyRuntimeState(Math.max(repair.repaired.doc.runtime.nextActIndex, sidecarRuntime.nextActIndex));
|
|
248
|
-
repair.repaired.actions.push({ message: 'reset runtime delivery metadata because act indexes changed' });
|
|
249
|
-
}
|
|
250
|
-
const quarantinePath = squarePath.replace(/\.md$/, '') + '.quarantine.md';
|
|
251
|
-
const intent = {
|
|
252
|
-
type: 'repair',
|
|
253
|
-
doc: repair.repaired.doc,
|
|
254
|
-
...(repair.repaired.quarantinedBlocks.length === 0
|
|
255
|
-
? {}
|
|
256
|
-
: { quarantine: { path: quarantinePath, blocks: repair.repaired.quarantinedBlocks } }),
|
|
257
|
-
};
|
|
258
|
-
commitPlan(squarePath, repair.repaired.doc, plan(repair.repaired.doc, intent));
|
|
259
|
-
return { repair };
|
|
191
|
+
writeSquareFile(squarePath, createSquareDoc(options, snippet));
|
|
260
192
|
});
|
|
261
|
-
return result.repair;
|
|
262
193
|
}
|
package/dist/square-core.js
CHANGED
|
@@ -1,9 +1,62 @@
|
|
|
1
|
+
export function formatActivityId(index) {
|
|
2
|
+
if (!Number.isSafeInteger(index) || index < 0) {
|
|
3
|
+
throw new Error(`Invalid activity index: ${index}`);
|
|
4
|
+
}
|
|
5
|
+
return `act/${index}`;
|
|
6
|
+
}
|
|
7
|
+
export function parseActivityId(value) {
|
|
8
|
+
if (value === 'act/0')
|
|
9
|
+
return 0;
|
|
10
|
+
if (typeof value !== 'string' || !/^act\/[1-9]\d*$/.test(value))
|
|
11
|
+
return undefined;
|
|
12
|
+
const index = Number(value.slice(4));
|
|
13
|
+
return Number.isSafeInteger(index) ? index : undefined;
|
|
14
|
+
}
|
|
1
15
|
function nameKey(name) {
|
|
2
16
|
return name.toLocaleLowerCase();
|
|
3
17
|
}
|
|
4
18
|
function sameName(a, b) {
|
|
5
19
|
return nameKey(a) === nameKey(b);
|
|
6
20
|
}
|
|
21
|
+
export function extractMentions(body) {
|
|
22
|
+
const matches = [];
|
|
23
|
+
const re = /@([\p{L}\p{N}_-]+)/gu;
|
|
24
|
+
let match;
|
|
25
|
+
while ((match = re.exec(body)) !== null)
|
|
26
|
+
matches.push(match[1]);
|
|
27
|
+
return matches;
|
|
28
|
+
}
|
|
29
|
+
function uniqueMentionNames(names) {
|
|
30
|
+
const unique = [];
|
|
31
|
+
for (const name of names) {
|
|
32
|
+
if (unique.some((existing) => sameName(existing, name)))
|
|
33
|
+
continue;
|
|
34
|
+
unique.push(name);
|
|
35
|
+
}
|
|
36
|
+
return unique;
|
|
37
|
+
}
|
|
38
|
+
export function audienceOf(say) {
|
|
39
|
+
if (say.reach === 'bell')
|
|
40
|
+
return { kind: 'bell' };
|
|
41
|
+
return { kind: 'mentions', names: uniqueMentionNames(extractMentions(say.body)) };
|
|
42
|
+
}
|
|
43
|
+
export function audienceIncludes(audience, name) {
|
|
44
|
+
if (audience.kind === 'bell')
|
|
45
|
+
return true;
|
|
46
|
+
return audience.names.some((mentioned) => sameName(mentioned, name));
|
|
47
|
+
}
|
|
48
|
+
export function resolveAudience(audience, candidateNames) {
|
|
49
|
+
if (audience.kind === 'bell')
|
|
50
|
+
return [...candidateNames];
|
|
51
|
+
const resolved = [];
|
|
52
|
+
for (const mention of audience.names) {
|
|
53
|
+
const known = candidateNames.find((candidate) => sameName(candidate, mention));
|
|
54
|
+
if (known !== undefined && !resolved.some((existing) => sameName(existing, known))) {
|
|
55
|
+
resolved.push(known);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return resolved;
|
|
59
|
+
}
|
|
7
60
|
function actorOf(act) {
|
|
8
61
|
if ('actor' in act && typeof act.actor === 'string')
|
|
9
62
|
return act.actor;
|
|
@@ -160,16 +213,10 @@ export function validate(state, act, options = {}) {
|
|
|
160
213
|
return { ok: true };
|
|
161
214
|
}
|
|
162
215
|
}
|
|
163
|
-
export function perceive(
|
|
164
|
-
void state;
|
|
216
|
+
export function perceive(act, viewer) {
|
|
165
217
|
if (act.kind !== 'say')
|
|
166
218
|
return 'full';
|
|
167
|
-
|
|
168
|
-
if (sameName(actor, viewer))
|
|
169
|
-
return 'full';
|
|
170
|
-
if (act.reach === undefined || act.reach === 'bell')
|
|
171
|
-
return 'full';
|
|
172
|
-
if (sameName(act.reach.beside, viewer))
|
|
219
|
+
if (sameName(act.actor, viewer))
|
|
173
220
|
return 'full';
|
|
174
|
-
return 'presence';
|
|
221
|
+
return audienceIncludes(audienceOf(act), viewer) ? 'full' : 'presence';
|
|
175
222
|
}
|
package/dist/wake-attempts.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { withFileLockSync } from './file-lock.js';
|
|
5
5
|
import { isWakeRouteKind, nameKey } from './model.js';
|
|
6
6
|
import { canonicalSquarePath } from './registry.js';
|
|
7
|
+
import { formatActivityId, parseActivityId } from './square-core.js';
|
|
7
8
|
const RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
8
9
|
const LOCK_STALE_MS = 5 * 60 * 1000;
|
|
9
10
|
const LOCK_RETRY_MS = 10;
|
|
@@ -12,7 +13,7 @@ export function wakeAttemptsPath(env = process.env) {
|
|
|
12
13
|
return env.SQUARE_WAKE_ATTEMPTS || path.join(os.homedir(), '.square', 'wake-attempts.ndjsonl');
|
|
13
14
|
}
|
|
14
15
|
export function wakeAttentionKey(attention) {
|
|
15
|
-
return JSON.stringify([canonicalSquarePath(attention.squarePath),
|
|
16
|
+
return JSON.stringify([canonicalSquarePath(attention.squarePath), formatActivityId(attention.actIndex), nameKey(attention.recipient)]);
|
|
16
17
|
}
|
|
17
18
|
function parseRow(raw, now) {
|
|
18
19
|
let value;
|
|
@@ -27,7 +28,7 @@ function parseRow(raw, now) {
|
|
|
27
28
|
const row = value;
|
|
28
29
|
if (row.v !== 1 || typeof row.ts !== 'number' || !Number.isFinite(row.ts) || row.ts > now || now - row.ts > RETENTION_MS ||
|
|
29
30
|
row.attention === undefined || typeof row.attention.square_path !== 'string' || row.attention.square_path === '' ||
|
|
30
|
-
typeof row.attention.act_id !== 'string' ||
|
|
31
|
+
typeof row.attention.act_id !== 'string' || parseActivityId(row.attention.act_id) === undefined ||
|
|
31
32
|
typeof row.attention.recipient !== 'string' || row.attention.recipient === '' ||
|
|
32
33
|
typeof row.outcome !== 'string' || !VALID_OUTCOMES.has(row.outcome) ||
|
|
33
34
|
typeof row.attempt_n !== 'number' || !Number.isInteger(row.attempt_n) || row.attempt_n <= 0 ||
|
|
@@ -62,11 +63,14 @@ function writeRows(filePath, rows) {
|
|
|
62
63
|
fs.renameSync(temporary, filePath);
|
|
63
64
|
}
|
|
64
65
|
function fromRow(row) {
|
|
66
|
+
const actIndex = parseActivityId(row.attention.act_id);
|
|
67
|
+
if (actIndex === undefined)
|
|
68
|
+
throw new Error(`Invalid wake activity id: ${row.attention.act_id}`);
|
|
65
69
|
return {
|
|
66
70
|
at: row.ts,
|
|
67
71
|
attention: {
|
|
68
72
|
squarePath: canonicalSquarePath(row.attention.square_path),
|
|
69
|
-
actIndex
|
|
73
|
+
actIndex,
|
|
70
74
|
recipient: row.attention.recipient,
|
|
71
75
|
},
|
|
72
76
|
routeKind: row.route_kind,
|
|
@@ -121,7 +125,7 @@ function toRow(attempt, env) {
|
|
|
121
125
|
ts: safe.at,
|
|
122
126
|
attention: {
|
|
123
127
|
square_path: canonicalSquarePath(safe.attention.squarePath),
|
|
124
|
-
act_id:
|
|
128
|
+
act_id: formatActivityId(safe.attention.actIndex),
|
|
125
129
|
recipient: safe.attention.recipient,
|
|
126
130
|
},
|
|
127
131
|
route_kind: safe.routeKind,
|