@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.
Files changed (57) hide show
  1. package/codex-plugin/.codex-plugin/plugin.json +3 -2
  2. package/codex-plugin/hooks/hooks.json +3 -14
  3. package/dist/activity-feed.js +26 -18
  4. package/dist/activity.js +10 -10
  5. package/dist/artifact.js +138 -203
  6. package/dist/boundary-presentation.js +77 -0
  7. package/dist/claude-hook.js +4 -94
  8. package/dist/cli/context.js +7 -7
  9. package/dist/cli/maintenance-commands.js +10 -26
  10. package/dist/cli/meta-commands.js +3 -6
  11. package/dist/cli/observation-commands.js +48 -53
  12. package/dist/cli/program.js +4 -4
  13. package/dist/cli/registry.js +5 -5
  14. package/dist/cli/square-commands.js +27 -20
  15. package/dist/cmd/notify-once.js +23 -21
  16. package/dist/codex-hook.js +22 -0
  17. package/dist/compact.js +1 -1
  18. package/dist/decisions.js +61 -88
  19. package/dist/delivery-health.js +104 -210
  20. package/dist/delivery.js +68 -18
  21. package/dist/doctor.js +9 -8
  22. package/dist/harness-claude.js +38 -245
  23. package/dist/harness-codex.js +82 -616
  24. package/dist/harness-stage.js +36 -0
  25. package/dist/harness.js +3 -5
  26. package/dist/help.js +43 -35
  27. package/dist/inbox.js +12 -11
  28. package/dist/index.js +10 -121
  29. package/dist/list.js +1 -1
  30. package/dist/model.js +0 -6
  31. package/dist/notification-failures.js +54 -0
  32. package/dist/notifications.js +47 -62
  33. package/dist/paseo-delivery.js +160 -0
  34. package/dist/paseo-state.js +31 -0
  35. package/dist/paseo-timeline.js +58 -188
  36. package/dist/presentation.js +57 -64
  37. package/dist/presented.js +9 -8
  38. package/dist/registry.js +55 -45
  39. package/dist/runtime.js +27 -84
  40. package/dist/square-application.js +135 -130
  41. package/dist/square-core.js +3 -11
  42. package/dist/stream.js +27 -126
  43. package/dist/wake-sink.js +3 -214
  44. package/dist/watch.js +65 -122
  45. package/extensions/square-opencode.js +8 -73
  46. package/extensions/square-pi.js +8 -132
  47. package/guides/architect.md +3 -3
  48. package/guides/participant.md +25 -16
  49. package/package.json +2 -2
  50. package/skills/brainstorm/SKILL.md +25 -32
  51. package/skills/square/.claude-plugin/plugin.json +1 -1
  52. package/skills/square/SKILL.md +39 -107
  53. package/skills/square/hooks/hooks.json +2 -13
  54. package/skills/square-feedback/SKILL.md +4 -4
  55. package/dist/harness-lifecycle.js +0 -102
  56. package/dist/square-store.js +0 -111
  57. 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
- mentionReceipts: {},
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 loadRuntimeSidecar(squarePath) {
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
- const parsed = JSON.parse(raw);
125
- if (!isObject(parsed) || typeof parsed.nextActIndex !== 'number' || !Number.isInteger(parsed.nextActIndex)) {
126
- throw new Error('corrupt sidecar');
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 emptyRuntimeState(0);
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
- fs.writeFileSync(sidecarPath(squarePath), JSON.stringify(runtime, null, 2));
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 isMentionReceipt(value) {
246
+ export function isDeliveryReceipt(value) {
218
247
  if (!isObject(value))
219
248
  return false;
220
- if (value.status !== 'delivered' && value.status !== 'presented')
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 isMentionReceiptMap(value) {
233
- return isObject(value) && Object.entries(value).every(([id, receipt]) => /^act_\d+$/.test(id) && isMentionReceipt(receipt));
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, 0);
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 sidecar = loadRuntimeSidecar(squarePath);
321
- doc.runtime = sidecar;
322
- if (sidecar.nextActIndex > doc.runtime.nextActIndex) {
323
- doc.runtime.nextActIndex = sidecar.nextActIndex;
324
- }
325
- doc.runtime.firstActIndex = sidecar.firstActIndex;
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
- return { index: marker.index, ...(marker.reach !== undefined ? { reach: marker.reach } : {}) };
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 parseActs(text, _firstActIndex) {
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
- const marker = parseActMarker(lines[i]?.trim());
442
- if (!marker)
443
- throw new SquareError('invalid_args', `Invalid square: expected act marker, got: ${lines[i]}`);
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 = meta.index;
458
- if (indexes.has(meta.index))
459
- throw new SquareError('invalid_args', `Invalid square: duplicate act index ${meta.index}.`);
460
- indexes.add(meta.index);
461
- maxIndex = Math.max(maxIndex, meta.index);
462
- let body = '';
463
- if (head.kind === 'say' || head.kind === 'done' || head.kind === 'hold') {
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, firstActIndex: detectedFirst >= 0 ? detectedFirst : 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
- let i = 0;
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
- i++;
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[i] ?? ''}` };
575
- i++;
576
- if (blockLines[i] === '')
577
- i++;
578
- const head = parseActLine(blockLines[i] ?? '', actor);
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
- quarantined.push({ raw: lines[i], reason: 'expected act marker' });
639
- i++;
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 && detectBlockParser(lines[j]) === null)
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
+ }