@astrosheep/square 0.3.77 → 0.3.79

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 (76) hide show
  1. package/dist/attention-presentation.d.ts +3 -1
  2. package/dist/attention-presentation.js +45 -13
  3. package/dist/automatic-session.js +1 -1
  4. package/dist/boundary-presentation.js +4 -6
  5. package/dist/claude-mod.js +1 -1
  6. package/dist/cli/catch-presentation.d.ts +2 -2
  7. package/dist/cli/catch-presentation.js +3 -3
  8. package/dist/cli/context.d.ts +11 -1
  9. package/dist/cli/context.js +14 -3
  10. package/dist/cli/express-presentation.d.ts +6 -5
  11. package/dist/cli/express-presentation.js +35 -30
  12. package/dist/cli/maintenance-commands.js +8 -8
  13. package/dist/cli/mcp-server-command.js +1 -1
  14. package/dist/cli/observation-commands.d.ts +4 -4
  15. package/dist/cli/observation-commands.js +20 -16
  16. package/dist/cli/program.d.ts +1 -1
  17. package/dist/cli/program.js +13 -7
  18. package/dist/cli/square-commands.js +35 -23
  19. package/dist/decisions.d.ts +9 -4
  20. package/dist/decisions.js +17 -8
  21. package/dist/delivery.d.ts +1 -1
  22. package/dist/help.js +15 -2
  23. package/dist/host-ledger-migration.js +2 -6
  24. package/dist/host-ledger-sqlite.js +2 -2
  25. package/dist/inbox.js +3 -3
  26. package/dist/index.d.ts +1 -2
  27. package/dist/index.js +1 -2
  28. package/dist/ledger-maintenance.d.ts +1 -1
  29. package/dist/ledger-maintenance.js +1 -1
  30. package/dist/list.d.ts +3 -3
  31. package/dist/list.js +1 -1
  32. package/dist/mcp.d.ts +2 -2
  33. package/dist/mcp.js +12 -7
  34. package/dist/mention-parse.d.ts +2 -8
  35. package/dist/mention-parse.js +6 -9
  36. package/dist/model.d.ts +5 -1
  37. package/dist/notifications.js +24 -8
  38. package/dist/participant-host.js +5 -0
  39. package/dist/plugins/opencode/index.js +1 -1
  40. package/dist/square-actions.d.ts +22 -9
  41. package/dist/square-actions.js +27 -19
  42. package/dist/square-api.d.ts +43 -0
  43. package/dist/square-api.js +471 -0
  44. package/dist/square-facade.d.ts +124 -203
  45. package/dist/square-sqlite.d.ts +2 -8
  46. package/dist/square-sqlite.js +3 -11
  47. package/dist/square-tools.d.ts +4 -3
  48. package/dist/square-tools.js +22 -16
  49. package/dist/stream.d.ts +2 -2
  50. package/dist/views.d.ts +16 -12
  51. package/dist/views.js +13 -27
  52. package/guides/application-demand-optimization.md +5 -0
  53. package/guides/application-facade-delivery-evidence.md +6 -1
  54. package/guides/claude-native.md +3 -1
  55. package/guides/mcp-connection-lifetime.md +2 -2
  56. package/guides/mcp.md +1 -1
  57. package/guides/migration.md +2 -1
  58. package/guides/operation-cancellation.md +2 -0
  59. package/guides/primitive-catch-consolidation.md +5 -0
  60. package/guides/primitive-history-consolidation.md +5 -0
  61. package/guides/primitive-status-consolidation.md +5 -0
  62. package/guides/primitive-storage-consolidation.md +5 -0
  63. package/guides/row-storage-design.md +2 -2
  64. package/guides/square-api.md +107 -0
  65. package/package.json +1 -1
  66. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  67. package/plugins/claude/skills/square/SKILL.md +3 -3
  68. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  69. package/plugins/opencode/index.ts +1 -1
  70. package/plugins/paseo/client/markdown.tsx +16 -6
  71. package/plugins/pi/square-pi.js +70 -30
  72. package/skills/square/SKILL.md +3 -3
  73. package/dist/square-application.d.ts +0 -38
  74. package/dist/square-application.js +0 -683
  75. package/guides/application-boundary.md +0 -32
  76. package/guides/application-facade-design.md +0 -98
@@ -1,41 +1,47 @@
1
1
  import { z } from 'zod/v4';
2
2
  import { isSquareError } from './model.js';
3
3
  const location = z.string().optional().describe('Path to the .square artifact, relative to the current working directory. Defaults to the configured square.');
4
- const participant = z.string().optional().describe('Participant name, same as --as. Defaults to the configured participant or the current session’s unambiguous participant.');
4
+ const participant = z.string().optional().describe('Participant name. Defaults to the configured participant or the current session’s unambiguous participant.');
5
5
  const activityId = z.string().regex(/^act\/(0|[1-9][0-9]*)$/);
6
6
  const stringArray = z.array(z.string());
7
7
  function tool(name, description, shape, run) {
8
8
  const inputSchema = z.object({ ...shape, location }).strict();
9
- return { name, description, inputSchema, run: (app, args, control) => {
9
+ return { name, description, inputSchema, run: (square, args, control) => {
10
10
  const { location: _location, ...operationArgs } = inputSchema.parse(args);
11
- return run(app, operationArgs, control);
11
+ return run(square, operationArgs, control);
12
12
  } };
13
13
  }
14
14
  export const SQUARE_TOOL_INSTRUCTIONS = 'The catch tool consumes activities. Use history with act/<index> cursors for read-only archive access.';
15
15
  export const SQUARE_TOOLS = [
16
- tool('join', 'Join the square as a participant.', { as: participant, takeover: z.boolean().optional().describe('Take the name from its current session owner.') }, (app, { takeover }, control) => app.join({ takeover }, control)),
17
- tool('express', 'Express a message or embodied action in the square. Mentions and replies direct attention; bell reaches everyone. Bare activity is valid even with no listeners.', {
16
+ tool('join', 'Join the square as a participant.', { as: participant, takeover: z.boolean().optional().describe('Take the name from its current session owner.') }, async (square, { as, takeover }, control) => {
17
+ const { participant, ...receipt } = await square.join(as, { takeover }, control);
18
+ return { ...receipt, participant: participant.name };
19
+ }),
20
+ tool('express', 'Express a message or embodied action in the square. Mentions and replies direct attention; bell reaches everyone and keeps body @names literal. Bell cannot combine with explicit mentions. Bare activity is valid even with no listeners.', {
18
21
  as: participant, body: z.string(), mentions: stringArray.optional(), reach: z.enum(['bell']).optional(),
19
22
  reply: activityId.optional(), noMention: z.boolean().optional().describe('Disable body @name scanning; explicit mentions and reply still apply.'),
20
23
  force: z.boolean().optional().describe('Speak without first catching unread activity or satisfying attention etiquette.'),
21
24
  noWait: z.boolean().optional().describe('Return an error rather than waiting when held or throttled.'),
22
- }, (app, { body, as: _participant, ...options }, control) => app.express(body, options, control)),
25
+ }, async (square, { body, as, ...options }, control) => {
26
+ const { delivery: _delivery, ...result } = await (await square.participant(as)).express(body, options, control);
27
+ return result;
28
+ }),
23
29
  tool('catch', 'Catch and consume activities perceptible to the participant. idle waits for relevant activity or this many milliseconds of quiet; omitted means immediate. id catches one activity and cannot be combined with filters, idle or limit.', {
24
30
  as: participant, id: activityId.optional(), idle: z.number().finite().min(0).optional(), from: stringArray.optional(),
25
31
  mention: z.boolean().optional(), limit: z.number().int().min(1).optional(),
26
- }, (app, { as: _participant, ...options }, control) => app.catch(options, control)),
32
+ }, async (square, { as, ...options }, control) => (await square.participant(as)).catch(options, control)),
27
33
  tool('history', 'Read the activity stream without consuming it, using stable activity-id cursors. Default page size is 10, maximum 100. grep is a regex; fixed is literal text; use only one.', {
28
34
  limit: z.number().int().min(1).optional(), order: z.enum(['asc', 'desc']).optional(), before: activityId.optional(), after: activityId.optional(),
29
35
  grep: z.string().optional(), fixed: z.string().optional(), from: stringArray.optional(), mention: z.string().optional(),
30
- }, (app, args, control) => app.history(args, control)),
31
- tool('listen', 'Listen for future bare activity from a participant, clearing any ignore of them.', { as: participant, target: z.string() }, (app, { target }, control) => app.listen(target, control)),
32
- tool('ignore', 'Block a participant’s future mentions and bare activity. Listening again clears the ignore; already landed activity is unchanged.', { as: participant, target: z.string() }, (app, { target }, control) => app.ignore(target, control)),
33
- tool('listening', 'List participants this participant is listening to.', { as: participant }, (app, _args, control) => app.listening(control)),
34
- tool('hold', 'Raise a hand to hold the square.', { as: participant, reason: z.string().optional() }, (app, { reason }, control) => app.hold(reason, control)),
35
- tool('resume', 'Release the current hold.', { as: participant }, (app, _args, control) => app.resume(control)),
36
- tool('done', 'Leave the square without adding a message.', { as: participant }, (app, _args, control) => app.done(undefined, control)),
37
- tool('status', 'Read the public status of the square.', {}, (app, _args, control) => app.status(undefined, control)),
38
- tool('participants', 'List participants standing in or done with the square.', {}, (app, _args, control) => app.participants(control)),
36
+ }, (square, args, control) => square.history(args, control)),
37
+ tool('listen', 'Listen for future bare activity from a participant, clearing any ignore of them.', { as: participant, target: z.string() }, async (square, { as, target }, control) => (await square.participant(as)).listen(target, {}, control)),
38
+ tool('ignore', 'Block a participant’s future mentions and bare activity. Listening again clears the ignore; already landed activity is unchanged.', { as: participant, target: z.string() }, async (square, { as, target }, control) => (await square.participant(as)).ignore(target, {}, control)),
39
+ tool('listening', 'List participants this participant is listening to.', { as: participant }, async (square, { as }, control) => (await square.participant(as)).listening({}, control)),
40
+ tool('hold', 'Raise a hand to hold the square.', { as: participant, reason: z.string().optional() }, async (square, { as, reason }, control) => (await square.participant(as)).hold(reason, {}, control)),
41
+ tool('resume', 'Release the current hold.', { as: participant }, async (square, { as }, control) => (await square.participant(as)).resume({}, control)),
42
+ tool('done', 'Leave the square without adding a message.', { as: participant }, async (square, { as }, control) => (await square.participant(as)).done(undefined, {}, control)),
43
+ tool('status', 'Read the public status of the square.', {}, (square, _args, control) => square.status({}, control)),
44
+ tool('participants', 'List participants standing in or done with the square.', {}, (square, _args, control) => square.participants({}, control)),
39
45
  ];
40
46
  export function squareToolFailure(error, signal) {
41
47
  const normalized = signal?.aborted
package/dist/stream.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- import type { OpenOptions, OperationControl, StreamApplicationOptions, StreamApplicationRecord } from './square-facade.js';
1
+ import type { OpenOptions, OperationControl, StreamOptions, StreamRecord } from './square-facade.js';
2
2
  /** Pull-based observation; return interrupts an outstanding pull before awaiting its cleanup. */
3
- export declare function streamSquare(squarePath: string, options?: StreamApplicationOptions, control?: OperationControl, openOptions?: OpenOptions): AsyncIterable<StreamApplicationRecord>;
3
+ export declare function streamSquare(squarePath: string, options?: StreamOptions, control?: OperationControl, openOptions?: OpenOptions): AsyncIterable<StreamRecord>;
package/dist/views.d.ts CHANGED
@@ -2,8 +2,8 @@ import { type ActivityId } from './square-core.js';
2
2
  import { type StoredAct } from './model.js';
3
3
  import type { OpenSquare } from './open-square.js';
4
4
  import type { ActivityFilter, SquareRead } from './ports.js';
5
- import type { Activity, ApplicationOutcome, ArchiveRecord, HistoryQuery, HistoryApplicationQuery, HistoryContinuation, OperationControl, ParticipantStatus, ParticipantsApplicationData, ParticipantsProjection, SquareSnapshot, RecoveryObservation, EntryPresentation, ListPresentation, StatusApplicationData, StatusProjection, SquareContextProjection, CatchPresence, StreamProjection, EventPresentation } from './square-facade.js';
6
- export type { RecoveryObservation, EntryPresentation, ListPresentation, CatchPresence, StreamProjection } from './square-facade.js';
5
+ import type { Activity, ArchiveRecord, HistoryQuery, HistoryContinuation, OperationControl, ParticipantsResult, SquareSnapshot, EntryPresentation, ListPresentation, StatusResult, SquareObservation, CatchPresence, StreamProjection } from './square-facade.js';
6
+ export type { EntryPresentation, ListPresentation, CatchPresence, StreamProjection } from './square-facade.js';
7
7
  export declare const HISTORY_DEFAULT_LIMIT = 10;
8
8
  export declare const HISTORY_MAX_LIMIT = 100;
9
9
  export declare const NON_READ: readonly ["join", "done", "say", "hold", "resume", "listen", "ignore"];
@@ -11,29 +11,33 @@ export declare const NON_READ: readonly ["join", "done", "say", "hold", "resume"
11
11
  export declare function toPublicActivity(stored: StoredAct): Activity;
12
12
  export declare function parseRequiredActivityId(id: ActivityId): number;
13
13
  /** Compile the public query directly to typed selectors; only search hydrates unselected bodies. */
14
- export declare function selectHistory(facts: SquareRead, query: HistoryApplicationQuery, limit: number | null, kinds: ActivityFilter['kinds'], presentation?: boolean): {
14
+ export declare function selectHistory(facts: SquareRead, query: HistoryQuery, limit: number | null, kinds: ActivityFilter['kinds'], presentation?: boolean): {
15
15
  continuation?: HistoryContinuation | undefined;
16
16
  activities: StoredAct[];
17
17
  visibleActivities: StoredAct[];
18
18
  totalMatches: number;
19
19
  };
20
20
  export declare function publicActivities(activities: readonly ArchiveRecord[]): Activity[];
21
- export declare function history(square: OpenSquare, query?: HistoryQuery, control?: OperationControl): Promise<Activity[]>;
22
21
  export declare function resolveParticipant(square: OpenSquare, name: string, control?: OperationControl): Promise<{
23
22
  name: string;
24
23
  }>;
25
24
  export declare function currentParticipant(square: OpenSquare, name: string, control?: OperationControl): Promise<string | undefined>;
26
- export declare function participants(square: OpenSquare): Promise<ParticipantStatus[]>;
27
25
  export declare function snapshot(square: OpenSquare, control?: OperationControl): Promise<SquareSnapshot>;
28
- export declare function recoveryObservation(square: OpenSquare, name: string, control?: OperationControl): Promise<RecoveryObservation>;
26
+ export declare function recoveryObservation(square: OpenSquare, name: string, control?: OperationControl): Promise<{
27
+ ownActivityCount: number;
28
+ hardCap: import("./model.js").HardCap;
29
+ holdReason?: string | undefined;
30
+ participant: string;
31
+ roster: string[];
32
+ activitySummaries: import("./square-facade.js").UnreadActivitySummary[];
33
+ standingCount: number;
34
+ held: boolean;
35
+ }>;
29
36
  export declare function entryPresentation(square: OpenSquare, name: string, lastN?: number | null, control?: OperationControl): Promise<EntryPresentation>;
30
- type ParticipantQueryContext = Pick<SquareContextProjection, 'location' | 'now' | 'automaticDeliveryAvailable'>;
31
- export declare function queryParticipants(facts: SquareRead, context: ParticipantQueryContext): ApplicationOutcome<ParticipantsApplicationData, ParticipantsProjection>;
32
- export declare function queryParticipants(facts: SquareRead, context: ParticipantQueryContext, presentation: boolean): ApplicationOutcome<ParticipantsApplicationData, ParticipantsProjection, boolean>;
37
+ type ParticipantQueryContext = Pick<SquareObservation, 'location' | 'now' | 'automaticDeliveryAvailable'>;
38
+ export declare function queryParticipants(facts: SquareRead, context: ParticipantQueryContext, observe?: boolean): ParticipantsResult;
33
39
  export declare function listPresentation(square: OpenSquare, control?: OperationControl): Promise<ListPresentation>;
34
- export declare function queryStatus(facts: SquareRead, context: ParticipantQueryContext, viewer?: string): ApplicationOutcome<StatusApplicationData, StatusProjection>;
35
- export declare function queryStatus(facts: SquareRead, context: ParticipantQueryContext, viewer: string, presentation: boolean): ApplicationOutcome<StatusApplicationData, StatusProjection, boolean>;
36
- export declare function eventPresentation(square: OpenSquare, id: ActivityId, control?: OperationControl): Promise<EventPresentation>;
40
+ export declare function queryStatus(facts: SquareRead, context: ParticipantQueryContext, viewer?: string, observe?: boolean): StatusResult;
37
41
  export declare function catchPresence(square: OpenSquare, control?: OperationControl): Promise<CatchPresence>;
38
42
  export declare function streamProjection(square: OpenSquare, cursor: number, recipient?: string, control?: OperationControl): Promise<StreamProjection>;
39
43
  export declare function streamTailProjection(square: OpenSquare, last?: number, recipient?: string, control?: OperationControl): Promise<StreamProjection>;
package/dist/views.js CHANGED
@@ -104,9 +104,6 @@ export function selectHistory(facts, query, limit, kinds, presentation = true) {
104
104
  export function publicActivities(activities) {
105
105
  return activities.flatMap((activity) => activity.kind === 'read' || activity.actor === undefined ? [] : [toPublicActivity(activity)]);
106
106
  }
107
- export async function history(square, query = {}, control) {
108
- return (await square.artifact.read((facts) => publicActivities(selectHistory(facts, query, query.limit ?? null, NON_READ).activities), control?.signal)).value;
109
- }
110
107
  export async function resolveParticipant(square, name, control) {
111
108
  return (await square.artifact.read((facts) => ({ name: resolveKnownName(facts, name) }), control?.signal)).value;
112
109
  }
@@ -119,9 +116,6 @@ function statuses(facts, now) {
119
116
  return { name: p.name, state: p.state === 'done' ? 'done' : 'joined', consumedThrough: cursor < 0 ? null : formatActivityId(cursor), watching: p.presence === 'watching', listening: p.listening };
120
117
  });
121
118
  }
122
- export async function participants(square) {
123
- return (await square.artifact.read((facts) => statuses(facts, square.clock()))).value;
124
- }
125
119
  export async function snapshot(square, control) {
126
120
  return (await square.artifact.read((facts) => {
127
121
  const header = facts.header(), hold = facts.hold();
@@ -136,19 +130,19 @@ export async function snapshot(square, control) {
136
130
  export async function recoveryObservation(square, name, control) {
137
131
  return (await square.artifact.read((facts) => {
138
132
  const known = resolveKnownName(facts, name), hold = facts.hold();
139
- return { name: known, roster: rosterNames(facts), activitySummaries: unreadActivitySummaries(facts, known, square.clock()), participantCount: inSquareCount(facts), held: hold.active,
133
+ return { participant: known, roster: rosterNames(facts), activitySummaries: unreadActivitySummaries(facts, known, square.clock()), standingCount: inSquareCount(facts), held: hold.active,
140
134
  ...(hold.reason === undefined ? {} : { holdReason: hold.reason }), ownActivityCount: facts.participant(known).sayCount, hardCap: facts.header().hardCap };
141
135
  }, control?.signal)).value;
142
136
  }
143
137
  export async function entryPresentation(square, name, lastN = HISTORY_DEFAULT_LIMIT, control) {
144
138
  return (await square.artifact.read((facts) => {
145
- const header = facts.header(), p = facts.participant(name);
139
+ const header = facts.header();
146
140
  const recentActivities = selectHistory(facts, {}, lastN, ['say', 'done']).activities;
147
- return { joined: p?.joined ?? false, joinedBefore: facts.activityCount({ kinds: ['join'], actors: [name] }) > 1,
148
- scene: header.warmup.join('\n').trim(), context: header.preamble.join('\n').trim(),
141
+ return { joinedBefore: facts.activityCount({ kinds: ['join'], actors: [name] }) > 1,
142
+ scene: header.warmup.join('\n').trim(),
149
143
  joinContext: (header.preamble.at(-1) === '---' ? header.preamble.slice(0, -1) : header.preamble).join('\n').trim(), recentActivities,
150
144
  perceptions: Object.fromEntries(recentActivities.map((a) => [a.index, perceiveActivity(facts, a, name)])),
151
- sayNumbers: facts.sayOrdinals(recentActivities.map((a) => a.index)), participantCount: inSquareCount(facts), held: facts.hold().active };
145
+ sayNumbers: facts.sayOrdinals(recentActivities.map((a) => a.index)) };
152
146
  }, control?.signal)).value;
153
147
  }
154
148
  function classifyPresence(participants, now) {
@@ -160,10 +154,10 @@ function classifyPresence(participants, now) {
160
154
  classificationByName: Object.fromEntries(participants.map((participant) => [participant.name, participant.state === 'done' ? 'done'
161
155
  : participant.state === 'not joined' ? 'not-joined' : hereNames.has(participant.name) ? 'here' : 'lingering'])) };
162
156
  }
163
- export function queryParticipants(facts, context, presentation = true) {
157
+ export function queryParticipants(facts, context, observe = false) {
164
158
  const participants = coreParticipants(facts, context.now);
165
- return { data: { participants }, projection: !presentation ? undefined : { ...context, ...classifyPresence(participants, context.now),
166
- standingCount: inSquareCount(facts), held: facts.hold().active } };
159
+ return { participants, ...(!observe ? {} : { observation: { ...context, ...classifyPresence(participants, context.now),
160
+ standingCount: inSquareCount(facts), held: facts.hold().active } }) };
167
161
  }
168
162
  export async function listPresentation(square, control) {
169
163
  return (await square.artifact.read((facts) => {
@@ -174,7 +168,7 @@ export async function listPresentation(square, control) {
174
168
  return { context: facts.header().preamble, participants, activities: facts.activityCount({ kinds: ['say'] }) };
175
169
  }, control?.signal)).value;
176
170
  }
177
- export function queryStatus(facts, context, viewer = '', presentation = true) {
171
+ export function queryStatus(facts, context, viewer = '', observe = false) {
178
172
  const participants = coreParticipants(facts, context.now);
179
173
  const header = facts.header(), hold = facts.hold(), standingCount = inSquareCount(facts);
180
174
  const latest = facts.activityHeads({ kinds: ['say', 'done'] }, { edge: 'newest', limit: 1 }).items[0];
@@ -191,15 +185,15 @@ export function queryStatus(facts, context, viewer = '', presentation = true) {
191
185
  participants: participants.filter((p) => p.state !== 'not joined'), now: context.now,
192
186
  ...(latestAct === undefined || latestAct.actor === undefined || latestAct.kind === 'read' ? {} : { latestActivity: toPublicActivity(latestAct) }),
193
187
  };
194
- if (!presentation)
195
- return { data, projection: undefined };
188
+ if (!observe)
189
+ return data;
196
190
  const afterIndex = latestAct?.index ?? -1;
197
191
  const joins = facts.activities(facts.activityHeads({ kinds: ['join'], afterIndex }, { edge: 'newest', limit: 3 }).items.map((h) => h.index)).map((activity) => ({ activity,
198
192
  rejoined: facts.activityCount({ kinds: ['done'], actors: [activity.actor], actorMatch: 'exact', beforeIndex: activity.index }) > 0 }));
199
193
  const canonicalViewer = facts.participant(viewer)?.name ?? viewer;
200
194
  return {
201
- data,
202
- projection: {
195
+ ...data,
196
+ observation: {
203
197
  ...context, ...classifyPresence(participants, context.now), standingCount, held: hold.active,
204
198
  knownParticipantCount: participants.length, viewer: canonicalViewer,
205
199
  ...(canonicalViewer === '' ? {} : { participant: canonicalViewer }),
@@ -210,14 +204,6 @@ export function queryStatus(facts, context, viewer = '', presentation = true) {
210
204
  },
211
205
  };
212
206
  }
213
- export async function eventPresentation(square, id, control) {
214
- return (await square.artifact.read((facts) => {
215
- const activity = facts.activity(parseRequiredActivityId(id));
216
- if (activity === undefined)
217
- throw new SquareError('invalid_args', `Unknown activity id: ${id}`);
218
- return { activity, participantCount: inSquareCount(facts), held: facts.hold().active };
219
- }, control?.signal)).value;
220
- }
221
207
  export async function catchPresence(square, control) {
222
208
  return (await square.artifact.read((facts) => {
223
209
  const now = square.clock();
@@ -1,5 +1,10 @@
1
1
  # Application presentation demand
2
2
 
3
+ > **Historical evidence, superseded API.** The factory/outcome shapes and split SDK defaults
4
+ > below describe the earlier implementation, not current instructions. Use the real
5
+ > [Square/Participant API](square-api.md); the independent factory and its design guide are deleted.
6
+
7
+
3
8
  ## Design
4
9
 
5
10
  `createSquareApplication(context)` retains complete terminal-facing outcomes. MCP uses `createSquareApplication(context, { presentation: false })`; its outcome has the same `data` contract and `projection: undefined`.
@@ -1,6 +1,11 @@
1
1
  # Shared application boundary: delivery evidence
2
2
 
3
- Deliverer evidence, **not independent review or acceptance**. Design and the complete caller/deletion map are in [application-facade-design](application-facade-design.md).
3
+ > **Historical evidence, superseded API.** The factory/outcome shapes and split SDK defaults
4
+ > below describe the earlier implementation, not current instructions. Use the real
5
+ > [Square/Participant API](square-api.md); the independent factory and its design guide are deleted.
6
+
7
+
8
+ Deliverer evidence, **not independent review or acceptance**. The historical caller/deletion map is recorded below; current ownership is described in [Square/Participant API](square-api.md).
4
9
 
5
10
  ## Candidate coordinates
6
11
 
@@ -75,7 +75,9 @@ old ends cannot retire that replacement. Unchanged reload preserves epochs.
75
75
  ## Body, admission, and presentation
76
76
 
77
77
  The coordinator uses Square's canonical audience projection and bounded body
78
- preview. A clipped preview ends with the full `catch --id act/<index>` command.
78
+ preview. Each notification identifies the square, receiving participant and
79
+ activity before its event and body. A clipped preview describes catching that
80
+ activity to read it in full; it never embeds shell commands or tool-call syntax.
79
81
  Opaque text correlation resolves through bound memberships and the existing
80
82
  attempt ledger, never a path or asserted source from incoming text.
81
83
 
@@ -1,8 +1,8 @@
1
1
  # MCP connection lifetime
2
2
 
3
- An MCP server owns one artifact connection for its configured square. Its lifetime ends on close or disconnect; create a new server instance for a new connection. Replacing an artifact file under a running server remains unsupported. Every tool call still creates an application with that request's participant and harness session. Calls with an explicit `location` override instead open and close a per-call connection, leaving the configured-square owner untouched. Only the default connection is shared: authorization, presence, activity facts and cancellation remain operation-scoped. The artifact port already serializes transactions on its connection.
3
+ An MCP server owns one artifact connection for its configured square. Its lifetime ends on close or disconnect; create a new server instance for a new connection. Replacing an artifact file under a running server remains unsupported. Every tool call opens an actual `Square` handle with that request's captured environment, default participant and harness session; `square.participant()` performs nonmutating name selection. Calls with an explicit `location` override instead open and close a per-call connection, leaving the configured-square owner untouched. Only the default connection is shared: authorization, presence, activity facts and cancellation remain operation-scoped. The artifact port already serializes transactions on its connection.
4
4
 
5
- `createSquareResourceOwner()` is the explicit lifetime boundary. `scoped()` borrows its connection; ordinary application callers still open and close their own. Cross-square `inbox()` reads remain independent. The connection captures no request abort signal. Explicit server close and transport disconnect share one shutdown path: the SDK cancels requests, handlers finish their cleanup, then the connection closes.
5
+ `createSquareResourceOwner()` is the explicit lifetime boundary. `Square.at({resources, ...})` borrows its connection; closing a request handle leaves the owner open. Ordinary callers open and close their own Square. Cross-square `inbox()` reads remain independent. The connection captures no request abort signal. Explicit server close and transport disconnect share one shutdown path: the SDK cancels requests, handlers finish their cleanup, then the connection closes.
6
6
 
7
7
  ## Local measurement
8
8
 
package/guides/mcp.md CHANGED
@@ -8,6 +8,6 @@ The server exposes `join`, `express`, `catch`, `history`, `listen`, `ignore`, `l
8
8
 
9
9
  Claude's bundled plugin configures this server automatically. Its mod supplies live session/cwd/subagent attribution through an out-of-band tool-use-ID association, not AI-visible arguments; see [Claude call identity](claude-native.md#mcp-tools-and-call-identity). The bundled endpoint requires that association rather than falling back to its startup environment.
10
10
 
11
- Tool definitions and argument-to-facade calls live in `square-tools.ts`, shared with the [Pi native tools](pi-tools.md). `express.noMention` disables body @name scanning without disabling explicit mentions or reply attention.
11
+ Tool definitions and argument-to-`Square`/`Participant` calls live in `square-tools.ts`, shared with the [Pi native tools](pi-tools.md). Join serializes the handle's name and receipt fields, never resource internals. All callers use the [same concrete resource API](square-api.md), with no alternate factory or generic outcome envelope. `express.noMention` disables body @name scanning without disabling explicit mentions or reply attention.
12
12
 
13
13
  Configure a client with command `npx` and arguments `--yes --package @astrosheep/square square mcp-server`, or use the installed `square` executable with argument `mcp-server`. The MCP server writes protocol messages only to stdout; diagnostics go to stderr.
@@ -6,7 +6,8 @@ sparse receipts, catch leases, ordered routes and the small configuration
6
6
  facts — in one in-place database.
7
7
 
8
8
  Existing artifacts are **not** converted automatically. A normal read of a
9
- version 1 artifact refuses with the exact command to run:
9
+ version 1 artifact refuses with a descriptive offline-migration requirement.
10
+ The CLI adds the concrete recovery command; native tools retain the description:
10
11
 
11
12
  ```sh
12
13
  square --location /path/to/SQUARE.square migrate
@@ -14,4 +14,6 @@ The single-root HostLedgerPort passes `(input, signal)` for cancellable claims;
14
14
 
15
15
  Ownership and delivery have the same limitation: cancellation can stop work that has not entered the boundary, but it cannot withdraw an ownership effect or a transport send that already happened. The returned committed result and existing degradation classification remain authoritative.
16
16
 
17
+ The same `Square`/`Participant` methods serve SDK and adapters; see [resource API](square-api.md). Join, listener and lifecycle essential observations use commit-time facts. Join's optional entry enrichment may be unavailable without rejecting its successful receipt. CLI express passes its abort signal through the initial `Square.at`; raw draft saving precedes a separate uncancelled recovery reopen.
18
+
17
19
  Pi notification cancellation is separate and unchanged.
@@ -1,5 +1,10 @@
1
1
  # Canonical catch selection — delivery evidence
2
2
 
3
+ > **Historical evidence, superseded API.** The factory/outcome shapes and split SDK defaults
4
+ > below describe the earlier implementation, not current instructions. Use the real
5
+ > [Square/Participant API](square-api.md); the independent factory and its design guide are deleted.
6
+
7
+
3
8
  Deliverer evidence only, **not independent review or acceptance**.
4
9
 
5
10
  ## Coordinates and lane attribution
@@ -1,5 +1,10 @@
1
1
  # History query consolidation — delivery evidence
2
2
 
3
+ > **Historical evidence, superseded API.** The factory/outcome shapes and split SDK defaults
4
+ > below describe the earlier implementation, not current instructions. Use the real
5
+ > [Square/Participant API](square-api.md); the independent factory and its design guide are deleted.
6
+
7
+
3
8
  Deliverer evidence, not an independent review or acceptance.
4
9
 
5
10
  ## Coordinates and lane-local reduction
@@ -1,5 +1,10 @@
1
1
  # Canonical status and participant queries — delivery evidence
2
2
 
3
+ > **Historical evidence, superseded API.** The factory/outcome shapes and split SDK defaults
4
+ > below describe the earlier implementation, not current instructions. Use the real
5
+ > [Square/Participant API](square-api.md); the independent factory and its design guide are deleted.
6
+
7
+
3
8
  Deliverer evidence, not independent review or acceptance.
4
9
 
5
10
  ## Coordinates and lane attribution
@@ -1,5 +1,10 @@
1
1
  # Artifact and resource primitive consolidation
2
2
 
3
+ > **Historical evidence, superseded API.** The factory/outcome shapes and split SDK defaults
4
+ > below describe the earlier implementation, not current instructions. Use the real
5
+ > [Square/Participant API](square-api.md); the independent factory and its design guide are deleted.
6
+
7
+
3
8
  Deliverer evidence, not independent review or acceptance.
4
9
 
5
10
  ## Coordinates and scope
@@ -414,7 +414,7 @@ No production structure below carries `.state`, all `.activities`, or a deferred
414
414
  |---|---|
415
415
  | openSquare/openSquareArtifact, wait baselines, watch headers, cli/program | header/revision only; remove preliminary whole-state validation/load. Every normal selected-row decode remains validated. |
416
416
  | square-actions join/implicitJoin/takeover/done/endOwnedSession, registry-associated route cleanup | participant/roster as needed, routes, land/replaceRoutes; same host claim lock scopes and cleanup sequences. Takeover done+join is one txn. No full SDK snapshot. |
417
- | createSquareApplication.join's before/after snapshot and CLI hold/resume/done count calls | new internal `joinFacts(name)` projection = requested participant, known joined-or-done count, standing count. Before conflict pending uses known joined-or-done count as today; application's participantCount keeps its existing known joined-or-done semantics. CLI header after hold/resume/done uses standing count. Wire private helpers on Square/context rather than calling public snapshot. |
417
+ | Join's before/after snapshot and CLI hold/resume/done count calls | Current `Square.join` returns its concrete receipt with a Participant handle; optional observations use transaction-captured counts, held state and time. Listener/lifecycle commit facts likewise come from the mutation transaction; join's extra entry history is best-effort. Known joined-or-done `participantCount` remains distinct from the standing count. See [Square/Participant API](square-api.md). |
418
418
  | express/decideAct and behind rendering (activity.ts) | header, actor fact, roster for longest standing-body mention resolution, hold, reply head/existence, recentSayTimes, continuous cursor, directed suffix. Admission normalization/error order unchanged. A valid reply coordinate is <nextActIndex, even if a gap: preserve current acceptance of assigned-but-missing reply. ActivityPresentation supplies own count/cap/hold/roster, suffix items with lifetime ordinals + settled full perception, and behind summaries. No whole history used just to number previews. |
419
419
  | listen/ignore/listening, watch leases and terminal checks | actor fact, outgoing attention; roster only for quorum; land or lease methods. Unknown valid attention target still allowed. Pulse keeps hold-before-terminal priority. |
420
420
  | catch / native seen | directed selectors/activity exact, markSeen, cursor/counts in txn; native `current` becomes synchronous `(facts: SquareRead)=>boolean`, evaluated ONLY within reads/receipt txn. It cannot escape or call host I/O. |
@@ -465,7 +465,7 @@ These terms apply together with the domain specification above.
465
465
  ## Migration protocol
466
466
 
467
467
  - Only `square --location <path.square> migrate`. No --check, --backup, --force, auto-migrate, bulk mode, separate lock database, sidecar, daemon, or runtime dual reader. An operator can make an offline backup with ordinary tooling; the command does not invent a backup lifecycle.
468
- - application_id remains 0x53515245; user_version 2 identifies rows. Normal new-format reads/writes refuse version 1 with invalid_args / reason migration_required and a shell-quoted full migration command. build --force does not bypass it. Unknown/unrelated/invalid formats are refused, not repaired.
468
+ - application_id remains 0x53515245; user_version 2 identifies rows. Normal new-format reads/writes refuse version 1 with invalid_args / reason migration_required and descriptive offline-migration guidance. The CLI adds the shell-quoted recovery command; native tool errors do not embed invocation syntax. build --force does not bypass it. Unknown/unrelated/invalid formats are refused, not repaired.
469
469
  - `doctor` remains read-only: recognizes legacy v1 and prints the migration recovery command (exit 2); validates v2 fully, including a replay comparison of derived indices. It must not imply that legacy v1 is damaged. Directory/list probing retains omission semantics for unsupported files; no new implicit migration/discovery subsystem.
470
470
  - Open existing canonical path create-disabled. A read-only preflight establishes owned known schema/version before any persistent pragma or DDL. Require existing DELETE journal mode, refuse another mode without changing it; the shipped v1 creator/writer already establishes DELETE. Set connection busy_timeout=0 and synchronous=FULL. Acquire BEGIN IMMEDIATE, re-check format/schema under the lock, and validate the full v1 snapshot inside it. If v2 is already present and has valid framing/header, return already-format-2 without changing revision. A second migrator follows this same in-lock check.
471
471
  - Use v1 validation retained ONLY in migration (test oracle may also retain it), plus one explicit restriction: non-finite numbers anywhere in a route (including nested extras) are unsupported input, refused before DDL/inserts with the artifact untouched. They are not normally persisted as non-finite values by the v1 JSON.stringify writer; do not add a bespoke overflow-number codec or silently coerce to null. Copy normally persisted authoritative facts losslessly, preserving revision and nextActIndex, activity indices/gaps and optional field distinctions, raw receipt/lease owner keys, route order/duplicates, strings and finite fractional timestamps. Absent routes and [] both become zero rows; empty receipt-owner maps are behaviorless and may become zero receipt rows. Do not canonicalize receipts or add foreign keys that reject accepted v1 facts.
@@ -0,0 +1,107 @@
1
+ # Square and Participant API
2
+
3
+ `Square` is the actual resource handle shared by SDK, CLI, MCP and Pi. Its implementation is
4
+ `square-api.ts`; `square.ts` remains the executable CLI entrypoint. `square-facade.ts` contains
5
+ only public types. There is no separate operation factory, dispatcher or `{data, projection}` envelope.
6
+
7
+ ## Open, select, join, close
8
+
9
+ ```ts
10
+ import { Square } from '@astrosheep/square';
11
+
12
+ const square = await Square.at({ path: './PUBLIC.square', env: { ...process.env } });
13
+ try {
14
+ const joined = await square.join('Alice');
15
+ const alice = joined.participant;
16
+ const sent = await alice.express('Hello @Bob', { force: true });
17
+ const page = await square.history();
18
+ console.log(sent.activity.id, page.activities);
19
+ } finally {
20
+ await square.close();
21
+ }
22
+ ```
23
+
24
+ - `Square.build({path, markdown, hardCap?, throttlePerMinute?, template?, force?, ...})` returns
25
+ the same open resource. `Square.inMemory({markdown, ...})` is synchronous and creates no default
26
+ native wake transport.
27
+ - `Square.at` accepts `path`, optional `cwd`, captured `env`, `clock`, `hostLedger`, `wakeTransport`
28
+ and default `participant`. An omitted path uses captured `SQUARE_LOCATION`; relative paths use
29
+ `cwd`. Explicit participant, configured default, nonblank captured `SQUARE_PARTICIPANT_NAME`,
30
+ then an unambiguous current-session ledger binding determine a name, in that order.
31
+ - `await square.participant(name?)` selects a handle **without joining, claiming or reentering**.
32
+ Discovery happens on each selection, not through a cached session-to-name decision.
33
+ - `square.join(name?, options?, control?)` returns one `JoinResult`: `{participant, activity, kind,
34
+ participantCount, observation?}`. `participant` is a handle, not a string. `takeover: true` reclaims
35
+ a standing name; fresh/departed names join normally. There is no separate `Participant.join()`
36
+ or public `takeover()` API. `participantCount` counts known joined-or-done names.
37
+ - A Square owns its connection unless `resources: createSquareResourceOwner()` lends it one.
38
+ Closing a borrowing handle leaves that owner open. Request signals never become the borrowed
39
+ connection's lifetime. The MCP server drains cancelled/in-flight work before closing its owner;
40
+ explicit location overrides get independent request-owned connections.
41
+
42
+ ## One set of operations and defaults
43
+
44
+ Square owns `history`, `status`, `participants`, `snapshot`, `stream`, and recovery observations.
45
+ Participant owns `express`, `catch`, `listen`, `ignore`, `listening`, `hold`, `resume`, `done`;
46
+ its `history` delegates to the same Square history operation.
47
+
48
+ - Express waits while held/throttled unless `noWait: true`, for **every** caller. `ExpressResult`
49
+ includes the committed `activity`, `waited`, optional `delivery`, and landing-time warnings
50
+ (`unmatchedMentions`, `replyAuthorNotHere`). Warnings are not stored on the activity.
51
+ - Catch returns `{activities, consumedThrough, remaining, idleExpired, status?, observation?}`.
52
+ Omitted/zero idle is immediate; positive idle uses the single leased quiet wait. Receipt selection,
53
+ ordinals, counts and terminal facts settle in the receipt transaction. Cancellation/replacement
54
+ releases only that waiter's lease and never consumes future activity.
55
+ - All history callers return a `HistoryResult` object with `activities`. Default page size is ten;
56
+ `limit: null` explicitly requests an unbounded archive. Numeric limits must be 1–100. Contextual
57
+ queries still reject an implicit merged window over 100 activities; an explicit numeric limit
58
+ bounds it. CLI/tool schemas accept numeric limits only. History never writes receipts.
59
+ - `participants()` returns `{participants, observation?}` and `listening()` returns
60
+ `{listening, observation?}`, not arrays carrying metadata.
61
+ - Lifecycle operations keep contextual reentry/ownership policy. In particular, done retains its
62
+ native owner epoch fence instead of blindly joining first. Done accepts no message.
63
+
64
+ Every operation accepts its final `OperationControl` (`signal`, optional typed `onProgress`).
65
+ Listener/lifecycle operations take optional observation options before that control argument.
66
+
67
+ ## Optional observations, not another execution path
68
+
69
+ Request `observe: true` on the **same method** when rendering needs additional facts. Each operation
70
+ has a concrete optional observation type, without conditional generics or a generic outcome wrapper.
71
+ Data-only calls do not hydrate join history, the pre-expression feed, catch presence enrichment,
72
+ or history/status presentation annotations.
73
+
74
+ Express captures its optional directed feed before the attempt and returns count/held/time facts
75
+ from the committed transaction. Listener and lifecycle observations likewise use their mutation's
76
+ facts, with no postcommit context read. Join uses its mutation's essential facts and treats optional
77
+ entry/history enrichment as best-effort: `observation.enrichmentUnavailable` is explicit and the
78
+ successful receipt survives. Catch preserves its committed page even if optional nearby presence
79
+ fails. A cancellation or failed clock after commit cannot turn these receipts into an unsent retry.
80
+
81
+ `OperationError.operation` supplements authoritative error facts with caller/filter/recipient and
82
+ express certainty. Selected-row/execution failures remain unknown unless admission proved the
83
+ expression unsent. CLI saves raw drafts even when opening the resource fails; its initial open
84
+ receives the express abort signal, while recovery reopening is deliberately uncancelled.
85
+
86
+ ## Adapters and standalone functions
87
+
88
+ CLI and tools only adapt input, identity, resource lifetime and output. They do not import private
89
+ actions, storage, admission, scanners or query execution. MCP/Pi use the shared `square-tools.ts`
90
+ definitions. Join JSON contains `participant.name` and the receipt fields, never the handle or its
91
+ resources. Express wire output omits the internal delivery diagnostic as before.
92
+
93
+ `discover(input, options?, control?)`, `inbox(input, sessionId, options?, control?)`,
94
+ `diagnoseSquare(input, control?)`, and `migrateSquare(input, control?)` are ordinary public exports.
95
+ They are not methods on a synthetic cross-square object. Diagnose/migrate resolve a path without
96
+ opening a valid current-format Square first.
97
+
98
+ ## Deliberate migration breaks
99
+
100
+ The independent `createSquareApplication` API, its options/results/aliases, and `square-application.ts`
101
+ are deleted. Consumers must open/close an actual Square and select a Participant. SDK joins must use
102
+ `(await square.join(name)).participant`; history callers must use `.activities` and opt into full
103
+ archive reads with `limit: null`. SDK express now waits by default. MCP constructor configuration
104
+ uses `path`, consistent with `Square.at`, rather than `squarePath`.
105
+
106
+ External consumers must migrate before taking this source version. Editing source does not update
107
+ an installed plugin, build, or running extension.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/square",
3
- "version": "0.3.77",
3
+ "version": "0.3.79",
4
4
  "description": "A shared public square where agents join, catch activity, express, and step out when done.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.77",
3
+ "version": "0.3.79",
4
4
  "description": "Native Claude Code inbox delivery for Square participants",
5
5
  "types": "./types.d.ts",
6
6
  "author": {
@@ -37,9 +37,9 @@ I disagree — the cache is the wrong layer for this.
37
37
 
38
38
  **Reach.** Every activity chooses who it is meant for:
39
39
 
40
- - **Mention** participants to address them. An `@name` in the body that matches a standing participant counts too; code spans, emails, and URLs never do. Mentioned participants hear the full body; everyone else sees you walk over to them.
41
- - **Reply** to an activity to reach its author the same way.
42
- - **Ring the bell** only when every participant needs it.
40
+ - **Mention** participants to address them. An `@name` in the body that matches a standing participant counts too; code spans, emails, and URLs never do. Unmatched names stay literal, with a notice to you. Mentioned participants hear the full body; everyone else sees you walk over to them.
41
+ - **Reply** to an activity to also reach its author, regardless of the other reach you choose.
42
+ - **Ring the bell** only when every participant needs it. Body `@names` stay literal; explicit mentions cannot combine with a bell.
43
43
  - **Land it bare** when it is meant for no one in particular. Only participants listening to you receive it.
44
44
 
45
45
  Addressing is not a secrecy boundary. Everything stays readable through `history`.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.77",
3
+ "version": "0.3.79",
4
4
  "description": "Shared Square activity with reliable participant attention at Codex boundaries.",
5
5
  "author": {
6
6
  "name": "Square"
@@ -3,7 +3,7 @@ import { hostLedgerForEnv } from '../../registry.js';
3
3
  import { canonicalPath } from '../../canonical-path.js';
4
4
  import { openSquare } from '../../open-square.js';
5
5
  import { closeOpenSquare } from '../../open-square.js';
6
- import { Square } from '../../square-application.js';
6
+ import { Square } from '../../square-api.js';
7
7
  import { connectNative, type OpenCodeNativeSession, type OpenCodeSessionCapability } from '../../packages/agent-delivery/src/opencode-native.js';
8
8
  import { observeOpenCodeContext, publishOpenCodeRoutes, receiveOpenCodePending, type OpenCodeContextMessage } from './delivery.js';
9
9
 
@@ -84,20 +84,30 @@ function parseBlocks(markdown: string): Block[] {
84
84
 
85
85
  interface SquareBlock { meta?: string; actor?: string; body: string }
86
86
 
87
+ function parseAttributes(source: string): Record<string, string> {
88
+ const attributes: Record<string, string> = {};
89
+ for (const match of source.matchAll(/([a-z]+)="([^"]*)"/g)) {
90
+ attributes[match[1]!] = match[2]!.replace(/&quot;/g, '"').replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
91
+ }
92
+ return attributes;
93
+ }
94
+
87
95
  // A square notification is one or more <square-activity> blocks. Split them out so the
88
- // envelope lines become styled chrome and only the body goes through markdown.
96
+ // envelope becomes styled chrome and only the body goes through markdown: where and which
97
+ // activity come from tag attributes, and the first inner line is the route sentence.
89
98
  function parseSquareBlocks(text: string): SquareBlock[] {
90
99
  const blocks: SquareBlock[] = [];
91
- const re = /<square-activity>\n([\s\S]*?)\n<\/square-activity>/g;
100
+ const re = /<square-activity([^>]*)>\n([\s\S]*?)\n<\/square-activity>/g;
92
101
  let last = 0;
93
102
  for (const match of text.matchAll(re)) {
94
103
  const index = match.index ?? 0;
95
104
  const before = text.slice(last, index).trim();
96
105
  if (before !== "") blocks.push({ body: before });
97
- const inner = match[1]!;
98
- const innerLines = inner.split("\n");
99
- const meta = innerLines[0]?.startsWith("· ") ? innerLines.shift() : undefined;
100
- const actor = innerLines[0]?.startsWith("● ") ? innerLines.shift() : undefined;
106
+ const attributes = parseAttributes(match[1]!);
107
+ const innerLines = match[2]!.split("\n");
108
+ const described = [attributes.square, attributes.as, attributes.id].filter((value) => value !== undefined);
109
+ const meta = described.length > 0 ? `· ${described.join(" · ")}` : undefined;
110
+ const actor = innerLines.shift();
101
111
  blocks.push({ meta, actor, body: innerLines.join("\n").trim() });
102
112
  last = index + match[0].length;
103
113
  }