@astrosheep/square 0.3.2
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 +25 -0
- package/codex-plugin/hooks/hooks.json +28 -0
- package/dist/activity-feed.js +36 -0
- package/dist/activity.js +151 -0
- package/dist/artifact.js +739 -0
- package/dist/claude-hook.js +112 -0
- package/dist/cmd/notify-once.js +37 -0
- package/dist/compact.js +39 -0
- package/dist/decisions.js +286 -0
- package/dist/delivery-health.js +249 -0
- package/dist/delivery.js +93 -0
- package/dist/doctor.js +34 -0
- package/dist/harness.js +584 -0
- package/dist/help.js +131 -0
- package/dist/inbox.js +33 -0
- package/dist/index.js +163 -0
- package/dist/list.js +126 -0
- package/dist/model.js +44 -0
- package/dist/notifications.js +97 -0
- package/dist/paseo-timeline.js +206 -0
- package/dist/presentation.js +468 -0
- package/dist/presented.js +211 -0
- package/dist/registry.js +299 -0
- package/dist/runtime.js +304 -0
- package/dist/search.js +54 -0
- package/dist/square-core.js +183 -0
- package/dist/square.js +1366 -0
- package/dist/stream.js +149 -0
- package/dist/terminal.js +125 -0
- package/dist/time.js +81 -0
- package/dist/wake-sink.js +219 -0
- package/dist/watch.js +386 -0
- package/extensions/square-opencode.js +87 -0
- package/extensions/square-pi.js +167 -0
- package/guides/architect.md +165 -0
- package/guides/brainstorm.md +404 -0
- package/guides/participant.md +171 -0
- package/package.json +57 -0
- package/skills/brainstorm/SKILL.md +136 -0
- package/skills/square/.claude-plugin/plugin.json +8 -0
- package/skills/square/SKILL.md +154 -0
- package/skills/square/hooks/hooks.json +27 -0
- package/skills/square-feedback/SKILL.md +55 -0
- package/skills/square-feedback/agents/openai.yaml +4 -0
- package/template.md +4 -0
- package/templates/architect.md +4 -0
- package/templates/brainstorm.md +4 -0
package/dist/artifact.js
ADDED
|
@@ -0,0 +1,739 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { ACTIVITIES_HEADING, ACTIVITIES_MARKER, ACT_MARKER_PREFIX, WARMUP_HEADING, WARMUP_MARKER, SquareError, CURRENT_FORMAT_VERSION, formatHardCap, sameName, } from './model.js';
|
|
3
|
+
import { formatTimestamp, parseTimestamp } from './time.js';
|
|
4
|
+
const V1_EVENT_MARKER_PREFIX = '<!-- square:event';
|
|
5
|
+
const V2_KINDS = new Set(['say', 'join', 'done', 'hold', 'resume']);
|
|
6
|
+
export function quoteBody(body) {
|
|
7
|
+
const normalized = body.replace(/\r\n/g, '\n').trim();
|
|
8
|
+
if (normalized === '')
|
|
9
|
+
return '>';
|
|
10
|
+
return normalized
|
|
11
|
+
.split('\n')
|
|
12
|
+
.map((line) => (line === '' ? '>' : `> ${line}`))
|
|
13
|
+
.join('\n');
|
|
14
|
+
}
|
|
15
|
+
export function unquoteBody(lines) {
|
|
16
|
+
const out = [...lines];
|
|
17
|
+
while (out.length > 0 && out[0] === '')
|
|
18
|
+
out.shift();
|
|
19
|
+
while (out.length > 0 && out[out.length - 1] === '')
|
|
20
|
+
out.pop();
|
|
21
|
+
return out
|
|
22
|
+
.map((line) => {
|
|
23
|
+
if (line === '>')
|
|
24
|
+
return '';
|
|
25
|
+
if (line.startsWith('> '))
|
|
26
|
+
return line.slice(2);
|
|
27
|
+
if (line.startsWith('>'))
|
|
28
|
+
return line.slice(1);
|
|
29
|
+
return line;
|
|
30
|
+
})
|
|
31
|
+
.join('\n')
|
|
32
|
+
.trim();
|
|
33
|
+
}
|
|
34
|
+
function actIndex(act) {
|
|
35
|
+
if (act.index === undefined || !Number.isInteger(act.index) || act.index < 0) {
|
|
36
|
+
throw new SquareError('invalid_args', `Invalid square act: missing stable index for ${act.kind}.`);
|
|
37
|
+
}
|
|
38
|
+
return act.index;
|
|
39
|
+
}
|
|
40
|
+
function renderActMarker(act) {
|
|
41
|
+
const marker = {
|
|
42
|
+
index: actIndex(act),
|
|
43
|
+
kind: act.kind,
|
|
44
|
+
actor: act.actor,
|
|
45
|
+
at: act.at,
|
|
46
|
+
...(act.kind === 'say' && act.reach !== undefined ? { reach: act.reach } : {}),
|
|
47
|
+
};
|
|
48
|
+
return `${ACT_MARKER_PREFIX} ${JSON.stringify(marker)} -->`;
|
|
49
|
+
}
|
|
50
|
+
function actNeedsBody(act) {
|
|
51
|
+
return act.kind === 'say' || act.kind === 'done' || act.kind === 'hold';
|
|
52
|
+
}
|
|
53
|
+
export function renderArtifactAct(act, opts = {}) {
|
|
54
|
+
const head = opts.first ? '' : '\n';
|
|
55
|
+
const out = [`${head}${renderActMarker(act)}`, `### ${act.actor}`, `_${act.kind} · ${formatTimestamp(act.at)}_`];
|
|
56
|
+
if (actNeedsBody(act))
|
|
57
|
+
out.push('', quoteBody('body' in act ? act.body ?? '' : ''));
|
|
58
|
+
return out.join('\n');
|
|
59
|
+
}
|
|
60
|
+
export function emptyRuntimeState(nextActIndex = 0) {
|
|
61
|
+
return {
|
|
62
|
+
version: 2,
|
|
63
|
+
nextActIndex,
|
|
64
|
+
firstActIndex: 0,
|
|
65
|
+
cursors: {},
|
|
66
|
+
mentionReceipts: {},
|
|
67
|
+
leases: {},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function renderFrontmatter(doc) {
|
|
71
|
+
return [
|
|
72
|
+
'---',
|
|
73
|
+
`hard_cap: ${formatHardCap(doc.hardCap)}`,
|
|
74
|
+
...(doc.throttlePerMinute === undefined ? [] : [`throttle_per_minute: ${doc.throttlePerMinute}`]),
|
|
75
|
+
`format_version: ${CURRENT_FORMAT_VERSION}`,
|
|
76
|
+
'---',
|
|
77
|
+
].join('\n');
|
|
78
|
+
}
|
|
79
|
+
export function renderSquare(opts, snippet) {
|
|
80
|
+
const templateFile = opts.template
|
|
81
|
+
? new URL(`../templates/${opts.template}.md`, import.meta.url)
|
|
82
|
+
: new URL('../template.md', import.meta.url);
|
|
83
|
+
const template = fs.readFileSync(templateFile, 'utf8');
|
|
84
|
+
const body = template.replace(/^---\n[\s\S]*?\n---\n?/, '').trimStart();
|
|
85
|
+
const genericGuide = fs.readFileSync(new URL('../guides/participant.md', import.meta.url), 'utf8').trim();
|
|
86
|
+
let templateGuide = '';
|
|
87
|
+
if (opts.template) {
|
|
88
|
+
try {
|
|
89
|
+
templateGuide = '\n\n' + fs.readFileSync(new URL(`../guides/${opts.template}.md`, import.meta.url), 'utf8').trim();
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// No template-specific guide.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const training = [
|
|
96
|
+
'---',
|
|
97
|
+
'',
|
|
98
|
+
WARMUP_HEADING,
|
|
99
|
+
WARMUP_MARKER,
|
|
100
|
+
'',
|
|
101
|
+
genericGuide + templateGuide,
|
|
102
|
+
'',
|
|
103
|
+
].join('\n');
|
|
104
|
+
const normalizedSnippet = snippet.replace(/\r\n/g, '\n').trim();
|
|
105
|
+
return [
|
|
106
|
+
renderFrontmatter({
|
|
107
|
+
hardCap: opts.hardCap,
|
|
108
|
+
throttlePerMinute: opts.throttlePerMinute,
|
|
109
|
+
}),
|
|
110
|
+
'',
|
|
111
|
+
normalizedSnippet,
|
|
112
|
+
'',
|
|
113
|
+
training,
|
|
114
|
+
body,
|
|
115
|
+
].join('\n');
|
|
116
|
+
}
|
|
117
|
+
function sidecarPath(squarePath) {
|
|
118
|
+
return `${squarePath}.runtime.json`;
|
|
119
|
+
}
|
|
120
|
+
function loadRuntimeSidecar(squarePath) {
|
|
121
|
+
const sp = sidecarPath(squarePath);
|
|
122
|
+
try {
|
|
123
|
+
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');
|
|
127
|
+
}
|
|
128
|
+
return parsed;
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
if (err.code === 'ENOENT') {
|
|
132
|
+
return emptyRuntimeState(0);
|
|
133
|
+
}
|
|
134
|
+
throw err;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
export function saveRuntimeSidecar(squarePath, runtime) {
|
|
138
|
+
fs.writeFileSync(sidecarPath(squarePath), JSON.stringify(runtime, null, 2));
|
|
139
|
+
}
|
|
140
|
+
export function renderSquareDoc(doc) {
|
|
141
|
+
const warmup = renderWarmupSection(doc.warmup);
|
|
142
|
+
return [
|
|
143
|
+
renderFrontmatter({ hardCap: doc.hardCap, throttlePerMinute: doc.throttlePerMinute }),
|
|
144
|
+
'',
|
|
145
|
+
...doc.preamble,
|
|
146
|
+
...(doc.preamble.length > 0 ? [''] : []),
|
|
147
|
+
...warmup,
|
|
148
|
+
'',
|
|
149
|
+
ACTIVITIES_HEADING,
|
|
150
|
+
ACTIVITIES_MARKER,
|
|
151
|
+
...(doc.acts.length === 0 ? [] : ['', doc.acts.map((act, index) => renderArtifactAct(act, { first: index === 0 })).join('\n')]),
|
|
152
|
+
'',
|
|
153
|
+
].join('\n');
|
|
154
|
+
}
|
|
155
|
+
function renderWarmupSection(warmup) {
|
|
156
|
+
if (warmup[0]?.trim() === WARMUP_HEADING)
|
|
157
|
+
return [warmup[0], WARMUP_MARKER, ...warmup.slice(1)];
|
|
158
|
+
return [WARMUP_MARKER, ...warmup];
|
|
159
|
+
}
|
|
160
|
+
export function parsePreamble(text) {
|
|
161
|
+
const lines = text.split('\n');
|
|
162
|
+
const frontmatterEnd = lines.findIndex((line, index) => index > 0 && line === '---');
|
|
163
|
+
if (lines[0] !== '---' || frontmatterEnd < 0)
|
|
164
|
+
throw new SquareError('invalid_args', 'Invalid square: missing frontmatter.');
|
|
165
|
+
const marker = lines.findIndex((line) => line.trim() === WARMUP_MARKER);
|
|
166
|
+
if (marker < 0)
|
|
167
|
+
throw new SquareError('invalid_args', 'Invalid square: missing embedded warmup.');
|
|
168
|
+
const warmupStart = marker > 0 && lines[marker - 1].trim() === WARMUP_HEADING ? marker - 1 : marker;
|
|
169
|
+
const out = lines.slice(frontmatterEnd + 1, warmupStart);
|
|
170
|
+
trimBlankEdges(out);
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
function trimBlankEdges(lines) {
|
|
174
|
+
while (lines.length > 0 && lines[0] === '')
|
|
175
|
+
lines.shift();
|
|
176
|
+
while (lines.length > 0 && lines[lines.length - 1] === '')
|
|
177
|
+
lines.pop();
|
|
178
|
+
}
|
|
179
|
+
function parseFrontmatter(text) {
|
|
180
|
+
const match = text.match(/^---\n([\s\S]*?)\n---(?:\n|$)/);
|
|
181
|
+
if (!match)
|
|
182
|
+
throw new SquareError('invalid_args', 'Invalid square: missing frontmatter.');
|
|
183
|
+
return match[1];
|
|
184
|
+
}
|
|
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
|
+
export function isObject(value) {
|
|
205
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
206
|
+
}
|
|
207
|
+
export function isReadCursor(value) {
|
|
208
|
+
if (!isObject(value))
|
|
209
|
+
return false;
|
|
210
|
+
return (typeof value.consumedThroughIndex === 'number' &&
|
|
211
|
+
Number.isInteger(value.consumedThroughIndex) &&
|
|
212
|
+
value.consumedThroughIndex >= -1 &&
|
|
213
|
+
typeof value.updatedAt === 'number' &&
|
|
214
|
+
Number.isFinite(value.updatedAt) &&
|
|
215
|
+
(value.source === 'join' || value.source === 'watch' || value.source === 'api'));
|
|
216
|
+
}
|
|
217
|
+
export function isMentionReceipt(value) {
|
|
218
|
+
if (!isObject(value))
|
|
219
|
+
return false;
|
|
220
|
+
if (value.status !== 'delivered' && value.status !== 'presented')
|
|
221
|
+
return false;
|
|
222
|
+
if (typeof value.at !== 'number' || !Number.isFinite(value.at))
|
|
223
|
+
return false;
|
|
224
|
+
if (value.reason !== undefined && value.reason !== 'reconciled')
|
|
225
|
+
return false;
|
|
226
|
+
if (value.actor !== undefined && (typeof value.actor !== 'string' || value.actor === ''))
|
|
227
|
+
return false;
|
|
228
|
+
if (value.reason === 'reconciled' && value.status !== 'delivered')
|
|
229
|
+
return false;
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
export function isMentionReceiptMap(value) {
|
|
233
|
+
return isObject(value) && Object.entries(value).every(([id, receipt]) => /^act_\d+$/.test(id) && isMentionReceipt(receipt));
|
|
234
|
+
}
|
|
235
|
+
export function isWatchLease(value) {
|
|
236
|
+
if (!isObject(value))
|
|
237
|
+
return false;
|
|
238
|
+
if (typeof value.leaseId !== 'string' || value.leaseId === '')
|
|
239
|
+
return false;
|
|
240
|
+
if (typeof value.heartbeatAt !== 'number' || !Number.isFinite(value.heartbeatAt))
|
|
241
|
+
return false;
|
|
242
|
+
if (typeof value.expiresAt !== 'number' || !Number.isFinite(value.expiresAt))
|
|
243
|
+
return false;
|
|
244
|
+
if (value.expiresAt < value.heartbeatAt)
|
|
245
|
+
return false;
|
|
246
|
+
if (value.filter === undefined)
|
|
247
|
+
return true;
|
|
248
|
+
if (!isObject(value.filter))
|
|
249
|
+
return false;
|
|
250
|
+
const participants = value.filter.participants;
|
|
251
|
+
if (participants !== undefined && (!Array.isArray(participants) || !participants.every((item) => typeof item === 'string')))
|
|
252
|
+
return false;
|
|
253
|
+
const mention = value.filter.mention;
|
|
254
|
+
return mention === undefined || typeof mention === 'string';
|
|
255
|
+
}
|
|
256
|
+
function invalidVersionGuidance(reason) {
|
|
257
|
+
return new SquareError('invalid_args', `${reason} This format is no longer supported. Create a new square with \`square build\`.`);
|
|
258
|
+
}
|
|
259
|
+
function parseCap(text) {
|
|
260
|
+
const frontmatter = parseFrontmatter(text);
|
|
261
|
+
const match = frontmatter.match(/^hard_cap:\s*(-1|\d+)\s*$/m);
|
|
262
|
+
if (!match)
|
|
263
|
+
throw new SquareError('invalid_args', 'Invalid square: missing hard_cap in frontmatter. Expected a positive integer or -1.');
|
|
264
|
+
if (match[1] === '-1')
|
|
265
|
+
return null;
|
|
266
|
+
const hardCap = parseInt(match[1], 10);
|
|
267
|
+
if (hardCap <= 0)
|
|
268
|
+
throw new SquareError('invalid_args', 'Invalid square: hard_cap must be a positive integer or -1.');
|
|
269
|
+
return hardCap;
|
|
270
|
+
}
|
|
271
|
+
function parseThrottle(text) {
|
|
272
|
+
const frontmatter = parseFrontmatter(text);
|
|
273
|
+
const match = frontmatter.match(/^throttle_per_minute:\s*(\d+)\s*$/m);
|
|
274
|
+
if (!match)
|
|
275
|
+
return undefined;
|
|
276
|
+
const throttle = parseInt(match[1], 10);
|
|
277
|
+
if (throttle <= 0)
|
|
278
|
+
throw new SquareError('invalid_args', 'Invalid square: throttle_per_minute must be a positive integer.');
|
|
279
|
+
return throttle;
|
|
280
|
+
}
|
|
281
|
+
export function parseFormatVersion(text) {
|
|
282
|
+
const frontmatter = parseFrontmatter(text);
|
|
283
|
+
const match = frontmatter.match(/^format_version:\s*(\d+)\s*$/m);
|
|
284
|
+
if (!match) {
|
|
285
|
+
throw invalidVersionGuidance('Invalid square: missing format_version in frontmatter.');
|
|
286
|
+
}
|
|
287
|
+
const version = parseInt(match[1], 10);
|
|
288
|
+
if (version !== CURRENT_FORMAT_VERSION) {
|
|
289
|
+
throw invalidVersionGuidance(`Invalid square: unsupported format_version ${version} (expected ${CURRENT_FORMAT_VERSION}).`);
|
|
290
|
+
}
|
|
291
|
+
return version;
|
|
292
|
+
}
|
|
293
|
+
export function parseSquare(text) {
|
|
294
|
+
parseFormatVersion(text);
|
|
295
|
+
const runtime = emptyRuntimeState(0);
|
|
296
|
+
const parsedActs = parseActs(text, 0);
|
|
297
|
+
runtime.nextActIndex = parsedActs.nextActIndex;
|
|
298
|
+
runtime.firstActIndex = parsedActs.firstActIndex;
|
|
299
|
+
return {
|
|
300
|
+
hardCap: parseCap(text),
|
|
301
|
+
throttlePerMinute: parseThrottle(text),
|
|
302
|
+
preamble: parsePreamble(text),
|
|
303
|
+
warmup: parseWarmup(text),
|
|
304
|
+
acts: parsedActs.acts,
|
|
305
|
+
runtime,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
export function loadSquare(squarePath) {
|
|
309
|
+
let text;
|
|
310
|
+
try {
|
|
311
|
+
text = fs.readFileSync(squarePath, 'utf8');
|
|
312
|
+
}
|
|
313
|
+
catch (err) {
|
|
314
|
+
if (err.code === 'ENOENT') {
|
|
315
|
+
throw new SquareError('not_found', `square file not found: ${squarePath}`);
|
|
316
|
+
}
|
|
317
|
+
throw err;
|
|
318
|
+
}
|
|
319
|
+
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;
|
|
326
|
+
return doc;
|
|
327
|
+
}
|
|
328
|
+
function isSquareMarker(line) {
|
|
329
|
+
return /^<!-- square:(warmup|activities) -->$/.test(line.trim());
|
|
330
|
+
}
|
|
331
|
+
function findActivitiesMarker(lines, after = -1) {
|
|
332
|
+
return lines.findIndex((line, index) => index > after && line.trim() === ACTIVITIES_MARKER);
|
|
333
|
+
}
|
|
334
|
+
export function parseWarmup(text) {
|
|
335
|
+
const lines = text.split('\n');
|
|
336
|
+
const marker = lines.findIndex((line) => line.trim() === WARMUP_MARKER);
|
|
337
|
+
if (marker < 0)
|
|
338
|
+
throw new SquareError('invalid_args', 'Invalid square: missing embedded warmup.');
|
|
339
|
+
const start = marker > 0 && lines[marker - 1].trim() === WARMUP_HEADING ? marker - 1 : marker;
|
|
340
|
+
const endMarker = findActivitiesMarker(lines, start);
|
|
341
|
+
if (endMarker < 0)
|
|
342
|
+
throw new SquareError('invalid_args', 'Invalid square: missing ACTIVITIES section.');
|
|
343
|
+
const end = lines[endMarker].trim() === ACTIVITIES_MARKER && endMarker > 0 && lines[endMarker - 1].trim() === ACTIVITIES_HEADING
|
|
344
|
+
? endMarker - 1
|
|
345
|
+
: endMarker;
|
|
346
|
+
const out = lines.slice(start, end).filter((line) => !isSquareMarker(line));
|
|
347
|
+
trimSection(out);
|
|
348
|
+
return out;
|
|
349
|
+
}
|
|
350
|
+
function parseReach(value) {
|
|
351
|
+
if (value === undefined)
|
|
352
|
+
return undefined;
|
|
353
|
+
if (value === 'bell')
|
|
354
|
+
return 'bell';
|
|
355
|
+
if (isObject(value) && typeof value.beside === 'string')
|
|
356
|
+
return { beside: value.beside };
|
|
357
|
+
throw new SquareError('invalid_args', 'Invalid square: malformed act reach metadata.');
|
|
358
|
+
}
|
|
359
|
+
export function parseActMarker(line) {
|
|
360
|
+
if (line === undefined || !line.startsWith(ACT_MARKER_PREFIX))
|
|
361
|
+
return null;
|
|
362
|
+
const match = line.match(/^<!-- square:act\s+(\{.*\})\s*-->$/);
|
|
363
|
+
if (!match)
|
|
364
|
+
return null;
|
|
365
|
+
let parsed;
|
|
366
|
+
try {
|
|
367
|
+
parsed = JSON.parse(match[1]);
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
throw new SquareError('invalid_args', 'Invalid square: malformed act marker JSON.');
|
|
371
|
+
}
|
|
372
|
+
if (!isObject(parsed))
|
|
373
|
+
throw new SquareError('invalid_args', 'Invalid square: act marker is missing index metadata.');
|
|
374
|
+
const index = parsed.index;
|
|
375
|
+
if (typeof index !== 'number' || !Number.isInteger(index) || index < 0) {
|
|
376
|
+
throw new SquareError('invalid_args', 'Invalid square: act marker is missing index metadata.');
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
index,
|
|
380
|
+
...(typeof parsed.kind === 'string' ? { kind: parsed.kind } : {}),
|
|
381
|
+
...(typeof parsed.actor === 'string' ? { actor: parsed.actor } : {}),
|
|
382
|
+
...(typeof parsed.at === 'number' && Number.isFinite(parsed.at) ? { at: parsed.at } : {}),
|
|
383
|
+
...(parsed.reach !== undefined ? { reach: parseReach(parsed.reach) } : {}),
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
function normalizeActMeta(marker, kind, actor, head) {
|
|
387
|
+
if (marker.kind === undefined || marker.actor === undefined) {
|
|
388
|
+
throw new SquareError('invalid_args', 'Invalid square: act marker is missing kind/actor metadata.');
|
|
389
|
+
}
|
|
390
|
+
if (marker.kind !== kind) {
|
|
391
|
+
throw new SquareError('invalid_args', `Invalid square: act marker kind ${marker.kind} does not match ${kind}.`);
|
|
392
|
+
}
|
|
393
|
+
if (!sameName(marker.actor, actor)) {
|
|
394
|
+
throw new SquareError('invalid_args', `Invalid square: act marker actor ${marker.actor} does not match ${actor}.`);
|
|
395
|
+
}
|
|
396
|
+
if (marker.at === undefined) {
|
|
397
|
+
throw new SquareError('invalid_args', `Invalid square: act marker is missing timestamp for ${kind} ${actor}.`);
|
|
398
|
+
}
|
|
399
|
+
if (marker.at !== head.at) {
|
|
400
|
+
throw new SquareError('invalid_args', `Invalid square: act marker timestamp does not match ${kind} ${actor}.`);
|
|
401
|
+
}
|
|
402
|
+
return { index: marker.index, ...(marker.reach !== undefined ? { reach: marker.reach } : {}) };
|
|
403
|
+
}
|
|
404
|
+
export function activitiesSourceLines(text) {
|
|
405
|
+
const lines = text.split('\n');
|
|
406
|
+
const marker = findActivitiesMarker(lines);
|
|
407
|
+
if (marker < 0)
|
|
408
|
+
throw new SquareError('invalid_args', 'Invalid square: missing ACTIVITIES section.');
|
|
409
|
+
let start = marker + 1;
|
|
410
|
+
while (start < lines.length && (lines[start] === '' || isSquareMarker(lines[start])))
|
|
411
|
+
start++;
|
|
412
|
+
return lines.slice(start);
|
|
413
|
+
}
|
|
414
|
+
export function parseParticipantHeading(line) {
|
|
415
|
+
const match = line.match(/^### (.+)\s*$/);
|
|
416
|
+
return match ? match[1] : null;
|
|
417
|
+
}
|
|
418
|
+
export function parseActLine(line, actor) {
|
|
419
|
+
const match = line.match(/^_(say|join|done|hold|resume) · ([^_]+)_\s*$/);
|
|
420
|
+
if (!match)
|
|
421
|
+
return null;
|
|
422
|
+
const at = parseTimestamp(match[2]);
|
|
423
|
+
if (!Number.isFinite(at) || !V2_KINDS.has(match[1]))
|
|
424
|
+
return null;
|
|
425
|
+
return { kind: match[1], actor, at };
|
|
426
|
+
}
|
|
427
|
+
function isActSeparator(lines, index) {
|
|
428
|
+
return lines[index]?.trim().startsWith(ACT_MARKER_PREFIX) === true;
|
|
429
|
+
}
|
|
430
|
+
function parseActs(text, _firstActIndex) {
|
|
431
|
+
const lines = activitiesSourceLines(text);
|
|
432
|
+
const acts = [];
|
|
433
|
+
const indexes = new Set();
|
|
434
|
+
let maxIndex = -1;
|
|
435
|
+
let detectedFirst = -1;
|
|
436
|
+
for (let i = 0; i < lines.length;) {
|
|
437
|
+
if (lines[i] === '') {
|
|
438
|
+
i++;
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
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);
|
|
456
|
+
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 } : {}) });
|
|
472
|
+
}
|
|
473
|
+
if (indexes.size > 0) {
|
|
474
|
+
const first = detectedFirst;
|
|
475
|
+
if (Math.min(...indexes) !== first || indexes.size !== maxIndex - first + 1) {
|
|
476
|
+
throw new SquareError('invalid_args', 'Invalid square: act indexes must be contiguous from the first retained index.');
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return { acts, nextActIndex: indexes.size > 0 ? maxIndex + 1 : 0, firstActIndex: detectedFirst >= 0 ? detectedFirst : 0 };
|
|
480
|
+
}
|
|
481
|
+
function isDivider(line) {
|
|
482
|
+
return line.trim() === '---';
|
|
483
|
+
}
|
|
484
|
+
function trimSection(lines) {
|
|
485
|
+
while (lines.length > 0 && (lines[0] === '' || isDivider(lines[0])))
|
|
486
|
+
lines.shift();
|
|
487
|
+
while (lines.length > 0 && (lines[lines.length - 1] === '' || isDivider(lines[lines.length - 1])))
|
|
488
|
+
lines.pop();
|
|
489
|
+
}
|
|
490
|
+
function unfixableResult(reason) {
|
|
491
|
+
return {
|
|
492
|
+
unfixable: reason,
|
|
493
|
+
legacyParticipants: [],
|
|
494
|
+
problems: [],
|
|
495
|
+
hardCap: null,
|
|
496
|
+
preamble: [],
|
|
497
|
+
warmup: [],
|
|
498
|
+
acts: [],
|
|
499
|
+
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
|
+
};
|
|
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
|
+
function tryParseV2ActBlock(blockLines) {
|
|
566
|
+
try {
|
|
567
|
+
let i = 0;
|
|
568
|
+
const marker = parseActMarker(blockLines[i]?.trim());
|
|
569
|
+
if (!marker || marker.kind === undefined || marker.actor === undefined)
|
|
570
|
+
return { ok: false, reason: 'act marker is missing kind/actor metadata.' };
|
|
571
|
+
i++;
|
|
572
|
+
const actor = parseParticipantHeading(blockLines[i] ?? '');
|
|
573
|
+
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 } : {}) } };
|
|
590
|
+
}
|
|
591
|
+
catch {
|
|
592
|
+
return { ok: false, reason: 'malformed act block.' };
|
|
593
|
+
}
|
|
594
|
+
}
|
|
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
|
+
function detectBlockParser(line) {
|
|
617
|
+
const trimmed = line.trim();
|
|
618
|
+
if (trimmed.startsWith(ACT_MARKER_PREFIX))
|
|
619
|
+
return tryParseV2ActBlock;
|
|
620
|
+
if (trimmed.startsWith(V1_EVENT_MARKER_PREFIX))
|
|
621
|
+
return tryParseV1EventBlock;
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
function diagnoseActs(text, problems) {
|
|
625
|
+
const lines = activitiesSourceLines(text);
|
|
626
|
+
const acts = [];
|
|
627
|
+
const quarantined = [];
|
|
628
|
+
const seenIndexes = new Set();
|
|
629
|
+
let i = 0;
|
|
630
|
+
while (i < lines.length) {
|
|
631
|
+
if (lines[i] === '') {
|
|
632
|
+
i++;
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
const parseBlock = detectBlockParser(lines[i]);
|
|
636
|
+
if (!parseBlock) {
|
|
637
|
+
problems.push({ kind: 'act_block', message: `expected act marker, got: ${lines[i]}` });
|
|
638
|
+
quarantined.push({ raw: lines[i], reason: 'expected act marker' });
|
|
639
|
+
i++;
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
const blockStart = i;
|
|
643
|
+
let j = i + 1;
|
|
644
|
+
while (j < lines.length && detectBlockParser(lines[j]) === null)
|
|
645
|
+
j++;
|
|
646
|
+
const blockLines = lines.slice(blockStart, j);
|
|
647
|
+
const outcome = parseBlock(blockLines);
|
|
648
|
+
if (outcome.ok) {
|
|
649
|
+
const index = outcome.act.index;
|
|
650
|
+
if (seenIndexes.has(index))
|
|
651
|
+
problems.push({ kind: 'duplicate_index', message: `duplicate act index ${index}.` });
|
|
652
|
+
seenIndexes.add(index);
|
|
653
|
+
acts.push({ act: outcome.act, raw: blockLines.join('\n') });
|
|
654
|
+
}
|
|
655
|
+
else {
|
|
656
|
+
problems.push({ kind: 'act_block', message: outcome.reason });
|
|
657
|
+
quarantined.push({ raw: blockLines.join('\n'), reason: outcome.reason });
|
|
658
|
+
}
|
|
659
|
+
i = j;
|
|
660
|
+
}
|
|
661
|
+
const uniqueIndexes = [...seenIndexes].sort((a, b) => a - b);
|
|
662
|
+
if (uniqueIndexes.length > 0) {
|
|
663
|
+
const min = uniqueIndexes[0];
|
|
664
|
+
const max = uniqueIndexes[uniqueIndexes.length - 1];
|
|
665
|
+
if (max - min + 1 !== uniqueIndexes.length) {
|
|
666
|
+
problems.push({ kind: 'non_contiguous_indexes', message: 'act indexes are not contiguous.' });
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return { acts, quarantined };
|
|
670
|
+
}
|
|
671
|
+
export function diagnoseSquare(text) {
|
|
672
|
+
const fmMatch = text.match(/^---\n([\s\S]*?)\n---(?:\n|$)/);
|
|
673
|
+
if (!fmMatch)
|
|
674
|
+
return unfixableResult('cannot locate frontmatter: missing opening/closing "---" delimiters.');
|
|
675
|
+
const frontmatter = fmMatch[1];
|
|
676
|
+
const lines = text.split('\n');
|
|
677
|
+
if (!lines.some((line) => line.trim() === WARMUP_MARKER) || !lines.some((line) => line.trim() === ACTIVITIES_MARKER)) {
|
|
678
|
+
return unfixableResult('cannot locate the warmup/activities section markers; the act stream boundaries are unknown.');
|
|
679
|
+
}
|
|
680
|
+
const problems = [];
|
|
681
|
+
const fvMatch = frontmatter.match(/^format_version:\s*(\d+)\s*$/m);
|
|
682
|
+
const formatVersion = fvMatch ? parseInt(fvMatch[1], 10) : undefined;
|
|
683
|
+
if (formatVersion === undefined)
|
|
684
|
+
return unfixableResult('missing format_version in frontmatter. Create a new square with `square build`.');
|
|
685
|
+
if (formatVersion !== CURRENT_FORMAT_VERSION)
|
|
686
|
+
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
|
+
let hardCap = null;
|
|
692
|
+
const hcMatch = frontmatter.match(/^hard_cap:\s*(-1|\d+)\s*$/m);
|
|
693
|
+
if (!hcMatch)
|
|
694
|
+
problems.push({ kind: 'hard_cap', message: 'missing or malformed hard_cap in frontmatter.' });
|
|
695
|
+
else
|
|
696
|
+
hardCap = hcMatch[1] === '-1' ? null : parseInt(hcMatch[1], 10);
|
|
697
|
+
let throttlePerMinute;
|
|
698
|
+
const tMatch = frontmatter.match(/^throttle_per_minute:\s*(\d+)\s*$/m);
|
|
699
|
+
if (tMatch)
|
|
700
|
+
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
|
+
let preamble = [];
|
|
713
|
+
try {
|
|
714
|
+
preamble = parsePreamble(text);
|
|
715
|
+
}
|
|
716
|
+
catch (err) {
|
|
717
|
+
problems.push({ kind: 'preamble', message: err instanceof Error ? err.message : String(err) });
|
|
718
|
+
}
|
|
719
|
+
let warmup = [];
|
|
720
|
+
try {
|
|
721
|
+
warmup = parseWarmup(text);
|
|
722
|
+
}
|
|
723
|
+
catch (err) {
|
|
724
|
+
problems.push({ kind: 'warmup', message: err instanceof Error ? err.message : String(err) });
|
|
725
|
+
}
|
|
726
|
+
const { acts, quarantined } = diagnoseActs(text, problems);
|
|
727
|
+
return {
|
|
728
|
+
formatVersion,
|
|
729
|
+
legacyParticipants,
|
|
730
|
+
problems,
|
|
731
|
+
hardCap,
|
|
732
|
+
throttlePerMinute,
|
|
733
|
+
preamble,
|
|
734
|
+
warmup,
|
|
735
|
+
acts,
|
|
736
|
+
quarantined,
|
|
737
|
+
runtimeRaw,
|
|
738
|
+
};
|
|
739
|
+
}
|