@astrosheep/square 0.3.4 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/codex-plugin/.codex-plugin/plugin.json +3 -2
- package/dist/activity-feed.js +26 -18
- package/dist/activity.js +23 -22
- package/dist/artifact.js +126 -202
- package/dist/claude-hook.js +45 -21
- package/dist/cli/context.js +143 -0
- package/dist/cli/harness-command.js +50 -0
- package/dist/cli/maintenance-commands.js +76 -0
- package/dist/cli/meta-commands.js +28 -0
- package/dist/cli/observation-commands.js +453 -0
- package/dist/cli/program.js +48 -0
- package/dist/cli/registry.js +40 -0
- package/dist/cli/square-commands.js +219 -0
- package/dist/cmd/notify-once.js +23 -21
- package/dist/compact.js +6 -19
- package/dist/decisions.js +53 -86
- package/dist/delivery-health.js +104 -210
- package/dist/delivery.js +68 -18
- package/dist/doctor.js +9 -8
- package/dist/harness-claude.js +68 -0
- package/dist/harness-codex.js +119 -0
- package/dist/harness-links.js +123 -0
- package/dist/harness-stage.js +36 -0
- package/dist/harness.js +94 -576
- package/dist/help.js +44 -35
- package/dist/inbox.js +12 -11
- package/dist/index.js +30 -129
- package/dist/list.js +1 -1
- package/dist/model.js +0 -6
- package/dist/notification-failures.js +54 -0
- package/dist/notifications.js +47 -62
- package/dist/paseo-timeline.js +58 -188
- package/dist/presentation.js +55 -63
- package/dist/presented.js +9 -8
- package/dist/registry.js +55 -45
- package/dist/runtime.js +26 -137
- package/dist/square-application.js +264 -0
- package/dist/square-core.js +3 -11
- package/dist/square.js +5 -1362
- package/dist/stream.js +27 -126
- package/dist/wake-sink.js +134 -188
- package/dist/watch.js +79 -138
- package/extensions/square-opencode.js +1 -1
- package/extensions/square-pi.js +8 -130
- package/guides/architect.md +3 -3
- package/guides/participant.md +25 -16
- package/package.json +2 -2
- package/skills/brainstorm/SKILL.md +25 -32
- package/skills/square/.claude-plugin/plugin.json +1 -1
- package/skills/square/SKILL.md +39 -107
- package/skills/square-feedback/SKILL.md +4 -4
- package/dist/terminal.js +0 -125
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "square",
|
|
3
|
-
"version": "0.3.
|
|
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
|
|
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": [
|
package/dist/activity-feed.js
CHANGED
|
@@ -1,36 +1,44 @@
|
|
|
1
1
|
import { sameName } from './model.js';
|
|
2
|
-
import {
|
|
3
|
-
import { matchesCatchFilter } from './delivery.js';
|
|
4
|
-
export function
|
|
5
|
-
return acts
|
|
6
|
-
|
|
7
|
-
|
|
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((
|
|
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((
|
|
21
|
+
return delta.filter((act) => act.actor !== undefined && !sameName(act.actor, name) && (act.kind === 'say' || act.kind === 'done'));
|
|
14
22
|
}
|
|
15
|
-
function matchesParticipants(
|
|
16
|
-
return participants === undefined || (
|
|
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(
|
|
19
|
-
if (
|
|
20
|
-
return matchesCatchFilter({ actor:
|
|
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(
|
|
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((
|
|
27
|
-
.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((
|
|
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,
|
|
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 {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
245
|
+
export function isDeliveryReceipt(value) {
|
|
218
246
|
if (!isObject(value))
|
|
219
247
|
return false;
|
|
220
|
-
if (value.status !== 'delivered'
|
|
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
|
|
233
|
-
return isObject(value) && Object.entries(value).every(([id, receipt]) => /^act_\d+$/.test(id) &&
|
|
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
|
|
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
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
doc.runtime
|
|
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
|
|
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
|
-
|
|
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);
|
|
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 =
|
|
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 } : {}) });
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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[
|
|
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 } : {}) } };
|
|
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
|
-
|
|
639
|
-
|
|
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 &&
|
|
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
|
}
|