@astrosheep/square 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/codex-plugin/.codex-plugin/plugin.json +3 -2
  2. package/dist/activity-feed.js +26 -18
  3. package/dist/activity.js +23 -22
  4. package/dist/artifact.js +126 -202
  5. package/dist/claude-hook.js +45 -21
  6. package/dist/cli/context.js +143 -0
  7. package/dist/cli/harness-command.js +50 -0
  8. package/dist/cli/maintenance-commands.js +76 -0
  9. package/dist/cli/meta-commands.js +28 -0
  10. package/dist/cli/observation-commands.js +453 -0
  11. package/dist/cli/program.js +48 -0
  12. package/dist/cli/registry.js +40 -0
  13. package/dist/cli/square-commands.js +219 -0
  14. package/dist/cmd/notify-once.js +23 -21
  15. package/dist/compact.js +6 -19
  16. package/dist/decisions.js +53 -86
  17. package/dist/delivery-health.js +104 -210
  18. package/dist/delivery.js +68 -18
  19. package/dist/doctor.js +9 -8
  20. package/dist/harness-claude.js +68 -0
  21. package/dist/harness-codex.js +119 -0
  22. package/dist/harness-links.js +123 -0
  23. package/dist/harness-stage.js +36 -0
  24. package/dist/harness.js +94 -576
  25. package/dist/help.js +44 -35
  26. package/dist/inbox.js +12 -11
  27. package/dist/index.js +30 -129
  28. package/dist/list.js +1 -1
  29. package/dist/model.js +0 -6
  30. package/dist/notification-failures.js +54 -0
  31. package/dist/notifications.js +47 -62
  32. package/dist/paseo-timeline.js +58 -188
  33. package/dist/presentation.js +55 -63
  34. package/dist/presented.js +9 -8
  35. package/dist/registry.js +55 -45
  36. package/dist/runtime.js +26 -137
  37. package/dist/square-application.js +264 -0
  38. package/dist/square-core.js +3 -11
  39. package/dist/square.js +5 -1362
  40. package/dist/stream.js +27 -126
  41. package/dist/wake-sink.js +134 -188
  42. package/dist/watch.js +79 -138
  43. package/extensions/square-opencode.js +1 -1
  44. package/extensions/square-pi.js +8 -130
  45. package/guides/architect.md +3 -3
  46. package/guides/participant.md +25 -16
  47. package/package.json +2 -2
  48. package/skills/brainstorm/SKILL.md +25 -32
  49. package/skills/square/.claude-plugin/plugin.json +1 -1
  50. package/skills/square/SKILL.md +39 -107
  51. package/skills/square-feedback/SKILL.md +4 -4
  52. package/dist/terminal.js +0 -125
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "Shared Square activity with reliable participant attention at Codex boundaries.",
5
5
  "author": {
6
6
  "name": "Square"
@@ -15,7 +15,8 @@
15
15
  "interface": {
16
16
  "displayName": "Square",
17
17
  "shortDescription": "Join and coordinate through a shared square",
18
- "longDescription": "Participate in a shared activity stream and receive addressed Square activity at Codex boundaries.",
18
+ "longDescription": "Participate in a shared square and receive addressed activity at Codex boundaries.",
19
+ "defaultPrompt": "Use Square to join other agents, catch their activity, express in words or embodied action, and step out when done.",
19
20
  "developerName": "Square",
20
21
  "category": "Developer Tools",
21
22
  "capabilities": [
@@ -1,36 +1,44 @@
1
1
  import { sameName } from './model.js';
2
- import { actStableIndex, advanceCursor, latestIndexedActIndex } from './runtime.js';
3
- import { matchesCatchFilter } from './delivery.js';
4
- export function indexedDelta(acts, cursor) {
5
- return acts
6
- .map((act) => ({ act, index: actStableIndex(act) }))
7
- .filter((item) => item.index > cursor);
2
+ import { advanceCursor, latestActIndex, readCursor } from './runtime.js';
3
+ import { deriveDeliveryModel, matchesCatchFilter } from './delivery.js';
4
+ export function actDelta(acts, cursor) {
5
+ return acts.filter((act) => act.index > cursor);
6
+ }
7
+ /** Public cursor changes plus directed receipts that remain pending behind it. */
8
+ export function deliveryDelta(doc, name) {
9
+ const items = actDelta(doc.acts, readCursor(doc, name));
10
+ const seen = new Set(items.map((act) => act.index));
11
+ for (const notification of deriveDeliveryModel(doc).pendingFor(name)) {
12
+ if (!seen.has(notification.item.index))
13
+ items.push(notification.item);
14
+ }
15
+ return items.sort((a, b) => a.index - b.index);
8
16
  }
9
17
  export function peerRoomChanges(delta, name) {
10
- return delta.filter((item) => item.act.actor !== undefined && !sameName(item.act.actor, name) && item.act.kind !== 'say' && item.act.kind !== 'read');
18
+ return delta.filter((act) => act.actor !== undefined && !sameName(act.actor, name) && act.kind !== 'say' && act.kind !== 'read');
11
19
  }
12
20
  export function peerPublicActs(delta, name) {
13
- return delta.filter((item) => item.act.actor !== undefined && !sameName(item.act.actor, name) && (item.act.kind === 'say' || item.act.kind === 'done'));
21
+ return delta.filter((act) => act.actor !== undefined && !sameName(act.actor, name) && (act.kind === 'say' || act.kind === 'done'));
14
22
  }
15
- function matchesParticipants(item, participants) {
16
- return participants === undefined || (item.act.actor !== undefined && participants.some((participant) => sameName(participant, item.act.actor)));
23
+ function matchesParticipants(act, participants) {
24
+ return participants === undefined || (act.actor !== undefined && participants.some((participant) => sameName(participant, act.actor)));
17
25
  }
18
- export function matchesFeedFilter(item, filter) {
19
- if (item.act.kind === 'say') {
20
- return matchesCatchFilter({ actor: item.act.actor, body: item.act.body, bell: item.act.reach === 'bell' }, filter);
26
+ export function matchesFeedFilter(act, filter) {
27
+ if (act.kind === 'say') {
28
+ return matchesCatchFilter({ actor: act.actor, body: act.body, reach: act.reach }, filter);
21
29
  }
22
- return filter.mention === undefined && matchesParticipants(item, filter.participants);
30
+ return filter.mention === undefined && matchesParticipants(act, filter.participants);
23
31
  }
24
32
  export function filteredPeerActivities(delta, name, filter) {
25
33
  return peerPublicActs(delta, name)
26
- .filter((item) => item.act.kind === 'say')
27
- .filter((item) => matchesFeedFilter(item, filter));
34
+ .filter((act) => act.kind === 'say')
35
+ .filter((act) => matchesFeedFilter(act, filter));
28
36
  }
29
37
  export function filteredRoomChanges(delta, name, filter) {
30
38
  if (filter.mention !== undefined)
31
39
  return [];
32
- return peerRoomChanges(delta, name).filter((item) => matchesParticipants(item, filter.participants));
40
+ return peerRoomChanges(delta, name).filter((act) => matchesParticipants(act, filter.participants));
33
41
  }
34
42
  export function ackPeerDelta(doc, name, delta) {
35
- return advanceCursor(doc, name, latestIndexedActIndex([...peerPublicActs(delta, name), ...peerRoomChanges(delta, name)]), 'watch');
43
+ return advanceCursor(doc, name, latestActIndex([...peerPublicActs(delta, name), ...peerRoomChanges(delta, name)]));
36
44
  }
package/dist/activity.js CHANGED
@@ -4,10 +4,10 @@ import path from 'node:path';
4
4
  import { setTimeout as sleep } from 'node:timers/promises';
5
5
  import { loadSquare } from './artifact.js';
6
6
  import { SquareError, validateName } from './model.js';
7
- import { dispatchActNotifications } from './notifications.js';
8
- import { actHintLine, renderActivityBlocked, renderActivityLimit, renderActNoWait, renderActWaiting, renderPendingFeed, withActivityNextOutput, } from './presentation.js';
9
- import { appendAct, currentHold, inSquareCount, nowMs, SLEEP_MS, withSquareLock, resolveRosterName } from './runtime.js';
10
- import { decideAct, resolveKnownName } from './decisions.js';
7
+ import { expressHintLine, renderActivityBlocked, renderActivityLimit, renderExpressNoWait, renderExpressWaiting, renderPendingFeed, withPathOutput, } from './presentation.js';
8
+ import { currentHold, inSquareCount, nowMs, SLEEP_MS, resolveRosterName } from './runtime.js';
9
+ import { resolveKnownName } from './decisions.js';
10
+ import { execute } from './square-application.js';
11
11
  import { formatTimestamp } from './time.js';
12
12
  function draftDirFor(squarePath) {
13
13
  return path.join(path.dirname(squarePath), 'drafts');
@@ -68,24 +68,25 @@ export async function cmdActivity(squarePath, name, activity, resolveBody, opts)
68
68
  : { beside: resolveKnownName(doc, opts.reach.beside) };
69
69
  let announcedWait;
70
70
  while (true) {
71
- const { decision, sent, headerCount, held } = await withSquareLock(squarePath, () => {
72
- const freshDoc = loadSquare(squarePath);
73
- const d = decideAct(freshDoc, { name, body, force, now: nowMs(), reach });
74
- if (d.type === 'sent') {
75
- const appended = appendAct(squarePath, freshDoc, d.act);
76
- return { decision: d, sent: { act: appended, index: appended.index }, headerCount: inSquareCount(freshDoc), held: currentHold(freshDoc.acts).active };
77
- }
78
- return { decision: d, sent: undefined, headerCount: inSquareCount(freshDoc), held: currentHold(freshDoc.acts).active };
71
+ const committed = await execute(squarePath, {
72
+ type: 'say',
73
+ name,
74
+ body,
75
+ force,
76
+ now: nowMs(),
77
+ ...(reach === undefined ? {} : { reach }),
79
78
  });
79
+ const decision = committed.result;
80
+ const freshDoc = loadSquare(squarePath);
81
+ const headerCount = inSquareCount(freshDoc);
82
+ const held = currentHold(freshDoc.acts).active;
80
83
  switch (decision.type) {
81
84
  case 'sent': {
82
- if (sent)
83
- await dispatchActNotifications(squarePath, sent);
84
85
  const hasPending = decision.pendingPublic.length > 0 || decision.pendingRoomChanges.length > 0;
85
- const pending = hasPending ? `\n\n${renderPendingFeed(decision.pendingPublic, decision.pendingRoomChanges)}` : '';
86
- const hint = actHintLine(decision.ownActCount);
86
+ const pending = hasPending ? `\n\n${renderPendingFeed(freshDoc.acts, decision.pendingPublic, decision.pendingRoomChanges)}` : '';
87
+ const hint = expressHintLine(decision.ownActCount);
87
88
  const withHint = hint ? `${decision.confirmation}\n${hint}` : decision.confirmation;
88
- process.stdout.write(withActivityNextOutput(squarePath, withHint + pending, { participantCount: headerCount, held }));
89
+ process.stdout.write(withPathOutput(squarePath, withHint + pending, { participantCount: headerCount, held }));
89
90
  return;
90
91
  }
91
92
  case 'blocked': {
@@ -119,11 +120,11 @@ export async function cmdActivity(squarePath, name, activity, resolveBody, opts)
119
120
  case 'throttled': {
120
121
  if (noWait) {
121
122
  const draftPath = saveActivityDraft(squarePath, name, rawInput);
122
- process.stdout.write(renderActNoWait({ squarePath, name: knownName, reason: 'throttled', delayMs: decision.delayMs, draftPath, participantCount: headerCount, held }));
123
+ process.stdout.write(renderExpressNoWait({ squarePath, name: knownName, reason: 'throttled', delayMs: decision.delayMs, draftPath, participantCount: headerCount, held }));
123
124
  process.exit(1);
124
125
  }
125
126
  if (announcedWait !== 'throttled') {
126
- process.stdout.write(renderActWaiting({ reason: 'throttled', delayMs: decision.delayMs }) + '\n');
127
+ process.stdout.write(renderExpressWaiting({ reason: 'throttled', delayMs: decision.delayMs }) + '\n');
127
128
  announcedWait = 'throttled';
128
129
  }
129
130
  await sleep(decision.delayMs);
@@ -132,18 +133,18 @@ export async function cmdActivity(squarePath, name, activity, resolveBody, opts)
132
133
  case 'held': {
133
134
  if (noWait) {
134
135
  const draftPath = saveActivityDraft(squarePath, name, rawInput);
135
- process.stdout.write(renderActNoWait({ squarePath, name: knownName, reason: 'held', holdReason: decision.reason, draftPath, participantCount: headerCount, held }));
136
+ process.stdout.write(renderExpressNoWait({ squarePath, name: knownName, reason: 'held', holdReason: decision.reason, draftPath, participantCount: headerCount, held }));
136
137
  process.exit(1);
137
138
  }
138
139
  if (announcedWait !== 'held') {
139
- process.stdout.write(renderActWaiting({ reason: 'held' }) + '\n');
140
+ process.stdout.write(renderExpressWaiting({ reason: 'held' }) + '\n');
140
141
  announcedWait = 'held';
141
142
  }
142
143
  await sleep(SLEEP_MS);
143
144
  break;
144
145
  }
145
146
  case 'bell_quota': {
146
- process.stdout.write(withActivityNextOutput(squarePath, [`✕ the bell stays quiet for now`, ` · you can ring it again at ${formatTimestamp(decision.nextAt)}`].join('\n'), { participantCount: headerCount, held }));
147
+ process.stdout.write(withPathOutput(squarePath, [`✕ the bell stays quiet for now`, ` · you can ring it again at ${formatTimestamp(decision.nextAt)}`].join('\n'), { participantCount: headerCount, held }));
147
148
  process.exit(1);
148
149
  }
149
150
  }
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();
@@ -61,9 +60,8 @@ export function emptyRuntimeState(nextActIndex = 0) {
61
60
  return {
62
61
  version: 2,
63
62
  nextActIndex,
64
- firstActIndex: 0,
65
63
  cursors: {},
66
- mentionReceipts: {},
64
+ deliveryReceipts: {},
67
65
  leases: {},
68
66
  };
69
67
  }
@@ -117,25 +115,75 @@ export function renderSquare(opts, snippet) {
117
115
  function sidecarPath(squarePath) {
118
116
  return `${squarePath}.runtime.json`;
119
117
  }
120
- function loadRuntimeSidecar(squarePath) {
118
+ function invalidRuntimeSidecar(squarePath, detail) {
119
+ return new SquareError('invalid_args', `Invalid square runtime sidecar ${sidecarPath(squarePath)}: ${detail}`);
120
+ }
121
+ function validateRuntimeSidecar(squarePath, value) {
122
+ if (!isObject(value))
123
+ throw invalidRuntimeSidecar(squarePath, 'expected a JSON object.');
124
+ if (value.version !== 2)
125
+ throw invalidRuntimeSidecar(squarePath, 'unsupported or missing version.');
126
+ if (typeof value.nextActIndex !== 'number' || !Number.isInteger(value.nextActIndex) || value.nextActIndex < 0) {
127
+ throw invalidRuntimeSidecar(squarePath, 'nextActIndex must be a non-negative integer.');
128
+ }
129
+ if (!isObject(value.cursors) || !Object.values(value.cursors).every(isReadCursor)) {
130
+ throw invalidRuntimeSidecar(squarePath, 'cursors contains an invalid read cursor.');
131
+ }
132
+ if (!isObject(value.deliveryReceipts) || !Object.values(value.deliveryReceipts).every(isDeliveryReceiptMap)) {
133
+ throw invalidRuntimeSidecar(squarePath, 'deliveryReceipts contains an invalid receipt map.');
134
+ }
135
+ if (!isObject(value.leases) || !Object.values(value.leases).every(isWatchLease)) {
136
+ throw invalidRuntimeSidecar(squarePath, 'leases contains an invalid watch lease.');
137
+ }
138
+ return {
139
+ version: 2,
140
+ nextActIndex: value.nextActIndex,
141
+ cursors: value.cursors,
142
+ deliveryReceipts: value.deliveryReceipts,
143
+ leases: value.leases,
144
+ };
145
+ }
146
+ export function loadRuntimeSidecar(squarePath, fallbackRuntime) {
121
147
  const sp = sidecarPath(squarePath);
122
148
  try {
123
149
  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');
150
+ let parsed;
151
+ try {
152
+ parsed = JSON.parse(raw);
153
+ }
154
+ catch {
155
+ throw invalidRuntimeSidecar(squarePath, 'malformed JSON.');
127
156
  }
128
- return parsed;
157
+ return validateRuntimeSidecar(squarePath, parsed);
129
158
  }
130
159
  catch (err) {
131
160
  if (err.code === 'ENOENT') {
132
- return emptyRuntimeState(0);
161
+ return fallbackRuntime;
133
162
  }
134
163
  throw err;
135
164
  }
136
165
  }
166
+ export function mergeRuntimeState(markdownRuntime, sidecarRuntime) {
167
+ return {
168
+ ...sidecarRuntime,
169
+ nextActIndex: Math.max(markdownRuntime.nextActIndex, sidecarRuntime.nextActIndex),
170
+ };
171
+ }
137
172
  export function saveRuntimeSidecar(squarePath, runtime) {
138
- fs.writeFileSync(sidecarPath(squarePath), JSON.stringify(runtime, null, 2));
173
+ const target = sidecarPath(squarePath);
174
+ const normalized = validateRuntimeSidecar(squarePath, runtime);
175
+ const temporary = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
176
+ try {
177
+ fs.writeFileSync(temporary, JSON.stringify(normalized, null, 2));
178
+ fs.renameSync(temporary, target);
179
+ }
180
+ catch (error) {
181
+ try {
182
+ fs.unlinkSync(temporary);
183
+ }
184
+ catch { }
185
+ throw error;
186
+ }
139
187
  }
140
188
  export function renderSquareDoc(doc) {
141
189
  const warmup = renderWarmupSection(doc.warmup);
@@ -182,25 +230,6 @@ function parseFrontmatter(text) {
182
230
  throw new SquareError('invalid_args', 'Invalid square: missing frontmatter.');
183
231
  return match[1];
184
232
  }
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
233
  export function isObject(value) {
205
234
  return typeof value === 'object' && value !== null && !Array.isArray(value);
206
235
  }
@@ -211,13 +240,12 @@ export function isReadCursor(value) {
211
240
  Number.isInteger(value.consumedThroughIndex) &&
212
241
  value.consumedThroughIndex >= -1 &&
213
242
  typeof value.updatedAt === 'number' &&
214
- Number.isFinite(value.updatedAt) &&
215
- (value.source === 'join' || value.source === 'watch' || value.source === 'api'));
243
+ Number.isFinite(value.updatedAt));
216
244
  }
217
- export function isMentionReceipt(value) {
245
+ export function isDeliveryReceipt(value) {
218
246
  if (!isObject(value))
219
247
  return false;
220
- if (value.status !== 'delivered' && value.status !== 'presented')
248
+ if (value.status !== 'delivered')
221
249
  return false;
222
250
  if (typeof value.at !== 'number' || !Number.isFinite(value.at))
223
251
  return false;
@@ -229,14 +257,16 @@ export function isMentionReceipt(value) {
229
257
  return false;
230
258
  return true;
231
259
  }
232
- export function isMentionReceiptMap(value) {
233
- return isObject(value) && Object.entries(value).every(([id, receipt]) => /^act_\d+$/.test(id) && isMentionReceipt(receipt));
260
+ export function isDeliveryReceiptMap(value) {
261
+ return isObject(value) && Object.entries(value).every(([id, receipt]) => /^act_\d+$/.test(id) && isDeliveryReceipt(receipt));
234
262
  }
235
263
  export function isWatchLease(value) {
236
264
  if (!isObject(value))
237
265
  return false;
238
266
  if (typeof value.leaseId !== 'string' || value.leaseId === '')
239
267
  return false;
268
+ if (value.ownerId !== undefined && (typeof value.ownerId !== 'string' || value.ownerId === ''))
269
+ return false;
240
270
  if (typeof value.heartbeatAt !== 'number' || !Number.isFinite(value.heartbeatAt))
241
271
  return false;
242
272
  if (typeof value.expiresAt !== 'number' || !Number.isFinite(value.expiresAt))
@@ -293,9 +323,8 @@ export function parseFormatVersion(text) {
293
323
  export function parseSquare(text) {
294
324
  parseFormatVersion(text);
295
325
  const runtime = emptyRuntimeState(0);
296
- const parsedActs = parseActs(text, 0);
326
+ const parsedActs = parseActs(text);
297
327
  runtime.nextActIndex = parsedActs.nextActIndex;
298
- runtime.firstActIndex = parsedActs.firstActIndex;
299
328
  return {
300
329
  hardCap: parseCap(text),
301
330
  throttlePerMinute: parseThrottle(text),
@@ -317,12 +346,12 @@ export function loadSquare(squarePath) {
317
346
  throw err;
318
347
  }
319
348
  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;
349
+ const markdownRuntime = doc.runtime;
350
+ const sidecar = loadRuntimeSidecar(squarePath, markdownRuntime);
351
+ // Markdown owns the activity history; runtime state owns delivery metadata.
352
+ // Preserve whichever history boundary is furthest ahead so a missing or
353
+ // stale sidecar can never cause an index to be reused.
354
+ doc.runtime = mergeRuntimeState(markdownRuntime, sidecar);
326
355
  return doc;
327
356
  }
328
357
  function isSquareMarker(line) {
@@ -427,7 +456,32 @@ export function parseActLine(line, actor) {
427
456
  function isActSeparator(lines, index) {
428
457
  return lines[index]?.trim().startsWith(ACT_MARKER_PREFIX) === true;
429
458
  }
430
- function parseActs(text, _firstActIndex) {
459
+ function parseActBlock(blockLines) {
460
+ let i = 0;
461
+ const marker = parseActMarker(blockLines[i]?.trim());
462
+ if (!marker)
463
+ throw new SquareError('invalid_args', `Invalid square: expected act marker, got: ${blockLines[i] ?? ''}`);
464
+ i++;
465
+ const actor = parseParticipantHeading(blockLines[i] ?? '');
466
+ if (!actor)
467
+ throw new SquareError('invalid_args', `Invalid square: expected participant heading, got: ${blockLines[i] ?? ''}`);
468
+ i++;
469
+ if (blockLines[i] === '')
470
+ i++;
471
+ const head = parseActLine(blockLines[i] ?? '', actor);
472
+ if (!head)
473
+ throw new SquareError('invalid_args', `Invalid square: expected act line, got: ${blockLines[i] ?? ''}`);
474
+ i++;
475
+ const meta = normalizeActMeta(marker, head.kind, actor, head);
476
+ let body = '';
477
+ if (head.kind === 'say' || head.kind === 'done' || head.kind === 'hold') {
478
+ if (blockLines[i] === '')
479
+ i++;
480
+ body = unquoteBody(blockLines.slice(i));
481
+ }
482
+ return { ...head, body, index: meta.index, ...(meta.reach !== undefined ? { reach: meta.reach } : {}) };
483
+ }
484
+ function parseActs(text) {
431
485
  const lines = activitiesSourceLines(text);
432
486
  const acts = [];
433
487
  const indexes = new Set();
@@ -438,37 +492,18 @@ function parseActs(text, _firstActIndex) {
438
492
  i++;
439
493
  continue;
440
494
  }
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);
495
+ let end = i + 1;
496
+ while (end < lines.length && !isActSeparator(lines, end))
497
+ end++;
498
+ const act = parseActBlock(lines.slice(i, end));
456
499
  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 } : {}) });
500
+ detectedFirst = act.index;
501
+ if (indexes.has(act.index))
502
+ throw new SquareError('invalid_args', `Invalid square: duplicate act index ${act.index}.`);
503
+ indexes.add(act.index);
504
+ maxIndex = Math.max(maxIndex, act.index);
505
+ acts.push(act);
506
+ i = end;
472
507
  }
473
508
  if (indexes.size > 0) {
474
509
  const first = detectedFirst;
@@ -476,7 +511,7 @@ function parseActs(text, _firstActIndex) {
476
511
  throw new SquareError('invalid_args', 'Invalid square: act indexes must be contiguous from the first retained index.');
477
512
  }
478
513
  }
479
- return { acts, nextActIndex: indexes.size > 0 ? maxIndex + 1 : 0, firstActIndex: detectedFirst >= 0 ? detectedFirst : 0 };
514
+ return { acts, nextActIndex: indexes.size > 0 ? maxIndex + 1 : 0 };
480
515
  }
481
516
  function isDivider(line) {
482
517
  return line.trim() === '---';
@@ -490,137 +525,40 @@ function trimSection(lines) {
490
525
  function unfixableResult(reason) {
491
526
  return {
492
527
  unfixable: reason,
493
- legacyParticipants: [],
494
528
  problems: [],
495
529
  hardCap: null,
496
530
  preamble: [],
497
531
  warmup: [],
498
532
  acts: [],
499
533
  quarantined: [],
500
- runtimeRaw: undefined,
501
534
  };
502
535
  }
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
- };
524
- }
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
536
  function tryParseV2ActBlock(blockLines) {
566
537
  try {
567
- let i = 0;
568
- const marker = parseActMarker(blockLines[i]?.trim());
538
+ const marker = parseActMarker(blockLines[0]?.trim());
569
539
  if (!marker || marker.kind === undefined || marker.actor === undefined)
570
540
  return { ok: false, reason: 'act marker is missing kind/actor metadata.' };
571
- i++;
572
- const actor = parseParticipantHeading(blockLines[i] ?? '');
541
+ const actor = parseParticipantHeading(blockLines[1] ?? '');
573
542
  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 } : {}) } };
543
+ return { ok: false, reason: `act block missing participant heading, got: ${blockLines[1] ?? ''}` };
544
+ const headLine = blockLines[2] === '' ? blockLines[3] : blockLines[2];
545
+ if (!parseActLine(headLine ?? '', actor))
546
+ return { ok: false, reason: `act block missing or malformed timestamp line, got: ${headLine ?? ''}` };
547
+ return { ok: true, act: parseActBlock(blockLines) };
590
548
  }
591
549
  catch {
592
550
  return { ok: false, reason: 'malformed act block.' };
593
551
  }
594
552
  }
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
553
  function detectBlockParser(line) {
617
554
  const trimmed = line.trim();
618
555
  if (trimmed.startsWith(ACT_MARKER_PREFIX))
619
556
  return tryParseV2ActBlock;
620
- if (trimmed.startsWith(V1_EVENT_MARKER_PREFIX))
621
- return tryParseV1EventBlock;
622
557
  return null;
623
558
  }
559
+ function isActBlockStart(line) {
560
+ return line.trim().startsWith('<!-- square:');
561
+ }
624
562
  function diagnoseActs(text, problems) {
625
563
  const lines = activitiesSourceLines(text);
626
564
  const acts = [];
@@ -635,13 +573,16 @@ function diagnoseActs(text, problems) {
635
573
  const parseBlock = detectBlockParser(lines[i]);
636
574
  if (!parseBlock) {
637
575
  problems.push({ kind: 'act_block', message: `expected act marker, got: ${lines[i]}` });
638
- quarantined.push({ raw: lines[i], reason: 'expected act marker' });
639
- i++;
576
+ let j = i + 1;
577
+ while (j < lines.length && !isActBlockStart(lines[j]))
578
+ j++;
579
+ quarantined.push({ raw: lines.slice(i, j).join('\n'), reason: 'expected current-format act marker' });
580
+ i = j;
640
581
  continue;
641
582
  }
642
583
  const blockStart = i;
643
584
  let j = i + 1;
644
- while (j < lines.length && detectBlockParser(lines[j]) === null)
585
+ while (j < lines.length && !isActBlockStart(lines[j]))
645
586
  j++;
646
587
  const blockLines = lines.slice(blockStart, j);
647
588
  const outcome = parseBlock(blockLines);
@@ -684,10 +625,6 @@ export function diagnoseSquare(text) {
684
625
  return unfixableResult('missing format_version in frontmatter. Create a new square with `square build`.');
685
626
  if (formatVersion !== CURRENT_FORMAT_VERSION)
686
627
  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
628
  let hardCap = null;
692
629
  const hcMatch = frontmatter.match(/^hard_cap:\s*(-1|\d+)\s*$/m);
693
630
  if (!hcMatch)
@@ -698,17 +635,6 @@ export function diagnoseSquare(text) {
698
635
  const tMatch = frontmatter.match(/^throttle_per_minute:\s*(\d+)\s*$/m);
699
636
  if (tMatch)
700
637
  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
638
  let preamble = [];
713
639
  try {
714
640
  preamble = parsePreamble(text);
@@ -726,7 +652,6 @@ export function diagnoseSquare(text) {
726
652
  const { acts, quarantined } = diagnoseActs(text, problems);
727
653
  return {
728
654
  formatVersion,
729
- legacyParticipants,
730
655
  problems,
731
656
  hardCap,
732
657
  throttlePerMinute,
@@ -734,6 +659,5 @@ export function diagnoseSquare(text) {
734
659
  warmup,
735
660
  acts,
736
661
  quarantined,
737
- runtimeRaw,
738
662
  };
739
663
  }