@astrosheep/square 0.3.5 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/codex-plugin/.codex-plugin/plugin.json +3 -2
- package/codex-plugin/hooks/hooks.json +3 -14
- package/dist/activity-feed.js +26 -18
- package/dist/activity.js +10 -10
- package/dist/artifact.js +138 -203
- package/dist/boundary-presentation.js +77 -0
- package/dist/claude-hook.js +4 -94
- package/dist/cli/context.js +7 -7
- package/dist/cli/maintenance-commands.js +10 -26
- package/dist/cli/meta-commands.js +3 -6
- package/dist/cli/observation-commands.js +48 -53
- package/dist/cli/program.js +4 -4
- package/dist/cli/registry.js +5 -5
- package/dist/cli/square-commands.js +27 -20
- package/dist/cmd/notify-once.js +23 -21
- package/dist/codex-hook.js +22 -0
- package/dist/compact.js +1 -1
- package/dist/decisions.js +61 -88
- package/dist/delivery-health.js +104 -210
- package/dist/delivery.js +68 -18
- package/dist/doctor.js +9 -8
- package/dist/harness-claude.js +38 -245
- package/dist/harness-codex.js +82 -616
- package/dist/harness-stage.js +36 -0
- package/dist/harness.js +3 -5
- package/dist/help.js +43 -35
- package/dist/inbox.js +12 -11
- package/dist/index.js +10 -121
- package/dist/list.js +1 -1
- package/dist/model.js +0 -6
- package/dist/notification-failures.js +54 -0
- package/dist/notifications.js +47 -62
- package/dist/paseo-delivery.js +160 -0
- package/dist/paseo-state.js +31 -0
- package/dist/paseo-timeline.js +58 -188
- package/dist/presentation.js +57 -64
- package/dist/presented.js +9 -8
- package/dist/registry.js +55 -45
- package/dist/runtime.js +27 -84
- package/dist/square-application.js +135 -130
- package/dist/square-core.js +3 -11
- package/dist/stream.js +27 -126
- package/dist/wake-sink.js +3 -214
- package/dist/watch.js +65 -122
- package/extensions/square-opencode.js +8 -73
- package/extensions/square-pi.js +8 -132
- package/guides/architect.md +3 -3
- package/guides/participant.md +25 -16
- package/package.json +2 -2
- package/skills/brainstorm/SKILL.md +25 -32
- package/skills/square/.claude-plugin/plugin.json +1 -1
- package/skills/square/SKILL.md +39 -107
- package/skills/square/hooks/hooks.json +2 -13
- package/skills/square-feedback/SKILL.md +4 -4
- package/dist/harness-lifecycle.js +0 -102
- package/dist/square-store.js +0 -111
- package/dist/terminal.js +0 -125
package/dist/artifact.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { ACTIVITIES_HEADING, ACTIVITIES_MARKER, ACT_MARKER_PREFIX, WARMUP_HEADING, WARMUP_MARKER, SquareError, CURRENT_FORMAT_VERSION, formatHardCap, sameName, } from './model.js';
|
|
3
3
|
import { formatTimestamp, parseTimestamp } from './time.js';
|
|
4
|
-
const V1_EVENT_MARKER_PREFIX = '<!-- square:event';
|
|
5
4
|
const V2_KINDS = new Set(['say', 'join', 'done', 'hold', 'resume']);
|
|
6
5
|
export function quoteBody(body) {
|
|
7
6
|
const normalized = body.replace(/\r\n/g, '\n').trim();
|
|
@@ -44,6 +43,7 @@ function renderActMarker(act) {
|
|
|
44
43
|
actor: act.actor,
|
|
45
44
|
at: act.at,
|
|
46
45
|
...(act.kind === 'say' && act.reach !== undefined ? { reach: act.reach } : {}),
|
|
46
|
+
...(act.kind === 'say' && act.reply !== undefined ? { reply: act.reply } : {}),
|
|
47
47
|
};
|
|
48
48
|
return `${ACT_MARKER_PREFIX} ${JSON.stringify(marker)} -->`;
|
|
49
49
|
}
|
|
@@ -61,9 +61,8 @@ export function emptyRuntimeState(nextActIndex = 0) {
|
|
|
61
61
|
return {
|
|
62
62
|
version: 2,
|
|
63
63
|
nextActIndex,
|
|
64
|
-
firstActIndex: 0,
|
|
65
64
|
cursors: {},
|
|
66
|
-
|
|
65
|
+
deliveryReceipts: {},
|
|
67
66
|
leases: {},
|
|
68
67
|
};
|
|
69
68
|
}
|
|
@@ -117,25 +116,75 @@ export function renderSquare(opts, snippet) {
|
|
|
117
116
|
function sidecarPath(squarePath) {
|
|
118
117
|
return `${squarePath}.runtime.json`;
|
|
119
118
|
}
|
|
120
|
-
function
|
|
119
|
+
function invalidRuntimeSidecar(squarePath, detail) {
|
|
120
|
+
return new SquareError('invalid_args', `Invalid square runtime sidecar ${sidecarPath(squarePath)}: ${detail}`);
|
|
121
|
+
}
|
|
122
|
+
function validateRuntimeSidecar(squarePath, value) {
|
|
123
|
+
if (!isObject(value))
|
|
124
|
+
throw invalidRuntimeSidecar(squarePath, 'expected a JSON object.');
|
|
125
|
+
if (value.version !== 2)
|
|
126
|
+
throw invalidRuntimeSidecar(squarePath, 'unsupported or missing version.');
|
|
127
|
+
if (typeof value.nextActIndex !== 'number' || !Number.isInteger(value.nextActIndex) || value.nextActIndex < 0) {
|
|
128
|
+
throw invalidRuntimeSidecar(squarePath, 'nextActIndex must be a non-negative integer.');
|
|
129
|
+
}
|
|
130
|
+
if (!isObject(value.cursors) || !Object.values(value.cursors).every(isReadCursor)) {
|
|
131
|
+
throw invalidRuntimeSidecar(squarePath, 'cursors contains an invalid read cursor.');
|
|
132
|
+
}
|
|
133
|
+
if (!isObject(value.deliveryReceipts) || !Object.values(value.deliveryReceipts).every(isDeliveryReceiptMap)) {
|
|
134
|
+
throw invalidRuntimeSidecar(squarePath, 'deliveryReceipts contains an invalid receipt map.');
|
|
135
|
+
}
|
|
136
|
+
if (!isObject(value.leases) || !Object.values(value.leases).every(isWatchLease)) {
|
|
137
|
+
throw invalidRuntimeSidecar(squarePath, 'leases contains an invalid watch lease.');
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
version: 2,
|
|
141
|
+
nextActIndex: value.nextActIndex,
|
|
142
|
+
cursors: value.cursors,
|
|
143
|
+
deliveryReceipts: value.deliveryReceipts,
|
|
144
|
+
leases: value.leases,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
export function loadRuntimeSidecar(squarePath, fallbackRuntime) {
|
|
121
148
|
const sp = sidecarPath(squarePath);
|
|
122
149
|
try {
|
|
123
150
|
const raw = fs.readFileSync(sp, 'utf8');
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
151
|
+
let parsed;
|
|
152
|
+
try {
|
|
153
|
+
parsed = JSON.parse(raw);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
throw invalidRuntimeSidecar(squarePath, 'malformed JSON.');
|
|
127
157
|
}
|
|
128
|
-
return parsed;
|
|
158
|
+
return validateRuntimeSidecar(squarePath, parsed);
|
|
129
159
|
}
|
|
130
160
|
catch (err) {
|
|
131
161
|
if (err.code === 'ENOENT') {
|
|
132
|
-
return
|
|
162
|
+
return fallbackRuntime;
|
|
133
163
|
}
|
|
134
164
|
throw err;
|
|
135
165
|
}
|
|
136
166
|
}
|
|
167
|
+
export function mergeRuntimeState(markdownRuntime, sidecarRuntime) {
|
|
168
|
+
return {
|
|
169
|
+
...sidecarRuntime,
|
|
170
|
+
nextActIndex: Math.max(markdownRuntime.nextActIndex, sidecarRuntime.nextActIndex),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
137
173
|
export function saveRuntimeSidecar(squarePath, runtime) {
|
|
138
|
-
|
|
174
|
+
const target = sidecarPath(squarePath);
|
|
175
|
+
const normalized = validateRuntimeSidecar(squarePath, runtime);
|
|
176
|
+
const temporary = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
177
|
+
try {
|
|
178
|
+
fs.writeFileSync(temporary, JSON.stringify(normalized, null, 2));
|
|
179
|
+
fs.renameSync(temporary, target);
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
try {
|
|
183
|
+
fs.unlinkSync(temporary);
|
|
184
|
+
}
|
|
185
|
+
catch { }
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
139
188
|
}
|
|
140
189
|
export function renderSquareDoc(doc) {
|
|
141
190
|
const warmup = renderWarmupSection(doc.warmup);
|
|
@@ -182,25 +231,6 @@ function parseFrontmatter(text) {
|
|
|
182
231
|
throw new SquareError('invalid_args', 'Invalid square: missing frontmatter.');
|
|
183
232
|
return match[1];
|
|
184
233
|
}
|
|
185
|
-
export function parseStateBlock(frontmatter) {
|
|
186
|
-
const lines = frontmatter.split('\n');
|
|
187
|
-
const start = lines.findIndex((line) => /^mind_square_state:\s*\|\s*$/.test(line));
|
|
188
|
-
if (start < 0)
|
|
189
|
-
return undefined;
|
|
190
|
-
const body = [];
|
|
191
|
-
for (let i = start + 1; i < lines.length; i++) {
|
|
192
|
-
const line = lines[i];
|
|
193
|
-
if (!line.startsWith(' ') && !line.startsWith('\t') && /^[A-Za-z_][A-Za-z0-9_-]*:/.test(line))
|
|
194
|
-
break;
|
|
195
|
-
if (line.startsWith(' '))
|
|
196
|
-
body.push(line.slice(2));
|
|
197
|
-
else if (line.trim() === '')
|
|
198
|
-
body.push('');
|
|
199
|
-
else
|
|
200
|
-
break;
|
|
201
|
-
}
|
|
202
|
-
return body.join('\n').trim();
|
|
203
|
-
}
|
|
204
234
|
export function isObject(value) {
|
|
205
235
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
206
236
|
}
|
|
@@ -211,13 +241,12 @@ export function isReadCursor(value) {
|
|
|
211
241
|
Number.isInteger(value.consumedThroughIndex) &&
|
|
212
242
|
value.consumedThroughIndex >= -1 &&
|
|
213
243
|
typeof value.updatedAt === 'number' &&
|
|
214
|
-
Number.isFinite(value.updatedAt)
|
|
215
|
-
(value.source === 'join' || value.source === 'watch' || value.source === 'api'));
|
|
244
|
+
Number.isFinite(value.updatedAt));
|
|
216
245
|
}
|
|
217
|
-
export function
|
|
246
|
+
export function isDeliveryReceipt(value) {
|
|
218
247
|
if (!isObject(value))
|
|
219
248
|
return false;
|
|
220
|
-
if (value.status !== 'delivered'
|
|
249
|
+
if (value.status !== 'delivered')
|
|
221
250
|
return false;
|
|
222
251
|
if (typeof value.at !== 'number' || !Number.isFinite(value.at))
|
|
223
252
|
return false;
|
|
@@ -229,14 +258,16 @@ export function isMentionReceipt(value) {
|
|
|
229
258
|
return false;
|
|
230
259
|
return true;
|
|
231
260
|
}
|
|
232
|
-
export function
|
|
233
|
-
return isObject(value) && Object.entries(value).every(([id, receipt]) => /^act_\d+$/.test(id) &&
|
|
261
|
+
export function isDeliveryReceiptMap(value) {
|
|
262
|
+
return isObject(value) && Object.entries(value).every(([id, receipt]) => /^act_\d+$/.test(id) && isDeliveryReceipt(receipt));
|
|
234
263
|
}
|
|
235
264
|
export function isWatchLease(value) {
|
|
236
265
|
if (!isObject(value))
|
|
237
266
|
return false;
|
|
238
267
|
if (typeof value.leaseId !== 'string' || value.leaseId === '')
|
|
239
268
|
return false;
|
|
269
|
+
if (value.ownerId !== undefined && (typeof value.ownerId !== 'string' || value.ownerId === ''))
|
|
270
|
+
return false;
|
|
240
271
|
if (typeof value.heartbeatAt !== 'number' || !Number.isFinite(value.heartbeatAt))
|
|
241
272
|
return false;
|
|
242
273
|
if (typeof value.expiresAt !== 'number' || !Number.isFinite(value.expiresAt))
|
|
@@ -293,9 +324,8 @@ export function parseFormatVersion(text) {
|
|
|
293
324
|
export function parseSquare(text) {
|
|
294
325
|
parseFormatVersion(text);
|
|
295
326
|
const runtime = emptyRuntimeState(0);
|
|
296
|
-
const parsedActs = parseActs(text
|
|
327
|
+
const parsedActs = parseActs(text);
|
|
297
328
|
runtime.nextActIndex = parsedActs.nextActIndex;
|
|
298
|
-
runtime.firstActIndex = parsedActs.firstActIndex;
|
|
299
329
|
return {
|
|
300
330
|
hardCap: parseCap(text),
|
|
301
331
|
throttlePerMinute: parseThrottle(text),
|
|
@@ -317,12 +347,12 @@ export function loadSquare(squarePath) {
|
|
|
317
347
|
throw err;
|
|
318
348
|
}
|
|
319
349
|
const doc = parseSquare(text);
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
doc.runtime
|
|
350
|
+
const markdownRuntime = doc.runtime;
|
|
351
|
+
const sidecar = loadRuntimeSidecar(squarePath, markdownRuntime);
|
|
352
|
+
// Markdown owns the activity history; runtime state owns delivery metadata.
|
|
353
|
+
// Preserve whichever history boundary is furthest ahead so a missing or
|
|
354
|
+
// stale sidecar can never cause an index to be reused.
|
|
355
|
+
doc.runtime = mergeRuntimeState(markdownRuntime, sidecar);
|
|
326
356
|
return doc;
|
|
327
357
|
}
|
|
328
358
|
function isSquareMarker(line) {
|
|
@@ -381,8 +411,15 @@ export function parseActMarker(line) {
|
|
|
381
411
|
...(typeof parsed.actor === 'string' ? { actor: parsed.actor } : {}),
|
|
382
412
|
...(typeof parsed.at === 'number' && Number.isFinite(parsed.at) ? { at: parsed.at } : {}),
|
|
383
413
|
...(parsed.reach !== undefined ? { reach: parseReach(parsed.reach) } : {}),
|
|
414
|
+
...(parsed.reply !== undefined ? { reply: parseReply(parsed.reply) } : {}),
|
|
384
415
|
};
|
|
385
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
|
+
}
|
|
386
423
|
function normalizeActMeta(marker, kind, actor, head) {
|
|
387
424
|
if (marker.kind === undefined || marker.actor === undefined) {
|
|
388
425
|
throw new SquareError('invalid_args', 'Invalid square: act marker is missing kind/actor metadata.');
|
|
@@ -399,7 +436,10 @@ function normalizeActMeta(marker, kind, actor, head) {
|
|
|
399
436
|
if (marker.at !== head.at) {
|
|
400
437
|
throw new SquareError('invalid_args', `Invalid square: act marker timestamp does not match ${kind} ${actor}.`);
|
|
401
438
|
}
|
|
402
|
-
|
|
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 } : {}) };
|
|
403
443
|
}
|
|
404
444
|
export function activitiesSourceLines(text) {
|
|
405
445
|
const lines = text.split('\n');
|
|
@@ -427,7 +467,32 @@ export function parseActLine(line, actor) {
|
|
|
427
467
|
function isActSeparator(lines, index) {
|
|
428
468
|
return lines[index]?.trim().startsWith(ACT_MARKER_PREFIX) === true;
|
|
429
469
|
}
|
|
430
|
-
function
|
|
470
|
+
function parseActBlock(blockLines) {
|
|
471
|
+
let i = 0;
|
|
472
|
+
const marker = parseActMarker(blockLines[i]?.trim());
|
|
473
|
+
if (!marker)
|
|
474
|
+
throw new SquareError('invalid_args', `Invalid square: expected act marker, got: ${blockLines[i] ?? ''}`);
|
|
475
|
+
i++;
|
|
476
|
+
const actor = parseParticipantHeading(blockLines[i] ?? '');
|
|
477
|
+
if (!actor)
|
|
478
|
+
throw new SquareError('invalid_args', `Invalid square: expected participant heading, got: ${blockLines[i] ?? ''}`);
|
|
479
|
+
i++;
|
|
480
|
+
if (blockLines[i] === '')
|
|
481
|
+
i++;
|
|
482
|
+
const head = parseActLine(blockLines[i] ?? '', actor);
|
|
483
|
+
if (!head)
|
|
484
|
+
throw new SquareError('invalid_args', `Invalid square: expected act line, got: ${blockLines[i] ?? ''}`);
|
|
485
|
+
i++;
|
|
486
|
+
const meta = normalizeActMeta(marker, head.kind, actor, head);
|
|
487
|
+
let body = '';
|
|
488
|
+
if (head.kind === 'say' || head.kind === 'done' || head.kind === 'hold') {
|
|
489
|
+
if (blockLines[i] === '')
|
|
490
|
+
i++;
|
|
491
|
+
body = unquoteBody(blockLines.slice(i));
|
|
492
|
+
}
|
|
493
|
+
return { ...head, body, index: meta.index, ...(meta.reach !== undefined ? { reach: meta.reach } : {}), ...(meta.reply !== undefined ? { reply: meta.reply } : {}) };
|
|
494
|
+
}
|
|
495
|
+
function parseActs(text) {
|
|
431
496
|
const lines = activitiesSourceLines(text);
|
|
432
497
|
const acts = [];
|
|
433
498
|
const indexes = new Set();
|
|
@@ -438,37 +503,18 @@ function parseActs(text, _firstActIndex) {
|
|
|
438
503
|
i++;
|
|
439
504
|
continue;
|
|
440
505
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
i
|
|
445
|
-
const actor = parseParticipantHeading(lines[i]);
|
|
446
|
-
if (!actor)
|
|
447
|
-
throw new SquareError('invalid_args', `Invalid square: expected participant heading, got: ${lines[i]}`);
|
|
448
|
-
i++;
|
|
449
|
-
if (lines[i] === '')
|
|
450
|
-
i++;
|
|
451
|
-
const head = parseActLine(lines[i], actor);
|
|
452
|
-
if (!head)
|
|
453
|
-
throw new SquareError('invalid_args', `Invalid square: expected act line, got: ${lines[i] ?? ''}`);
|
|
454
|
-
i++;
|
|
455
|
-
const meta = normalizeActMeta(marker, head.kind, actor, head);
|
|
506
|
+
let end = i + 1;
|
|
507
|
+
while (end < lines.length && !isActSeparator(lines, end))
|
|
508
|
+
end++;
|
|
509
|
+
const act = parseActBlock(lines.slice(i, end));
|
|
456
510
|
if (detectedFirst === -1)
|
|
457
|
-
detectedFirst =
|
|
458
|
-
if (indexes.has(
|
|
459
|
-
throw new SquareError('invalid_args', `Invalid square: duplicate act index ${
|
|
460
|
-
indexes.add(
|
|
461
|
-
maxIndex = Math.max(maxIndex,
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
if (lines[i] === '')
|
|
465
|
-
i++;
|
|
466
|
-
const bodyStart = i;
|
|
467
|
-
while (i < lines.length && !isActSeparator(lines, i))
|
|
468
|
-
i++;
|
|
469
|
-
body = unquoteBody(lines.slice(bodyStart, i));
|
|
470
|
-
}
|
|
471
|
-
acts.push({ ...head, body, index: meta.index, ...(meta.reach !== undefined ? { reach: meta.reach } : {}) });
|
|
511
|
+
detectedFirst = act.index;
|
|
512
|
+
if (indexes.has(act.index))
|
|
513
|
+
throw new SquareError('invalid_args', `Invalid square: duplicate act index ${act.index}.`);
|
|
514
|
+
indexes.add(act.index);
|
|
515
|
+
maxIndex = Math.max(maxIndex, act.index);
|
|
516
|
+
acts.push(act);
|
|
517
|
+
i = end;
|
|
472
518
|
}
|
|
473
519
|
if (indexes.size > 0) {
|
|
474
520
|
const first = detectedFirst;
|
|
@@ -476,7 +522,7 @@ function parseActs(text, _firstActIndex) {
|
|
|
476
522
|
throw new SquareError('invalid_args', 'Invalid square: act indexes must be contiguous from the first retained index.');
|
|
477
523
|
}
|
|
478
524
|
}
|
|
479
|
-
return { acts, nextActIndex: indexes.size > 0 ? maxIndex + 1 : 0
|
|
525
|
+
return { acts, nextActIndex: indexes.size > 0 ? maxIndex + 1 : 0 };
|
|
480
526
|
}
|
|
481
527
|
function isDivider(line) {
|
|
482
528
|
return line.trim() === '---';
|
|
@@ -490,137 +536,40 @@ function trimSection(lines) {
|
|
|
490
536
|
function unfixableResult(reason) {
|
|
491
537
|
return {
|
|
492
538
|
unfixable: reason,
|
|
493
|
-
legacyParticipants: [],
|
|
494
539
|
problems: [],
|
|
495
540
|
hardCap: null,
|
|
496
541
|
preamble: [],
|
|
497
542
|
warmup: [],
|
|
498
543
|
acts: [],
|
|
499
544
|
quarantined: [],
|
|
500
|
-
runtimeRaw: undefined,
|
|
501
|
-
};
|
|
502
|
-
}
|
|
503
|
-
function parseLegacyEventMarker(line) {
|
|
504
|
-
if (line === undefined || !line.startsWith(V1_EVENT_MARKER_PREFIX))
|
|
505
|
-
return null;
|
|
506
|
-
const match = line.match(/^<!-- square:event\s+(\{.*\})\s*-->$/);
|
|
507
|
-
if (!match)
|
|
508
|
-
return null;
|
|
509
|
-
let parsed;
|
|
510
|
-
try {
|
|
511
|
-
parsed = JSON.parse(match[1]);
|
|
512
|
-
}
|
|
513
|
-
catch {
|
|
514
|
-
return null;
|
|
515
|
-
}
|
|
516
|
-
if (!isObject(parsed) || typeof parsed.index !== 'number' || !Number.isInteger(parsed.index) || parsed.index < 0)
|
|
517
|
-
return null;
|
|
518
|
-
return {
|
|
519
|
-
index: parsed.index,
|
|
520
|
-
...(typeof parsed.type === 'string' ? { type: parsed.type } : {}),
|
|
521
|
-
...(typeof parsed.name === 'string' ? { name: parsed.name } : {}),
|
|
522
|
-
...(typeof parsed.at === 'number' && Number.isFinite(parsed.at) ? { at: parsed.at } : {}),
|
|
523
545
|
};
|
|
524
546
|
}
|
|
525
|
-
function parseLegacyEventLine(line, actor) {
|
|
526
|
-
let match = line.match(/^_activity (\d+) · ([^_]+)_\s*$/);
|
|
527
|
-
if (match) {
|
|
528
|
-
const at = parseTimestamp(match[2]);
|
|
529
|
-
return Number.isFinite(at) ? { type: 'activity', actor, at, number: parseInt(match[1], 10) } : null;
|
|
530
|
-
}
|
|
531
|
-
match = line.match(/^_(joined|done|hold|resume) · ([^_]+)_\s*$/);
|
|
532
|
-
if (!match)
|
|
533
|
-
return null;
|
|
534
|
-
const at = parseTimestamp(match[2]);
|
|
535
|
-
if (!Number.isFinite(at))
|
|
536
|
-
return null;
|
|
537
|
-
const type = match[1] === 'joined' ? 'join' : match[1];
|
|
538
|
-
return { type, actor, at };
|
|
539
|
-
}
|
|
540
|
-
function normalizeLegacyActor(name) {
|
|
541
|
-
if (name === undefined || sameName(name, 'system'))
|
|
542
|
-
return 'unknown';
|
|
543
|
-
return name;
|
|
544
|
-
}
|
|
545
|
-
function mapLegacyEventToAct(marker, head, body) {
|
|
546
|
-
if (marker.type === undefined || marker.name === undefined)
|
|
547
|
-
return null;
|
|
548
|
-
if (marker.type !== head.type || !sameName(marker.name, head.actor))
|
|
549
|
-
return null;
|
|
550
|
-
switch (head.type) {
|
|
551
|
-
case 'activity':
|
|
552
|
-
return { kind: 'say', actor: head.actor, at: head.at, body, index: marker.index };
|
|
553
|
-
case 'join':
|
|
554
|
-
return { kind: 'join', actor: head.actor, at: head.at, body: '', index: marker.index };
|
|
555
|
-
case 'done':
|
|
556
|
-
return { kind: 'done', actor: head.actor, at: head.at, body, index: marker.index };
|
|
557
|
-
case 'hold':
|
|
558
|
-
return { kind: 'hold', actor: normalizeLegacyActor(head.actor), at: head.at, body, index: marker.index };
|
|
559
|
-
case 'resume':
|
|
560
|
-
return { kind: 'resume', actor: normalizeLegacyActor(head.actor), at: head.at, body: '', index: marker.index };
|
|
561
|
-
default:
|
|
562
|
-
return null;
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
547
|
function tryParseV2ActBlock(blockLines) {
|
|
566
548
|
try {
|
|
567
|
-
|
|
568
|
-
const marker = parseActMarker(blockLines[i]?.trim());
|
|
549
|
+
const marker = parseActMarker(blockLines[0]?.trim());
|
|
569
550
|
if (!marker || marker.kind === undefined || marker.actor === undefined)
|
|
570
551
|
return { ok: false, reason: 'act marker is missing kind/actor metadata.' };
|
|
571
|
-
|
|
572
|
-
const actor = parseParticipantHeading(blockLines[i] ?? '');
|
|
552
|
+
const actor = parseParticipantHeading(blockLines[1] ?? '');
|
|
573
553
|
if (!actor)
|
|
574
|
-
return { ok: false, reason: `act block missing participant heading, got: ${blockLines[
|
|
575
|
-
|
|
576
|
-
if (
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
if (!head)
|
|
580
|
-
return { ok: false, reason: `act block missing or malformed timestamp line, got: ${blockLines[i] ?? ''}` };
|
|
581
|
-
i++;
|
|
582
|
-
normalizeActMeta(marker, head.kind, actor, head);
|
|
583
|
-
let body = '';
|
|
584
|
-
if (head.kind === 'say' || head.kind === 'done' || head.kind === 'hold') {
|
|
585
|
-
if (blockLines[i] === '')
|
|
586
|
-
i++;
|
|
587
|
-
body = unquoteBody(blockLines.slice(i));
|
|
588
|
-
}
|
|
589
|
-
return { ok: true, act: { ...head, body, index: marker.index, ...(marker.reach !== undefined ? { reach: marker.reach } : {}) } };
|
|
554
|
+
return { ok: false, reason: `act block missing participant heading, got: ${blockLines[1] ?? ''}` };
|
|
555
|
+
const headLine = blockLines[2] === '' ? blockLines[3] : blockLines[2];
|
|
556
|
+
if (!parseActLine(headLine ?? '', actor))
|
|
557
|
+
return { ok: false, reason: `act block missing or malformed timestamp line, got: ${headLine ?? ''}` };
|
|
558
|
+
return { ok: true, act: parseActBlock(blockLines) };
|
|
590
559
|
}
|
|
591
560
|
catch {
|
|
592
561
|
return { ok: false, reason: 'malformed act block.' };
|
|
593
562
|
}
|
|
594
563
|
}
|
|
595
|
-
function tryParseV1EventBlock(blockLines) {
|
|
596
|
-
const marker = parseLegacyEventMarker(blockLines[0]?.trim());
|
|
597
|
-
if (!marker)
|
|
598
|
-
return { ok: false, reason: `expected event marker, got: ${blockLines[0] ?? ''}` };
|
|
599
|
-
let i = 1;
|
|
600
|
-
const actor = parseParticipantHeading(blockLines[i] ?? '');
|
|
601
|
-
if (!actor)
|
|
602
|
-
return { ok: false, reason: `event block missing participant heading, got: ${blockLines[i] ?? ''}` };
|
|
603
|
-
i++;
|
|
604
|
-
if (blockLines[i] === '')
|
|
605
|
-
i++;
|
|
606
|
-
const head = parseLegacyEventLine(blockLines[i] ?? '', actor);
|
|
607
|
-
if (!head)
|
|
608
|
-
return { ok: false, reason: `event block missing or malformed timestamp line, got: ${blockLines[i] ?? ''}` };
|
|
609
|
-
i++;
|
|
610
|
-
if (blockLines[i] === '')
|
|
611
|
-
i++;
|
|
612
|
-
const body = unquoteBody(blockLines.slice(i));
|
|
613
|
-
const act = mapLegacyEventToAct(marker, head, body);
|
|
614
|
-
return act ? { ok: true, act } : { ok: false, reason: 'legacy event metadata does not match its body.' };
|
|
615
|
-
}
|
|
616
564
|
function detectBlockParser(line) {
|
|
617
565
|
const trimmed = line.trim();
|
|
618
566
|
if (trimmed.startsWith(ACT_MARKER_PREFIX))
|
|
619
567
|
return tryParseV2ActBlock;
|
|
620
|
-
if (trimmed.startsWith(V1_EVENT_MARKER_PREFIX))
|
|
621
|
-
return tryParseV1EventBlock;
|
|
622
568
|
return null;
|
|
623
569
|
}
|
|
570
|
+
function isActBlockStart(line) {
|
|
571
|
+
return line.trim().startsWith('<!-- square:');
|
|
572
|
+
}
|
|
624
573
|
function diagnoseActs(text, problems) {
|
|
625
574
|
const lines = activitiesSourceLines(text);
|
|
626
575
|
const acts = [];
|
|
@@ -635,13 +584,16 @@ function diagnoseActs(text, problems) {
|
|
|
635
584
|
const parseBlock = detectBlockParser(lines[i]);
|
|
636
585
|
if (!parseBlock) {
|
|
637
586
|
problems.push({ kind: 'act_block', message: `expected act marker, got: ${lines[i]}` });
|
|
638
|
-
|
|
639
|
-
|
|
587
|
+
let j = i + 1;
|
|
588
|
+
while (j < lines.length && !isActBlockStart(lines[j]))
|
|
589
|
+
j++;
|
|
590
|
+
quarantined.push({ raw: lines.slice(i, j).join('\n'), reason: 'expected current-format act marker' });
|
|
591
|
+
i = j;
|
|
640
592
|
continue;
|
|
641
593
|
}
|
|
642
594
|
const blockStart = i;
|
|
643
595
|
let j = i + 1;
|
|
644
|
-
while (j < lines.length &&
|
|
596
|
+
while (j < lines.length && !isActBlockStart(lines[j]))
|
|
645
597
|
j++;
|
|
646
598
|
const blockLines = lines.slice(blockStart, j);
|
|
647
599
|
const outcome = parseBlock(blockLines);
|
|
@@ -684,10 +636,6 @@ export function diagnoseSquare(text) {
|
|
|
684
636
|
return unfixableResult('missing format_version in frontmatter. Create a new square with `square build`.');
|
|
685
637
|
if (formatVersion !== CURRENT_FORMAT_VERSION)
|
|
686
638
|
return unfixableResult(`format_version ${formatVersion} is no longer supported. Create a new square with \`square build\`.`);
|
|
687
|
-
const legacyParticipantsMatch = frontmatter.match(/^participants:[^\S\r\n]*(.*?)[^\S\r\n]*$/m);
|
|
688
|
-
const legacyParticipants = legacyParticipantsMatch
|
|
689
|
-
? legacyParticipantsMatch[1].split(',').map((name) => name.trim()).filter(Boolean)
|
|
690
|
-
: [];
|
|
691
639
|
let hardCap = null;
|
|
692
640
|
const hcMatch = frontmatter.match(/^hard_cap:\s*(-1|\d+)\s*$/m);
|
|
693
641
|
if (!hcMatch)
|
|
@@ -698,17 +646,6 @@ export function diagnoseSquare(text) {
|
|
|
698
646
|
const tMatch = frontmatter.match(/^throttle_per_minute:\s*(\d+)\s*$/m);
|
|
699
647
|
if (tMatch)
|
|
700
648
|
throttlePerMinute = parseInt(tMatch[1], 10);
|
|
701
|
-
let runtimeRaw;
|
|
702
|
-
// V3: runtime state is in the sidecar file, not in markdown.
|
|
703
|
-
const rawState = parseStateBlock(frontmatter);
|
|
704
|
-
if (rawState !== undefined && rawState !== '') {
|
|
705
|
-
try {
|
|
706
|
-
runtimeRaw = JSON.parse(rawState);
|
|
707
|
-
}
|
|
708
|
-
catch {
|
|
709
|
-
problems.push({ kind: 'runtime_state', message: 'malformed legacy mind_square_state JSON in frontmatter.' });
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
649
|
let preamble = [];
|
|
713
650
|
try {
|
|
714
651
|
preamble = parsePreamble(text);
|
|
@@ -726,7 +663,6 @@ export function diagnoseSquare(text) {
|
|
|
726
663
|
const { acts, quarantined } = diagnoseActs(text, problems);
|
|
727
664
|
return {
|
|
728
665
|
formatVersion,
|
|
729
|
-
legacyParticipants,
|
|
730
666
|
problems,
|
|
731
667
|
hardCap,
|
|
732
668
|
throttlePerMinute,
|
|
@@ -734,6 +670,5 @@ export function diagnoseSquare(text) {
|
|
|
734
670
|
warmup,
|
|
735
671
|
acts,
|
|
736
672
|
quarantined,
|
|
737
|
-
runtimeRaw,
|
|
738
673
|
};
|
|
739
674
|
}
|
|
@@ -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
|
+
}
|