@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
package/dist/registry.js CHANGED
@@ -9,11 +9,19 @@ import fs from 'node:fs';
9
9
  import path from 'node:path';
10
10
  import { homedir } from 'node:os';
11
11
  import { randomUUID } from 'node:crypto';
12
+ import { loadSquare } from './artifact.js';
12
13
  import { nameKey, sameName } from './model.js';
14
+ import { isCurrentlyJoined } from './runtime.js';
13
15
  const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
14
16
  const COMPACT_BYTES = 64 * 1024;
15
17
  const COMPACT_LINES = 1000;
16
18
  const VALID_CHANNELS = new Set(['claude-code', 'codex', 'opencode', 'pi', 'paseo', 'unknown']);
19
+ const LOCAL_SESSION_SOURCES = [
20
+ { variable: 'CLAUDE_CODE_SESSION_ID', channel: 'claude-code', child: 'CLAUDE_CODE_CHILD_SESSION' },
21
+ { variable: 'CODEX_THREAD_ID', channel: 'codex' },
22
+ { variable: 'OPENCODE_SESSION_ID', channel: 'opencode' },
23
+ { variable: 'SQUARE_PI_SESSION_ID', channel: 'pi' },
24
+ ];
17
25
  export function registryPath() {
18
26
  if (process.env['SQUARE_REGISTRY'])
19
27
  return process.env['SQUARE_REGISTRY'];
@@ -113,9 +121,8 @@ function foldRegistry(raw, now) {
113
121
  }
114
122
  return active.sort((a, b) => b.updatedAt - a.updatedAt);
115
123
  }
116
- function compactRegistry(filePath, raw, now) {
117
- const active = foldRegistry(raw, now);
118
- const compacted = active
124
+ function writeRegistryBindings(filePath, bindings) {
125
+ const compacted = bindings
119
126
  .slice()
120
127
  .reverse()
121
128
  .map((binding) => JSON.stringify({
@@ -135,6 +142,9 @@ function compactRegistry(filePath, raw, now) {
135
142
  fs.writeFileSync(temporary, compacted === '' ? '' : `${compacted}\n`, { mode: 0o600 });
136
143
  fs.renameSync(temporary, filePath);
137
144
  }
145
+ function compactRegistry(filePath, raw, now) {
146
+ writeRegistryBindings(filePath, foldRegistry(raw, now));
147
+ }
138
148
  function maybeCompactRegistry(filePath, now) {
139
149
  let stat;
140
150
  try {
@@ -214,53 +224,53 @@ export function lookupParticipant(squarePath, name, now = Date.now()) {
214
224
  const canonicalPath = canonicalSquarePath(squarePath);
215
225
  return readActiveBindings(now).filter((binding) => binding.squarePath === canonicalPath && sameName(binding.name, name));
216
226
  }
217
- export function localSessionIdentities(env = process.env) {
218
- const paseoAgentId = env['PASEO_AGENT_ID']?.trim() || undefined;
219
- const identities = [];
220
- const claudeSessionId = env['CLAUDE_CODE_SESSION_ID']?.trim();
221
- if (claudeSessionId) {
222
- identities.push({
223
- sessionId: claudeSessionId,
224
- channel: 'claude-code',
225
- child: env['CLAUDE_CODE_CHILD_SESSION'] === '1',
226
- ...(paseoAgentId ? { paseoAgentId } : {}),
227
- });
227
+ /** Resolve the current local harness owner for a participant, if one is registered. */
228
+ export function localParticipantOwner(squarePath, name, env = process.env, now = Date.now()) {
229
+ const sessionIds = new Set(localSessionIdentities(env).map((identity) => identity.sessionId));
230
+ if (sessionIds.size === 0)
231
+ return undefined;
232
+ return lookupParticipant(squarePath, name, now).find((binding) => sessionIds.has(binding.sessionId))?.ownerId;
233
+ }
234
+ function bindingIsProvablyObsolete(binding) {
235
+ if (!fs.existsSync(binding.squarePath))
236
+ return true;
237
+ try {
238
+ return !isCurrentlyJoined(loadSquare(binding.squarePath).acts, binding.name);
228
239
  }
229
- const codexThreadId = env['CODEX_THREAD_ID']?.trim();
230
- if (codexThreadId && !identities.some((identity) => identity.sessionId === codexThreadId)) {
231
- identities.push({
232
- sessionId: codexThreadId,
233
- channel: 'codex',
234
- child: false,
235
- ...(paseoAgentId ? { paseoAgentId } : {}),
236
- });
240
+ catch {
241
+ // A temporarily unreadable artifact is uncertain, so preserve its binding.
242
+ return false;
237
243
  }
238
- const openCodeSessionId = env['OPENCODE_SESSION_ID']?.trim();
239
- if (openCodeSessionId && !identities.some((identity) => identity.sessionId === openCodeSessionId)) {
240
- identities.push({
241
- sessionId: openCodeSessionId,
242
- channel: 'opencode',
243
- child: false,
244
- ...(paseoAgentId ? { paseoAgentId } : {}),
245
- });
244
+ }
245
+ /** Compact the registry and remove only bindings disproved by their authoritative artifact. */
246
+ export function pruneRegistry(now = Date.now()) {
247
+ const filePath = registryPath();
248
+ let raw;
249
+ try {
250
+ raw = fs.readFileSync(filePath, 'utf8');
246
251
  }
247
- const piSessionId = env['SQUARE_PI_SESSION_ID']?.trim();
248
- if (piSessionId && !identities.some((identity) => identity.sessionId === piSessionId)) {
249
- identities.push({
250
- sessionId: piSessionId,
251
- channel: 'pi',
252
- child: false,
253
- ...(paseoAgentId ? { paseoAgentId } : {}),
254
- });
252
+ catch (error) {
253
+ if (error.code === 'ENOENT')
254
+ return { removed: 0, kept: 0 };
255
+ throw error;
255
256
  }
256
- if (paseoAgentId && !identities.some((identity) => identity.sessionId === paseoAgentId)) {
257
- identities.push({
258
- sessionId: paseoAgentId,
259
- channel: 'paseo',
260
- child: false,
261
- paseoAgentId,
262
- });
257
+ const active = foldRegistry(raw, now);
258
+ const kept = active.filter((binding) => !bindingIsProvablyObsolete(binding));
259
+ writeRegistryBindings(filePath, kept);
260
+ return { removed: active.length - kept.length, kept: kept.length };
261
+ }
262
+ function addLocalSession(identities, sessionId, channel, child, paseoAgentId) {
263
+ if (!sessionId || identities.some((identity) => identity.sessionId === sessionId))
264
+ return;
265
+ identities.push({ sessionId, channel, child, ...(paseoAgentId ? { paseoAgentId } : {}) });
266
+ }
267
+ export function localSessionIdentities(env = process.env) {
268
+ const paseoAgentId = env['PASEO_AGENT_ID']?.trim() || undefined;
269
+ const identities = [];
270
+ for (const source of LOCAL_SESSION_SOURCES) {
271
+ addLocalSession(identities, env[source.variable]?.trim(), source.channel, source.child !== undefined && env[source.child] === '1', paseoAgentId);
263
272
  }
273
+ addLocalSession(identities, paseoAgentId, 'paseo', false, paseoAgentId);
264
274
  return identities;
265
275
  }
266
276
  /** True when this process belongs to a harness that can deliver Square attention without a foreground catch. */
package/dist/runtime.js CHANGED
@@ -1,8 +1,4 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { setTimeout as sleep } from 'node:timers/promises';
4
1
  import { fold } from './square-core.js';
5
- import { renderSquareDoc, saveRuntimeSidecar } from './artifact.js';
6
2
  import { SquareError, findParticipantName, nameKey, sameName, } from './model.js';
7
3
  function parseIntegerEnvValue(name, raw, fallback) {
8
4
  if (raw === undefined)
@@ -63,8 +59,13 @@ export function extractMentions(body) {
63
59
  matches.push(match[1]);
64
60
  return matches;
65
61
  }
66
- /** Pure mention filter for say acts. Broadcast bodies (no @) match any named viewer. */
62
+ /** Pure directed-activity filter. Broadcast bodies (no @) match any named viewer. */
67
63
  export function matchesMentionTarget(act, mention) {
64
+ if (act.reach === 'bell')
65
+ return true;
66
+ if (act.reach !== undefined) {
67
+ return mention === true || sameName(act.reach.beside, mention);
68
+ }
68
69
  const mentions = extractMentions(act.body);
69
70
  if (mention === true)
70
71
  return mentions.length > 0;
@@ -108,24 +109,12 @@ export function sayNumberFor(acts, target) {
108
109
  export function doneNames(acts) {
109
110
  return new Set(fold(acts).done.map((participant) => nameKey(participant)));
110
111
  }
111
- export function hasJoined(acts, name) {
112
- return fold(acts).participants.some((participant) => sameName(participant.name, name) && participant.joined);
113
- }
114
112
  export function joinedNames(acts) {
115
113
  return new Set(acts.filter((act) => act.kind === 'join').map((act) => nameKey(act.actor)));
116
114
  }
117
115
  export function isCurrentlyJoined(acts, name) {
118
116
  return fold(acts).participants.some((participant) => sameName(participant.name, name) && participant.joined);
119
117
  }
120
- /** Timestamp of the recipient's most recent join act, if any. */
121
- export function lastJoinAt(acts, name) {
122
- let last;
123
- for (const act of acts) {
124
- if (act.kind === 'join' && sameName(act.actor, name))
125
- last = act.at;
126
- }
127
- return last;
128
- }
129
118
  /** Stable index of the recipient's most recent join act, if any. */
130
119
  export function lastJoinIndex(acts, name) {
131
120
  let last;
@@ -135,11 +124,6 @@ export function lastJoinIndex(acts, name) {
135
124
  }
136
125
  return last;
137
126
  }
138
- /** Notifications are live only when they land after the recipient joined. */
139
- export function isPostJoinActivity(acts, name, actIndex) {
140
- const joinIndex = lastJoinIndex(acts, name);
141
- return joinIndex !== undefined && actIndex > joinIndex;
142
- }
143
127
  export function actStableIndex(act) {
144
128
  if (act.index === undefined)
145
129
  throw new Error(`act ${act.kind} is missing a stable index`);
@@ -162,26 +146,13 @@ export function currentHold(acts) {
162
146
  export function publicActs(acts) {
163
147
  return acts.filter((act) => act.kind === 'say' || act.kind === 'done');
164
148
  }
165
- export function roomChangeActs(acts) {
166
- return acts.filter((act) => act.kind !== 'say' && act.kind !== 'read');
167
- }
168
- export function throttleDelayMs(doc, at) {
169
- const limit = doc.throttlePerMinute;
170
- if (limit === undefined)
171
- return 0;
172
- const recent = foldedState(doc).throttleActivityAts.filter((eventAt) => at - eventAt < THROTTLE_WINDOW_MS).sort((a, b) => a - b);
173
- if (recent.length < limit)
174
- return 0;
175
- const releaseAt = recent[recent.length - limit] + THROTTLE_WINDOW_MS;
176
- return Math.max(1, releaseAt - at);
177
- }
178
149
  function canonicalRuntimeName(doc, name) {
179
150
  return resolveRosterName(doc, name) ?? name;
180
151
  }
181
- export function advanceCursor(doc, name, index, source = 'watch', updatedAt = Date.now()) {
182
- return touchPresenceCursor(doc, name, updatedAt, source, index);
152
+ export function advanceCursor(doc, name, index, updatedAt = Date.now()) {
153
+ return touchPresenceCursor(doc, name, updatedAt, index);
183
154
  }
184
- export function touchPresenceCursor(doc, name, at, source, consumedThroughIndex) {
155
+ export function touchPresenceCursor(doc, name, at, consumedThroughIndex) {
185
156
  if (!Number.isFinite(at))
186
157
  return false;
187
158
  if (consumedThroughIndex !== undefined && (!Number.isInteger(consumedThroughIndex) || consumedThroughIndex < 0))
@@ -192,113 +163,31 @@ export function touchPresenceCursor(doc, name, at, source, consumedThroughIndex)
192
163
  ? (current?.consumedThroughIndex ?? readCursor(doc, key))
193
164
  : Math.max(current?.consumedThroughIndex ?? -1, consumedThroughIndex);
194
165
  const updatedAt = current === undefined ? at : Math.max(current.updatedAt, at);
195
- if (current?.consumedThroughIndex === nextIndex && current.updatedAt === updatedAt && current.source === source)
166
+ if (current?.consumedThroughIndex === nextIndex && current.updatedAt === updatedAt)
196
167
  return false;
197
- doc.runtime.cursors[key] = { consumedThroughIndex: nextIndex, updatedAt, source };
168
+ doc.runtime.cursors[key] = { consumedThroughIndex: nextIndex, updatedAt };
198
169
  return true;
199
170
  }
200
- /** The canonical delivery ledger is keyed by (recipient, stable act id). */
201
- export function deliveryReceipt(doc, name, actOrIndex) {
202
- const id = actId(actOrIndex);
203
- // Delivery planners and runtime writers pass roster-canonical names. Keeping
204
- // this a direct lookup is important: pending/status scans must stay linear in
205
- // activities rather than folding the whole square once per notification.
206
- return doc.runtime.mentionReceipts[name]?.[id];
207
- }
208
- export function isDeliveryDelivered(doc, name, actOrIndex) {
209
- return deliveryReceipt(doc, name, actOrIndex)?.status === 'delivered';
210
- }
211
- export function recordDeliveredDelivery(doc, name, actOrIndex, receipt) {
212
- const key = canonicalRuntimeName(doc, name);
213
- const id = actId(actOrIndex);
214
- const current = deliveryReceipt(doc, key, actOrIndex);
215
- if (current?.status === 'delivered')
216
- return false;
217
- const receipts = doc.runtime.mentionReceipts[key] ?? {};
218
- receipts[id] = { status: 'delivered', ...receipt };
219
- doc.runtime.mentionReceipts[key] = receipts;
220
- return true;
221
- }
222
- export function markDeliveredMention(doc, name, actOrIndex, at = Date.now()) {
223
- return recordDeliveredDelivery(doc, name, actOrIndex, { at });
224
- }
225
- export function mentionDeliveredStatus(doc, name, actOrIndex) {
226
- return isDeliveryDelivered(doc, name, actOrIndex) ? 'delivered' : undefined;
227
- }
228
- export function markDeliveredMentions(doc, name, delivered, at = Date.now()) {
229
- let changed = false;
230
- for (const item of delivered) {
231
- const act = item.act;
232
- if (act.kind !== 'say')
233
- continue;
234
- // A cursor says only where feed reading reached. The recipient/act receipt is
235
- // the delivery fact, so never create one for a broadcast or unrelated say.
236
- const directed = act.reach === 'bell' ||
237
- (act.reach !== undefined && sameName(act.reach.beside, name)) ||
238
- extractMentions(act.body).some((mention) => sameName(mention, name));
239
- if (!directed)
240
- continue;
241
- changed = markDeliveredMention(doc, name, actStableIndex(act), at) || changed;
242
- }
243
- return changed;
244
- }
245
- export function writeSquareDoc(squarePath, doc) {
246
- const dir = path.dirname(squarePath);
247
- const base = path.basename(squarePath);
248
- const tempPath = path.join(dir, `.${base}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`);
249
- fs.writeFileSync(tempPath, renderSquareDoc(doc));
250
- fs.renameSync(tempPath, squarePath);
251
- saveRuntimeSidecar(squarePath, doc.runtime);
252
- }
253
- export function appendAct(squarePath, doc, act) {
254
- const indexed = { ...act, index: doc.runtime.nextActIndex };
255
- doc.runtime.nextActIndex++;
256
- doc.acts.push(indexed);
257
- if (indexed.actor !== undefined)
258
- touchPresenceCursor(doc, indexed.actor, indexed.at, indexed.kind === 'join' ? 'join' : 'api', actStableIndex(indexed));
259
- writeSquareDoc(squarePath, doc);
260
- return indexed;
261
- }
262
- export function latestIndexedActIndex(items) {
263
- return items.reduce((max, item) => Math.max(max, item.index), -1);
171
+ export function latestActIndex(acts) {
172
+ return acts.reduce((max, act) => Math.max(max, act.index), -1);
264
173
  }
265
174
  export function freshWatchLease(doc, name, at = Date.now()) {
266
175
  const key = canonicalRuntimeName(doc, name);
267
- const lease = doc.runtime.leases[key];
176
+ const lease = watchLease(doc, key);
268
177
  if (lease === undefined || lease.expiresAt <= at || at - lease.heartbeatAt > WATCH_STALE_MS)
269
178
  return undefined;
270
179
  return lease;
271
180
  }
272
- export async function withSquareLock(squarePath, fn) {
273
- const lockPath = `${squarePath}.lock`;
274
- const lockDir = path.dirname(lockPath);
275
- fs.mkdirSync(lockDir, { recursive: true });
276
- while (true) {
277
- try {
278
- const fd = fs.openSync(lockPath, 'wx');
279
- fs.writeFileSync(fd, `${process.pid}\n${Date.now()}\n`, 'utf8');
280
- fs.closeSync(fd);
281
- try {
282
- return await fn();
283
- }
284
- finally {
285
- try {
286
- fs.unlinkSync(lockPath);
287
- }
288
- catch { }
289
- }
290
- }
291
- catch (err) {
292
- const errno = err;
293
- if (errno.code !== 'EEXIST')
294
- throw err;
295
- try {
296
- const stat = fs.statSync(lockPath);
297
- if (Date.now() - stat.mtimeMs > LOCK_STALE_MS)
298
- fs.unlinkSync(lockPath);
299
- }
300
- catch { }
301
- await sleep(LOCK_RETRY_MS);
302
- }
303
- }
181
+ export function watchLease(doc, name) {
182
+ return doc.runtime.leases[canonicalRuntimeName(doc, name)];
183
+ }
184
+ export function writeWatchLease(doc, name, lease) {
185
+ doc.runtime.leases[canonicalRuntimeName(doc, name)] = lease;
186
+ }
187
+ export function removeWatchLease(doc, name, leaseId) {
188
+ const key = canonicalRuntimeName(doc, name);
189
+ if (doc.runtime.leases[key]?.leaseId !== leaseId)
190
+ return false;
191
+ delete doc.runtime.leases[key];
192
+ return true;
304
193
  }
@@ -0,0 +1,264 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { setTimeout as sleep } from 'node:timers/promises';
4
+ import { emptyRuntimeState, loadRuntimeSidecar, loadSquare, mergeRuntimeState, renderArtifactAct, renderSquare, renderSquareDoc, saveRuntimeSidecar } from './artifact.js';
5
+ import { coreCompact, coreDone, coreHold, coreResume, decideAct, decideJoin, resolveKnownName } from './decisions.js';
6
+ import { dispatchActNotifications } from './notifications.js';
7
+ import { planRepair } from './doctor.js';
8
+ import { stageReplacement } from './harness-stage.js';
9
+ import { SquareError } from './model.js';
10
+ import { advanceCursor, freshWatchLease, LOCK_RETRY_MS, LOCK_STALE_MS, removeWatchLease, touchPresenceCursor, watchLease, writeWatchLease } from './runtime.js';
11
+ /** The only persistence primitive: one per-square lock, one Markdown write, one sidecar write. */
12
+ export async function withSquareLock(squarePath, fn) {
13
+ const lockPath = `${squarePath}.lock`;
14
+ fs.mkdirSync(path.dirname(lockPath), { recursive: true });
15
+ while (true) {
16
+ let fd;
17
+ try {
18
+ fd = fs.openSync(lockPath, 'wx');
19
+ fs.writeFileSync(fd, `${process.pid}\n${Date.now()}\n`, 'utf8');
20
+ }
21
+ catch (error) {
22
+ if (fd !== undefined) {
23
+ try {
24
+ fs.closeSync(fd);
25
+ }
26
+ catch { }
27
+ try {
28
+ fs.unlinkSync(lockPath);
29
+ }
30
+ catch { }
31
+ }
32
+ const errno = error;
33
+ if (errno.code !== 'EEXIST')
34
+ throw error;
35
+ try {
36
+ if (Date.now() - fs.statSync(lockPath).mtimeMs > LOCK_STALE_MS)
37
+ fs.unlinkSync(lockPath);
38
+ }
39
+ catch { }
40
+ await sleep(LOCK_RETRY_MS);
41
+ continue;
42
+ }
43
+ fs.closeSync(fd);
44
+ try {
45
+ return await fn();
46
+ }
47
+ finally {
48
+ try {
49
+ fs.unlinkSync(lockPath);
50
+ }
51
+ catch { }
52
+ }
53
+ }
54
+ }
55
+ export function writeSquareDoc(squarePath, doc) {
56
+ const temporary = path.join(path.dirname(squarePath), `.${path.basename(squarePath)}.${process.pid}.${Date.now()}.tmp`);
57
+ fs.writeFileSync(temporary, renderSquareDoc(doc));
58
+ fs.renameSync(temporary, squarePath);
59
+ saveRuntimeSidecar(squarePath, doc.runtime);
60
+ }
61
+ export function appendAct(squarePath, doc, act) {
62
+ const stored = applyActs(doc, [act])[0];
63
+ writeSquareDoc(squarePath, doc);
64
+ return stored;
65
+ }
66
+ function applyActs(doc, acts, mutateRuntime) {
67
+ const stored = [];
68
+ for (const act of acts) {
69
+ const item = { ...act, index: doc.runtime.nextActIndex };
70
+ doc.runtime.nextActIndex++;
71
+ doc.acts.push(item);
72
+ if (item.actor !== undefined)
73
+ touchPresenceCursor(doc, item.actor, item.at, item.index);
74
+ stored.push(item);
75
+ }
76
+ mutateRuntime?.(doc);
77
+ return stored;
78
+ }
79
+ /**
80
+ * Publish a dependent persistence file before the Square document. A retained
81
+ * backup lets a failed document commit restore the prior file exactly.
82
+ */
83
+ function prepareAppend(filePath, block, existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '') {
84
+ return stageReplacement(filePath, (stage) => {
85
+ fs.writeFileSync(stage, `${existing}${existing === '' ? '' : '\n'}${block}\n`);
86
+ });
87
+ }
88
+ function plan(doc, intent) {
89
+ switch (intent.type) {
90
+ case 'join': {
91
+ const decision = decideJoin(doc, intent.name, intent.now);
92
+ return { result: decision, acts: [decision.joinAct] };
93
+ }
94
+ case 'say': {
95
+ const decision = decideAct(doc, intent);
96
+ return {
97
+ result: decision,
98
+ acts: decision.type === 'sent' ? [decision.act] : [],
99
+ };
100
+ }
101
+ case 'hold':
102
+ return { result: undefined, acts: [coreHold(doc, intent.actor, intent.body, intent.now)] };
103
+ case 'resume':
104
+ return { result: undefined, acts: [coreResume(doc, intent.actor, intent.now)] };
105
+ case 'done':
106
+ return { result: undefined, acts: [coreDone(doc, intent.name, intent.body, intent.now)] };
107
+ case 'lease': {
108
+ const name = resolveKnownName(doc, intent.name);
109
+ const existing = freshWatchLease(doc, name, intent.at);
110
+ if (existing !== undefined && !intent.force) {
111
+ return { result: { type: 'active', lease: existing }, acts: [] };
112
+ }
113
+ return {
114
+ result: { type: 'started', name, replaced: existing !== undefined },
115
+ acts: [],
116
+ mutateRuntime: (nextDoc) => {
117
+ writeWatchLease(nextDoc, name, {
118
+ leaseId: intent.leaseId,
119
+ ...(intent.ownerId === undefined ? {} : { ownerId: intent.ownerId }),
120
+ heartbeatAt: intent.at,
121
+ expiresAt: intent.expiresAt,
122
+ ...(intent.filter === undefined ? {} : { filter: intent.filter }),
123
+ });
124
+ touchPresenceCursor(nextDoc, name, intent.at);
125
+ },
126
+ };
127
+ }
128
+ case 'release-lease': {
129
+ const name = resolveKnownName(doc, intent.name);
130
+ if (watchLease(doc, name)?.leaseId !== intent.leaseId) {
131
+ return { result: { released: false }, acts: [] };
132
+ }
133
+ return {
134
+ result: { released: true },
135
+ acts: [],
136
+ mutateRuntime: (nextDoc) => { removeWatchLease(nextDoc, name, intent.leaseId); },
137
+ };
138
+ }
139
+ case 'consume': {
140
+ const name = resolveKnownName(doc, intent.name);
141
+ return {
142
+ result: { name },
143
+ acts: [],
144
+ mutateRuntime: (nextDoc) => { advanceCursor(nextDoc, name, intent.throughIndex, intent.at); },
145
+ };
146
+ }
147
+ case 'compact': {
148
+ const result = coreCompact(doc, intent.keep);
149
+ const archive = result.archived;
150
+ return {
151
+ result,
152
+ acts: [],
153
+ replaceDoc: result.doc,
154
+ preparePersistence: archive.length === 0
155
+ ? undefined
156
+ : () => {
157
+ const existing = fs.existsSync(intent.archivePath) ? fs.readFileSync(intent.archivePath, 'utf8') : '';
158
+ const block = archive
159
+ .map((act, index) => renderArtifactAct(act, { first: existing === '' && index === 0 }))
160
+ .join('\n');
161
+ return prepareAppend(intent.archivePath, block, existing);
162
+ },
163
+ };
164
+ }
165
+ case 'repair':
166
+ return {
167
+ result: undefined,
168
+ acts: [],
169
+ replaceDoc: intent.doc,
170
+ preparePersistence: intent.quarantine === undefined || intent.quarantine.blocks.length === 0
171
+ ? undefined
172
+ : () => {
173
+ const block = intent.quarantine.blocks.join('\n\n');
174
+ return prepareAppend(intent.quarantine.path, block);
175
+ },
176
+ };
177
+ }
178
+ }
179
+ function commitPlan(squarePath, doc, planned) {
180
+ const nextDoc = planned.replaceDoc ?? doc;
181
+ const committed = { result: planned.result, acts: applyActs(nextDoc, planned.acts, planned.mutateRuntime) };
182
+ if (planned.acts.length === 0 && planned.mutateRuntime === undefined && planned.replaceDoc === undefined) {
183
+ return committed;
184
+ }
185
+ let persistence;
186
+ try {
187
+ persistence = planned.preparePersistence?.();
188
+ writeSquareDoc(squarePath, nextDoc);
189
+ persistence?.finalize();
190
+ return committed;
191
+ }
192
+ catch (error) {
193
+ try {
194
+ persistence?.rollback();
195
+ }
196
+ catch { }
197
+ throw error;
198
+ }
199
+ }
200
+ /** The one mutation pipeline shared by package and CLI adapters. */
201
+ export async function execute(squarePath, intent) {
202
+ const committed = await withSquareLock(squarePath, () => {
203
+ const doc = loadSquare(squarePath);
204
+ return commitPlan(squarePath, doc, plan(doc, intent));
205
+ });
206
+ for (const act of committed.acts) {
207
+ if (act.kind === 'say')
208
+ await dispatchActNotifications(squarePath, act);
209
+ }
210
+ return committed;
211
+ }
212
+ /** Application-owned artifact creation; adapters provide validated options and stdin text only. */
213
+ export async function createSquare(squarePath, options, snippet) {
214
+ await withSquareLock(squarePath, () => {
215
+ if (fs.existsSync(squarePath) && !options.force) {
216
+ throw new SquareError('conflict', `Refusing to overwrite existing square: ${squarePath}\nPass -f to overwrite.`);
217
+ }
218
+ const temporary = path.join(path.dirname(squarePath), `.${path.basename(squarePath)}.${process.pid}.${Date.now()}.tmp`);
219
+ fs.mkdirSync(path.dirname(squarePath), { recursive: true });
220
+ fs.writeFileSync(temporary, renderSquare(options, snippet));
221
+ fs.renameSync(temporary, squarePath);
222
+ saveRuntimeSidecar(squarePath, emptyRuntimeState(0));
223
+ });
224
+ }
225
+ /** Keep artifact repair planning and dependent quarantine persistence inside the application boundary. */
226
+ export async function repairSquare(squarePath) {
227
+ const result = await withSquareLock(squarePath, () => {
228
+ let text;
229
+ try {
230
+ text = fs.readFileSync(squarePath, 'utf8');
231
+ }
232
+ catch (error) {
233
+ if (error.code === 'ENOENT')
234
+ throw new SquareError('not_found', `square file not found: ${squarePath}`);
235
+ throw error;
236
+ }
237
+ const repair = planRepair(text);
238
+ if (repair.diagnosis.unfixable || repair.repaired === undefined)
239
+ return { repair };
240
+ // Repair changes Markdown only. Keep the sidecar's runtime metadata and
241
+ // merge history boundaries so a doctor run cannot erase delivery state or
242
+ // reuse a stable activity index.
243
+ const sidecarRuntime = loadRuntimeSidecar(squarePath, repair.repaired.doc.runtime);
244
+ const indexesPreserved = repair.diagnosis.acts.every(({ act }, index) => repair.repaired.doc.acts[index]?.index === act.index);
245
+ if (indexesPreserved) {
246
+ repair.repaired.doc.runtime = mergeRuntimeState(repair.repaired.doc.runtime, sidecarRuntime);
247
+ }
248
+ else {
249
+ repair.repaired.doc.runtime = emptyRuntimeState(Math.max(repair.repaired.doc.runtime.nextActIndex, sidecarRuntime.nextActIndex));
250
+ repair.repaired.actions.push({ message: 'reset runtime delivery metadata because act indexes changed' });
251
+ }
252
+ const quarantinePath = squarePath.replace(/\.md$/, '') + '.quarantine.md';
253
+ const intent = {
254
+ type: 'repair',
255
+ doc: repair.repaired.doc,
256
+ ...(repair.repaired.quarantinedBlocks.length === 0
257
+ ? {}
258
+ : { quarantine: { path: quarantinePath, blocks: repair.repaired.quarantinedBlocks } }),
259
+ };
260
+ commitPlan(squarePath, repair.repaired.doc, plan(repair.repaired.doc, intent));
261
+ return { repair };
262
+ });
263
+ return result.repair;
264
+ }
@@ -26,14 +26,6 @@ function touchParticipant(byKey, ordered, actor) {
26
26
  ordered.push(created);
27
27
  return created;
28
28
  }
29
- export function isWarm(lastSeen, now, threshold) {
30
- if (lastSeen === undefined)
31
- return false;
32
- if (!Number.isFinite(lastSeen) || !Number.isFinite(now) || !Number.isFinite(threshold) || threshold <= 0)
33
- return false;
34
- const delta = now - lastSeen;
35
- return delta >= 0 && delta <= threshold;
36
- }
37
29
  function pushThrottleAt(state, at) {
38
30
  if (typeof at === 'number' && Number.isFinite(at))
39
31
  state.throttleActivityAts.push(at);
@@ -59,7 +51,7 @@ function bellRecentAt(state, actor, at, windowMs) {
59
51
  }
60
52
  return latest;
61
53
  }
62
- export function fold(acts, options = {}) {
54
+ export function fold(acts) {
63
55
  const ordered = [];
64
56
  const byKey = new Map();
65
57
  const hold = { active: false };
@@ -168,12 +160,12 @@ export function validate(state, act, options = {}) {
168
160
  return { ok: true };
169
161
  }
170
162
  }
171
- export function perceive(state, act, viewer, options = {}) {
163
+ export function perceive(state, act, viewer) {
172
164
  void state;
173
165
  if (act.kind !== 'say')
174
166
  return 'full';
175
167
  const actor = act.actor;
176
- if ((options.includeActor ?? true) && sameName(actor, viewer))
168
+ if (sameName(actor, viewer))
177
169
  return 'full';
178
170
  if (act.reach === undefined || act.reach === 'bell')
179
171
  return 'full';