@astrosheep/square 0.3.12 → 0.3.13

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 (121) hide show
  1. package/codex-plugin/.codex-plugin/plugin.json +1 -1
  2. package/codex-plugin/hooks/hooks.json +9 -0
  3. package/dist/activity-feed.d.ts +14 -0
  4. package/dist/activity-feed.js +5 -5
  5. package/dist/activity.d.ts +9 -0
  6. package/dist/activity.js +111 -90
  7. package/dist/artifact.d.ts +20 -0
  8. package/dist/artifact.js +10 -29
  9. package/dist/automatic-session.d.ts +5 -0
  10. package/dist/automatic-session.js +86 -0
  11. package/dist/boundary-presentation.d.ts +5 -0
  12. package/dist/boundary-presentation.js +5 -4
  13. package/dist/claude-hook.d.ts +9 -0
  14. package/dist/claude-hook.js +35 -3
  15. package/dist/cli/context.d.ts +35 -0
  16. package/dist/cli/context.js +25 -30
  17. package/dist/cli/harness-command.d.ts +28 -0
  18. package/dist/cli/harness-command.js +29 -14
  19. package/dist/cli/maintenance-commands.d.ts +7 -0
  20. package/dist/cli/maintenance-commands.js +3 -3
  21. package/dist/cli/meta-commands.d.ts +5 -0
  22. package/dist/cli/observation-commands.d.ts +20 -0
  23. package/dist/cli/observation-commands.js +155 -128
  24. package/dist/cli/program.d.ts +2 -0
  25. package/dist/cli/program.js +2 -12
  26. package/dist/cli/registry.d.ts +9 -0
  27. package/dist/cli/registry.js +1 -2
  28. package/dist/cli/square-commands.d.ts +34 -0
  29. package/dist/cli/square-commands.js +81 -74
  30. package/dist/cmd/notify-once.d.ts +2 -0
  31. package/dist/codex-hook.d.ts +10 -0
  32. package/dist/codex-hook.js +35 -3
  33. package/dist/decisions.d.ts +92 -0
  34. package/dist/decisions.js +83 -77
  35. package/dist/delivery-health.d.ts +21 -0
  36. package/dist/delivery-health.js +40 -34
  37. package/dist/delivery.d.ts +81 -0
  38. package/dist/delivery.js +60 -22
  39. package/dist/file-lock.d.ts +6 -0
  40. package/dist/harness-claude.d.ts +19 -0
  41. package/dist/harness-claude.js +8 -3
  42. package/dist/harness-codex.d.ts +29 -0
  43. package/dist/harness-codex.js +2 -2
  44. package/dist/harness-links.d.ts +17 -0
  45. package/dist/harness-links.js +2 -2
  46. package/dist/harness-pi.d.ts +11 -0
  47. package/dist/harness-pi.js +2 -2
  48. package/dist/harness-stage.d.ts +6 -0
  49. package/dist/harness.d.ts +20 -0
  50. package/dist/harness.js +5 -3
  51. package/dist/help.d.ts +6 -0
  52. package/dist/help.js +6 -5
  53. package/dist/identity.d.ts +13 -0
  54. package/dist/inbox.d.ts +2 -0
  55. package/dist/inbox.js +15 -19
  56. package/dist/index.d.ts +13 -0
  57. package/dist/index.js +31 -69
  58. package/dist/landing.d.ts +10 -0
  59. package/dist/landing.js +95 -0
  60. package/dist/list.d.ts +1 -0
  61. package/dist/list.js +40 -18
  62. package/dist/model.d.ts +147 -0
  63. package/dist/model.js +12 -2
  64. package/dist/notifications.d.ts +30 -0
  65. package/dist/notifications.js +75 -91
  66. package/dist/open-square.d.ts +11 -0
  67. package/dist/open-square.js +4 -0
  68. package/dist/participant-identity.d.ts +1 -0
  69. package/dist/participant-identity.js +3 -0
  70. package/dist/paseo-connection.d.ts +15 -0
  71. package/dist/paseo-delivery.d.ts +14 -0
  72. package/dist/paseo-state.d.ts +11 -0
  73. package/dist/paseo-timeline.d.ts +16 -0
  74. package/dist/presence.d.ts +3 -0
  75. package/dist/presence.js +42 -0
  76. package/dist/presentation.d.ts +91 -0
  77. package/dist/presentation.js +33 -29
  78. package/dist/presented.d.ts +11 -0
  79. package/dist/registry.d.ts +58 -0
  80. package/dist/registry.js +36 -3
  81. package/dist/routes.d.ts +42 -0
  82. package/dist/runtime.d.ts +45 -0
  83. package/dist/runtime.js +42 -34
  84. package/dist/search.d.ts +11 -0
  85. package/dist/square-core.d.ts +111 -0
  86. package/dist/square-core.js +7 -0
  87. package/dist/square-facade.d.ts +86 -0
  88. package/dist/square-facade.js +1 -0
  89. package/dist/square-file-adapter.d.ts +18 -0
  90. package/dist/square-file-adapter.js +70 -0
  91. package/dist/square-storage.d.ts +20 -0
  92. package/dist/square-storage.js +171 -0
  93. package/dist/square-wiring.d.ts +19 -0
  94. package/dist/square-wiring.js +70 -0
  95. package/dist/square.d.ts +2 -0
  96. package/dist/state-cell.d.ts +14 -0
  97. package/dist/state-cell.js +1 -0
  98. package/dist/stream.d.ts +3 -0
  99. package/dist/stream.js +19 -23
  100. package/dist/time.d.ts +6 -0
  101. package/dist/views.d.ts +95 -0
  102. package/dist/views.js +81 -0
  103. package/dist/wake-attempts.d.ts +39 -0
  104. package/dist/wake-evidence.d.ts +12 -0
  105. package/dist/wake-evidence.js +7 -10
  106. package/dist/wake-port.d.ts +20 -0
  107. package/dist/wake-sink.d.ts +12 -0
  108. package/dist/wakes.d.ts +40 -0
  109. package/dist/wakes.js +62 -0
  110. package/dist/watch.d.ts +2 -0
  111. package/dist/watch.js +89 -210
  112. package/extensions/square-opencode.js +27 -1
  113. package/extensions/square-pi.js +13 -0
  114. package/package.json +5 -2
  115. package/skills/brainstorm/SKILL.md +26 -26
  116. package/skills/square/.claude-plugin/plugin.json +1 -1
  117. package/skills/square/SKILL.md +47 -29
  118. package/skills/square/hooks/hooks.json +9 -0
  119. package/skills/square-feedback/SKILL.md +2 -2
  120. package/dist/compact.js +0 -26
  121. package/dist/square-application.js +0 -193
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.12",
3
+ "version": "0.3.13",
4
4
  "description": "Shared Square activity with reliable participant attention at Codex boundaries.",
5
5
  "author": {
6
6
  "name": "Square"
@@ -1,5 +1,14 @@
1
1
  {
2
2
  "hooks": {
3
+ "SessionStart": [
4
+ { "hooks": [{ "type": "command", "command": "square codex-hook", "timeout": 5 }] }
5
+ ],
6
+ "SessionResume": [
7
+ { "hooks": [{ "type": "command", "command": "square codex-hook", "timeout": 5 }] }
8
+ ],
9
+ "SessionEnd": [
10
+ { "hooks": [{ "type": "command", "command": "square codex-hook", "timeout": 5 }] }
11
+ ],
3
12
  "PostToolUse": [
4
13
  {
5
14
  "hooks": [
@@ -0,0 +1,14 @@
1
+ import { type StoredAct, type SquareState, type PublicAct, type RoomChangeAct } from './model.js';
2
+ export interface ActivityFeedFilter {
3
+ participants?: string[];
4
+ mention?: string;
5
+ }
6
+ export declare function actDelta(acts: StoredAct[], cursor: number): StoredAct[];
7
+ /** Public cursor changes plus directed receipts that remain pending behind it. */
8
+ export declare function deliveryDelta(squareState: SquareState, name: string): StoredAct[];
9
+ export declare function peerRoomChanges(delta: StoredAct[], name: string): RoomChangeAct[];
10
+ export declare function peerPublicActs(delta: StoredAct[], name: string): PublicAct[];
11
+ export declare function matchesFeedFilter(act: StoredAct, filter: ActivityFeedFilter): boolean;
12
+ export declare function filteredPeerActivities(delta: StoredAct[], name: string, filter: ActivityFeedFilter): PublicAct[];
13
+ export declare function filteredRoomChanges(delta: StoredAct[], name: string, filter: ActivityFeedFilter): RoomChangeAct[];
14
+ export declare function ackPeerDelta(squareState: SquareState, name: string, delta: StoredAct[], at?: number): boolean;
@@ -5,10 +5,10 @@ export function actDelta(acts, cursor) {
5
5
  return acts.filter((act) => act.index > cursor);
6
6
  }
7
7
  /** Public cursor changes plus directed receipts that remain pending behind it. */
8
- export function deliveryDelta(doc, name) {
9
- const items = actDelta(doc.acts, readCursor(doc, name));
8
+ export function deliveryDelta(squareState, name) {
9
+ const items = actDelta(squareState.acts, readCursor(squareState, name));
10
10
  const seen = new Set(items.map((act) => act.index));
11
- for (const notification of deriveDeliveryModel(doc).pendingFor(name)) {
11
+ for (const notification of deriveDeliveryModel(squareState).pendingFor(name)) {
12
12
  if (!seen.has(notification.item.index))
13
13
  items.push(notification.item);
14
14
  }
@@ -39,6 +39,6 @@ export function filteredRoomChanges(delta, name, filter) {
39
39
  return [];
40
40
  return peerRoomChanges(delta, name).filter((act) => matchesParticipants(act, filter.participants));
41
41
  }
42
- export function ackPeerDelta(doc, name, delta) {
43
- return advanceCursor(doc, name, latestActIndex([...peerPublicActs(delta, name), ...peerRoomChanges(delta, name)]));
42
+ export function ackPeerDelta(squareState, name, delta, at) {
43
+ return advanceCursor(squareState, name, latestActIndex([...peerPublicActs(delta, name), ...peerRoomChanges(delta, name)]), at);
44
44
  }
@@ -0,0 +1,9 @@
1
+ export interface ActivityOptions {
2
+ force?: boolean;
3
+ forceCommand: string;
4
+ noWait?: boolean;
5
+ reach?: import('./model.js').Reach;
6
+ reply?: number;
7
+ }
8
+ export declare function saveActivityDraft(squarePath: string, name: string, body: string): string;
9
+ export declare function cmdActivity(squarePath: string, name: string, activity: string, resolveBody: (arg: string) => string, opts: ActivityOptions): Promise<void>;
package/dist/activity.js CHANGED
@@ -2,12 +2,15 @@ import crypto from 'node:crypto';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { setTimeout as sleep } from 'node:timers/promises';
5
- import { loadSquare } from './artifact.js';
6
- import { SquareError, validateName } from './model.js';
5
+ import { SquareError, isSquareError, validateName } from './model.js';
7
6
  import { expressHintLine, renderActivityBlocked, renderActivityLimit, renderExpressNoWait, renderExpressWaiting, renderPendingFeed, withPathOutput, } from './presentation.js';
8
- import { currentHold, inSquareCount, nowMs, SLEEP_MS, resolveRosterName } from './runtime.js';
9
- import { dispatchActNotifications } from './notifications.js';
10
- import { execute } from './square-application.js';
7
+ import { nowMs, SLEEP_MS } from './runtime.js';
8
+ import { wakeNotifierForSquare } from './notifications.js';
9
+ import { openSquare } from './square-file-adapter.js';
10
+ import { closeOpenSquare } from './open-square.js';
11
+ import { Square } from './square-wiring.js';
12
+ import { activityPresentation, resolveParticipant } from './views.js';
13
+ import { formatActivityId } from './square-core.js';
11
14
  import { formatTimestamp } from './time.js';
12
15
  function draftDirFor(squarePath) {
13
16
  return path.join(path.dirname(squarePath), 'drafts');
@@ -39,114 +42,132 @@ export function saveActivityDraft(squarePath, name, body) {
39
42
  export async function cmdActivity(squarePath, name, activity, resolveBody, opts) {
40
43
  validateName(name);
41
44
  const rawInput = String(resolveBody(activity)).replace(/\r\n/g, '\n');
42
- let doc;
45
+ const reader = await openSquare(squarePath, { clock: nowMs });
46
+ let knownName;
43
47
  try {
44
- doc = loadSquare(squarePath);
48
+ knownName = (await resolveParticipant(reader, name)).name;
45
49
  }
46
50
  catch (err) {
47
- if (err instanceof SquareError) {
51
+ await closeOpenSquare(reader);
52
+ if (isSquareError(err)) {
53
+ if (err.code === 'unknown_participant' || err.code === 'invalid_args') {
54
+ const draftPath = saveActivityDraft(squarePath, name, rawInput);
55
+ process.stderr.write(err.message + '\n');
56
+ process.stderr.write(`draft kept: ${draftPath}\n`);
57
+ process.exit(2);
58
+ }
48
59
  process.stderr.write(err.message + '\n');
49
60
  process.exit(err.code === 'not_found' ? 1 : 2);
50
61
  }
51
62
  throw err;
52
63
  }
53
- const knownName = resolveRosterName(doc, name);
54
- if (knownName === undefined) {
55
- const draftPath = saveActivityDraft(squarePath, name, rawInput);
56
- const expected = doc.acts.filter((act) => act.kind === 'join').map((act) => act.actor);
57
- process.stderr.write(`Unknown participant "${name}". Expected one of: ${expected.join(', ')}.\n`);
58
- process.stderr.write(`draft kept: ${draftPath}\n`);
59
- process.exit(2);
60
- }
64
+ await closeOpenSquare(reader);
61
65
  const body = rawInput.trim();
62
66
  const force = opts.force ?? false;
63
67
  const noWait = opts.noWait ?? false;
64
68
  const reach = opts.reach === 'bell' ? 'bell' : undefined;
65
69
  let announcedWait;
66
- while (true) {
67
- const committed = await execute(squarePath, {
68
- type: 'say',
69
- name,
70
- body,
71
- force,
72
- now: nowMs(),
73
- ...(reach === undefined ? {} : { reach }),
74
- ...(opts.reply === undefined ? {} : { reply: opts.reply }),
75
- });
76
- const decision = committed.result;
77
- const freshDoc = loadSquare(squarePath);
78
- const headerCount = inSquareCount(freshDoc);
79
- const held = currentHold(freshDoc.acts).active;
80
- switch (decision.type) {
81
- case 'sent': {
82
- const sayAct = committed.acts.find((act) => act.kind === 'say');
83
- if (sayAct !== undefined)
84
- await dispatchActNotifications(squarePath, sayAct);
85
- const hasPending = decision.pendingPublic.length > 0 || decision.pendingRoomChanges.length > 0;
86
- const pending = hasPending ? `\n\n${renderPendingFeed(freshDoc.acts, decision.pendingPublic, decision.pendingRoomChanges, knownName)}` : '';
87
- const hint = expressHintLine(decision.ownActCount);
88
- const withHint = hint ? `${decision.confirmation}\n${hint}` : decision.confirmation;
70
+ const square = await Square.at({ path: squarePath, clock: nowMs, notifier: wakeNotifierForSquare(squarePath) });
71
+ try {
72
+ const participant = await square.join(name);
73
+ while (true) {
74
+ const beforeSquare = await openSquare(squarePath, { clock: nowMs });
75
+ const before = await activityPresentation(beforeSquare, knownName).finally(() => closeOpenSquare(beforeSquare));
76
+ const pendingPublic = before.pendingPublic;
77
+ const pendingRoomChanges = before.pendingRoomChanges;
78
+ try {
79
+ await participant.express(body, {
80
+ force,
81
+ ...(reach === undefined ? {} : { reach }),
82
+ ...(opts.reply === undefined ? {} : { reply: formatActivityId(opts.reply) }),
83
+ });
84
+ const freshSquare = await openSquare(squarePath, { clock: nowMs });
85
+ const fresh = await activityPresentation(freshSquare, knownName).finally(() => closeOpenSquare(freshSquare));
86
+ const headerCount = fresh.participantCount;
87
+ const held = fresh.held;
88
+ const ownActCount = fresh.ownActivityCount;
89
+ const hasPending = pendingPublic.length > 0 || pendingRoomChanges.length > 0;
90
+ const pending = hasPending ? `\n\n${renderPendingFeed([...fresh.activities], [...pendingPublic], [...pendingRoomChanges], knownName)}` : '';
91
+ const hint = expressHintLine(ownActCount);
92
+ const confirmation = `● heads turn your way — #${ownActCount}`;
93
+ const withHint = hint ? `${confirmation}\n${hint}` : confirmation;
89
94
  process.stdout.write(withPathOutput(squarePath, withHint + pending, { participantCount: headerCount, held }));
90
95
  return;
91
96
  }
92
- case 'blocked': {
93
- const draftPath = saveActivityDraft(squarePath, name, rawInput);
94
- process.stdout.write(renderActivityBlocked({
95
- squarePath,
96
- name: knownName,
97
- forceCommand: opts.forceCommand,
98
- activitySummaries: decision.activitySummaries,
99
- unreadRoomChanges: decision.unreadRoomChanges,
100
- draftPath,
101
- participantCount: headerCount,
102
- held,
103
- }));
104
- process.exit(1);
105
- break;
106
- }
107
- case 'capped': {
108
- process.stdout.write(renderActivityLimit({
109
- squarePath,
110
- name: knownName,
111
- count: decision.count,
112
- hardCap: decision.hardCap,
113
- draftPath: saveActivityDraft(squarePath, name, rawInput),
114
- participantCount: headerCount,
115
- held,
116
- }));
117
- process.exit(1);
118
- break;
119
- }
120
- case 'throttled': {
121
- if (noWait) {
97
+ catch (error) {
98
+ if (!(error instanceof SquareError))
99
+ throw error;
100
+ const freshSquare = await openSquare(squarePath, { clock: nowMs });
101
+ const fresh = await activityPresentation(freshSquare, knownName).finally(() => closeOpenSquare(freshSquare));
102
+ const headerCount = fresh.participantCount;
103
+ const held = fresh.held;
104
+ if (error.code === 'behind') {
122
105
  const draftPath = saveActivityDraft(squarePath, name, rawInput);
123
- process.stdout.write(renderExpressNoWait({ squarePath, name: knownName, reason: 'throttled', delayMs: decision.delayMs, draftPath, participantCount: headerCount, held }));
106
+ process.stdout.write(renderActivityBlocked({
107
+ squarePath,
108
+ name: knownName,
109
+ forceCommand: opts.forceCommand,
110
+ activitySummaries: [],
111
+ unreadRoomChanges: [...pendingRoomChanges],
112
+ draftPath,
113
+ participantCount: headerCount,
114
+ held,
115
+ }));
124
116
  process.exit(1);
117
+ return;
125
118
  }
126
- if (announcedWait !== 'throttled') {
127
- process.stdout.write(renderExpressWaiting({ reason: 'throttled', delayMs: decision.delayMs }) + '\n');
128
- announcedWait = 'throttled';
129
- }
130
- await sleep(decision.delayMs);
131
- break;
132
- }
133
- case 'held': {
134
- if (noWait) {
135
- const draftPath = saveActivityDraft(squarePath, name, rawInput);
136
- process.stdout.write(renderExpressNoWait({ squarePath, name: knownName, reason: 'held', holdReason: decision.reason, draftPath, participantCount: headerCount, held }));
119
+ if (error.code === 'capped') {
120
+ process.stdout.write(renderActivityLimit({
121
+ squarePath,
122
+ name: knownName,
123
+ count: fresh.ownActivityCount,
124
+ ...(fresh.hardCap === null ? {} : { hardCap: fresh.hardCap }),
125
+ draftPath: saveActivityDraft(squarePath, name, rawInput),
126
+ participantCount: headerCount,
127
+ held,
128
+ }));
137
129
  process.exit(1);
130
+ return;
138
131
  }
139
- if (announcedWait !== 'held') {
140
- process.stdout.write(renderExpressWaiting({ reason: 'held' }) + '\n');
141
- announcedWait = 'held';
132
+ if (error.code === 'throttled') {
133
+ const delayMs = error.facts?.retryAfterMs ?? SLEEP_MS;
134
+ if (noWait) {
135
+ const draftPath = saveActivityDraft(squarePath, name, rawInput);
136
+ process.stdout.write(renderExpressNoWait({ squarePath, name: knownName, reason: 'throttled', delayMs, draftPath, participantCount: headerCount, held }));
137
+ process.exit(1);
138
+ }
139
+ if (announcedWait !== 'throttled') {
140
+ process.stdout.write(renderExpressWaiting({ reason: 'throttled', delayMs }) + '\n');
141
+ announcedWait = 'throttled';
142
+ }
143
+ await sleep(delayMs);
144
+ continue;
142
145
  }
143
- await sleep(SLEEP_MS);
144
- break;
145
- }
146
- case 'bell_quota': {
147
- process.stdout.write(withPathOutput(squarePath, [`✕ the bell stays quiet for now`, ` · you can ring it again at ${formatTimestamp(decision.nextAt)}`].join('\n'), { participantCount: headerCount, held }));
148
- process.exit(1);
146
+ if (error.code === 'held') {
147
+ const holdReason = fresh.holdReason;
148
+ if (noWait) {
149
+ const draftPath = saveActivityDraft(squarePath, name, rawInput);
150
+ process.stdout.write(renderExpressNoWait({ squarePath, name: knownName, reason: 'held', holdReason, draftPath, participantCount: headerCount, held }));
151
+ process.exit(1);
152
+ }
153
+ if (announcedWait !== 'held') {
154
+ process.stdout.write(renderExpressWaiting({ reason: 'held' }) + '\n');
155
+ announcedWait = 'held';
156
+ }
157
+ await sleep(SLEEP_MS);
158
+ continue;
159
+ }
160
+ if (error.code === 'bell_quota') {
161
+ const nextAt = nowMs() + (error.facts?.retryAfterMs ?? 1);
162
+ process.stdout.write(withPathOutput(squarePath, [`✕ the bell stays quiet for now`, ` · you can ring it again at ${formatTimestamp(nextAt)}`].join('\n'), { participantCount: headerCount, held }));
163
+ process.exit(1);
164
+ return;
165
+ }
166
+ throw error;
149
167
  }
150
168
  }
151
169
  }
170
+ finally {
171
+ await square.close();
172
+ }
152
173
  }
@@ -0,0 +1,20 @@
1
+ import { type BuildOptions, type HardCap, type SquareState, type SquareRuntimeState } from './model.js';
2
+ export interface DoctorProblem {
3
+ kind: string;
4
+ message: string;
5
+ }
6
+ export interface DiagnoseResult {
7
+ unfixable?: string;
8
+ problems: DoctorProblem[];
9
+ state?: SquareState;
10
+ }
11
+ export declare function emptyRuntimeState(nextActIndex?: number): SquareRuntimeState;
12
+ export declare function createSquareState(options: BuildOptions & {
13
+ hardCap: HardCap;
14
+ }, snippet: string): SquareState;
15
+ export declare function encodeSquare(squareState: SquareState): Buffer;
16
+ export declare function decodeSquare(bytes: Buffer): SquareState;
17
+ export declare function writeSquareFile(squarePath: string, squareState: SquareState): void;
18
+ export declare function loadSquare(squarePath: string): SquareState;
19
+ export declare function probeSquare(squarePath: string): SquareState | undefined;
20
+ export declare function diagnoseSquareFile(squarePath: string): DiagnoseResult;
package/dist/artifact.js CHANGED
@@ -3,10 +3,9 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { TextDecoder } from 'node:util';
5
5
  import zlib from 'node:zlib';
6
- import { isWakeRouteKind, nameKey, SquareError, } from './model.js';
6
+ import { isWakeRouteKind, InternalSquareError, nameKey, SquareError, } from './model.js';
7
7
  import { parseActivityId } from './square-core.js';
8
8
  const SQUARE_MAGIC = Buffer.from('SQUARE01', 'ascii');
9
- const ARCHIVE_MAGIC = Buffer.from('SQARCH01', 'ascii');
10
9
  const LENGTH_BYTES = 4;
11
10
  const DIGEST_BYTES = 32;
12
11
  const HEADER_BYTES = SQUARE_MAGIC.length + LENGTH_BYTES + DIGEST_BYTES;
@@ -185,7 +184,7 @@ function validateActs(value) {
185
184
  }
186
185
  return true;
187
186
  }
188
- function validateSquareDoc(value) {
187
+ function validateSquareState(value) {
189
188
  if (!isObject(value)
190
189
  || !hasExactKeys(value, ['hardCap', 'preamble', 'warmup', 'acts', 'runtime'], ['throttlePerMinute'])
191
190
  || !(value.hardCap === null || (Number.isSafeInteger(value.hardCap) && value.hardCap > 0))
@@ -279,7 +278,7 @@ function readGuide(name) {
279
278
  throw error;
280
279
  }
281
280
  }
282
- export function createSquareDoc(options, snippet) {
281
+ export function createSquareState(options, snippet) {
283
282
  const guides = [readGuide('participant')];
284
283
  if (options.template !== undefined)
285
284
  guides.push(readGuide(options.template));
@@ -292,23 +291,11 @@ export function createSquareDoc(options, snippet) {
292
291
  runtime: emptyRuntimeState(),
293
292
  };
294
293
  }
295
- export function encodeSquare(doc) {
296
- return encodeEnvelope(SQUARE_MAGIC, validateSquareDoc(doc));
294
+ export function encodeSquare(squareState) {
295
+ return encodeEnvelope(SQUARE_MAGIC, validateSquareState(squareState));
297
296
  }
298
297
  export function decodeSquare(bytes) {
299
- return validateSquareDoc(decodeEnvelope(bytes, SQUARE_MAGIC));
300
- }
301
- export function encodeArchive(acts) {
302
- if (!validateActs(acts))
303
- throw invalidArtifact('archive activity schema is malformed.');
304
- return encodeEnvelope(ARCHIVE_MAGIC, { acts });
305
- }
306
- export function decodeArchive(bytes) {
307
- const value = decodeEnvelope(bytes, ARCHIVE_MAGIC);
308
- if (!isObject(value) || !hasExactKeys(value, ['acts']) || !validateActs(value.acts)) {
309
- throw invalidArtifact('archive schema is malformed.');
310
- }
311
- return value.acts;
298
+ return validateSquareState(decodeEnvelope(bytes, SQUARE_MAGIC));
312
299
  }
313
300
  function readArtifact(squarePath) {
314
301
  try {
@@ -316,7 +303,7 @@ function readArtifact(squarePath) {
316
303
  }
317
304
  catch (error) {
318
305
  if (error.code === 'ENOENT') {
319
- throw new SquareError('not_found', `square file not found: ${squarePath}`);
306
+ throw new InternalSquareError('not_found', `square file not found: ${squarePath}`);
320
307
  }
321
308
  throw error;
322
309
  }
@@ -341,20 +328,14 @@ function atomicWrite(target, bytes) {
341
328
  throw error;
342
329
  }
343
330
  }
344
- export function writeSquareFile(squarePath, doc) {
331
+ export function writeSquareFile(squarePath, squareState) {
345
332
  requireSquareExtension(squarePath);
346
- atomicWrite(squarePath, encodeSquare(doc));
333
+ atomicWrite(squarePath, encodeSquare(squareState));
347
334
  }
348
335
  export function loadSquare(squarePath) {
349
336
  requireSquareExtension(squarePath);
350
337
  return decodeSquare(readArtifact(squarePath));
351
338
  }
352
- export function writeArchiveFile(archivePath, acts) {
353
- atomicWrite(archivePath, encodeArchive(acts));
354
- }
355
- export function loadArchive(archivePath) {
356
- return decodeArchive(readArtifact(archivePath));
357
- }
358
339
  export function probeSquare(squarePath) {
359
340
  if (!squarePath.endsWith('.square'))
360
341
  return undefined;
@@ -382,7 +363,7 @@ export function probeSquare(squarePath) {
382
363
  }
383
364
  export function diagnoseSquareFile(squarePath) {
384
365
  try {
385
- return { problems: [], doc: loadSquare(squarePath) };
366
+ return { problems: [], state: loadSquare(squarePath) };
386
367
  }
387
368
  catch (error) {
388
369
  return {
@@ -0,0 +1,5 @@
1
+ export type AutomaticProvider = 'codex' | 'claude' | 'opencode' | 'pi';
2
+ export declare function publicSquarePath(cwd: string): string;
3
+ export declare function automaticParticipant(provider: AutomaticProvider, sessionId: string, env: NodeJS.ProcessEnv): string;
4
+ export declare function automaticSessionStart(provider: AutomaticProvider, sessionId: string, cwd: string, env?: NodeJS.ProcessEnv): Promise<string | undefined>;
5
+ export declare function automaticSessionEnd(provider: AutomaticProvider, sessionId: string, cwd: string, env?: NodeJS.ProcessEnv): Promise<void>;
@@ -0,0 +1,86 @@
1
+ import { createHash } from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { openSquare } from './square-file-adapter.js';
5
+ import { closeOpenSquare } from './open-square.js';
6
+ import { Square } from './square-wiring.js';
7
+ import { entryPresentation } from './views.js';
8
+ import { canonicalSquarePath, lookupSession, lookupSessionBindings, recordSessionDone, recordSessionJoin } from './registry.js';
9
+ import { participantIdentity, renderAmbientEvent } from './presentation.js';
10
+ import { validateName } from './model.js';
11
+ const providerEnv = {
12
+ codex: 'CODEX_THREAD_ID',
13
+ claude: 'CLAUDE_CODE_SESSION_ID',
14
+ opencode: 'OPENCODE_SESSION_ID',
15
+ pi: 'SQUARE_PI_SESSION_ID',
16
+ };
17
+ export function publicSquarePath(cwd) {
18
+ return path.join(cwd, '.square', 'PUBLIC.square');
19
+ }
20
+ export function automaticParticipant(provider, sessionId, env) {
21
+ const configured = env.SQUARE_PARTICIPANT_NAME?.trim();
22
+ if (configured) {
23
+ validateName(configured);
24
+ return configured;
25
+ }
26
+ const digest = createHash('sha256').update(sessionId, 'utf8').digest('hex').slice(0, 12);
27
+ return `${provider}-${digest}`;
28
+ }
29
+ export async function automaticSessionStart(provider, sessionId, cwd, env = process.env) {
30
+ const squarePath = publicSquarePath(cwd);
31
+ if (!fs.existsSync(squarePath))
32
+ return undefined;
33
+ let reader;
34
+ try {
35
+ reader = await openSquare(squarePath);
36
+ }
37
+ catch (error) {
38
+ process.stderr.write(`! PUBLIC.square unreadable: ${error instanceof Error ? error.message : String(error)}\n`);
39
+ return undefined;
40
+ }
41
+ const name = automaticParticipant(provider, sessionId, env);
42
+ const bindings = lookupSession(sessionId);
43
+ const before = await entryPresentation(reader, name);
44
+ if (bindings.some((binding) => canonicalSquarePath(binding.squarePath) === canonicalSquarePath(squarePath) && binding.name === name) && before.joined) {
45
+ await closeOpenSquare(reader);
46
+ return undefined;
47
+ }
48
+ await closeOpenSquare(reader);
49
+ const square = await Square.at({ path: squarePath });
50
+ try {
51
+ await square.join(name);
52
+ const channel = provider === 'claude' ? 'claude-code' : provider;
53
+ recordSessionJoin(sessionId, name, squarePath, channel, { ...env, [providerEnv[provider]]: sessionId });
54
+ const afterSquare = await openSquare(squarePath);
55
+ const after = await entryPresentation(afterSquare, name, 10).finally(() => closeOpenSquare(afterSquare));
56
+ const activity = after.recentActivities.map((event) => renderAmbientEvent(event, name, {
57
+ now: Date.now(),
58
+ preview: 200,
59
+ actNumber: event.kind === 'say' ? after.sayNumbers[event.index] : undefined,
60
+ })).filter(Boolean).join('\n\n');
61
+ return [`You joined the public square as ${participantIdentity(name)}.`, after.scene, after.context ? `context\n${after.context}` : '', activity ? `recent activity\n${activity}` : ''].filter(Boolean).join('\n\n');
62
+ }
63
+ finally {
64
+ await square.close();
65
+ }
66
+ }
67
+ export async function automaticSessionEnd(provider, sessionId, cwd, env = process.env) {
68
+ const squarePath = publicSquarePath(cwd);
69
+ const channel = provider === 'claude' ? 'claude-code' : provider;
70
+ const binding = lookupSessionBindings(sessionId).find((item) => canonicalSquarePath(item.squarePath) === canonicalSquarePath(squarePath) && item.channel === channel);
71
+ if (binding === undefined || !fs.existsSync(squarePath))
72
+ return;
73
+ const reader = await openSquare(squarePath);
74
+ const joined = await entryPresentation(reader, binding.name).finally(() => closeOpenSquare(reader));
75
+ if (!joined.joined)
76
+ return;
77
+ const square = await Square.at({ path: squarePath });
78
+ try {
79
+ const participant = await square.join(binding.name);
80
+ await participant.done();
81
+ }
82
+ finally {
83
+ await square.close();
84
+ }
85
+ recordSessionDone(sessionId, binding.name, squarePath, channel, env);
86
+ }
@@ -0,0 +1,5 @@
1
+ import type { InboxMembership } from './model.js';
2
+ /** A fresh blocking catch owns only the notifications admitted by its filter. */
3
+ export declare function pendingAtBoundary(inbox: InboxMembership[]): InboxMembership[];
4
+ export declare function renderPendingAtBoundary(inbox: InboxMembership[]): string;
5
+ export declare function presentPendingAtBoundary<T>(sessionId: string, present: (context: string) => T, lookup?: (sessionId: string) => Promise<InboxMembership[]> | InboxMembership[], env?: NodeJS.ProcessEnv): Promise<T | undefined>;
@@ -1,6 +1,6 @@
1
1
  import { leaseOwnsNotification, notificationMessageId } from './delivery.js';
2
2
  import { sessionInbox } from './inbox.js';
3
- import { participantCommandPrefix } from './presentation.js';
3
+ import { participantCommandPrefix, participantIdentity } from './presentation.js';
4
4
  import { presentOnce } from './presented.js';
5
5
  const BODY_MAX = 200;
6
6
  const CONTEXT_MAX = 1200;
@@ -43,7 +43,7 @@ export function renderPendingAtBoundary(inbox) {
43
43
  const command = `${participantCommandPrefix(membership.squarePath, membership.name)} catch --now`;
44
44
  const id = notificationMessageId(membership.squarePath, notification.actIndex);
45
45
  const block = [
46
- `${id} · ${membership.squarePath}: @${membership.name} from @${notification.actor} (${notification.route})`,
46
+ `${id} · ${membership.squarePath}: ${participantIdentity(membership.name)} from ${participantIdentity(notification.actor)} (${notification.route})`,
47
47
  bodyPreview(notification.body),
48
48
  `Ack with: ${command}`,
49
49
  ].join('\n');
@@ -72,6 +72,7 @@ export function renderPendingAtBoundary(inbox) {
72
72
  ...footer,
73
73
  ].join('\n');
74
74
  }
75
- export function presentPendingAtBoundary(sessionId, present, lookup = sessionInbox, env = process.env) {
76
- return presentOnce(sessionId, (id) => pendingAtBoundary(lookup(id)), (inbox) => present(renderPendingAtBoundary(inbox)), env);
75
+ export async function presentPendingAtBoundary(sessionId, present, lookup = sessionInbox, env = process.env) {
76
+ const inbox = await lookup(sessionId);
77
+ return presentOnce(sessionId, () => pendingAtBoundary(inbox), (inbox) => present(renderPendingAtBoundary(inbox)), env);
77
78
  }
@@ -0,0 +1,9 @@
1
+ import type { InboxMembership } from './model.js';
2
+ export interface NativeHookInput {
3
+ session_id?: unknown;
4
+ hook_event_name?: unknown;
5
+ cwd?: unknown;
6
+ }
7
+ export declare function runClaudeHookAsync(inputText: string, env?: NodeJS.ProcessEnv): Promise<string>;
8
+ export declare function claudeHookResponse(input: NativeHookInput, lookup?: (sessionId: string) => Promise<InboxMembership[]> | InboxMembership[], env?: NodeJS.ProcessEnv): Promise<object | undefined>;
9
+ export declare function runClaudeHook(inputText: string, env?: NodeJS.ProcessEnv): Promise<string>;
@@ -1,13 +1,45 @@
1
1
  import { presentPendingAtBoundary } from './boundary-presentation.js';
2
2
  import { sessionInbox } from './inbox.js';
3
- export function claudeHookResponse(input, lookup = sessionInbox, env = process.env) {
3
+ import { automaticSessionEnd, automaticSessionStart } from './automatic-session.js';
4
+ export async function runClaudeHookAsync(inputText, env = process.env) {
5
+ let input;
6
+ try {
7
+ input = JSON.parse(inputText);
8
+ }
9
+ catch {
10
+ return '';
11
+ }
12
+ if (input === null || typeof input !== 'object')
13
+ return '';
14
+ const value = input;
15
+ if (typeof value.session_id !== 'string' || typeof value.cwd !== 'string')
16
+ return runClaudeHook(inputText, env);
17
+ if (value.hook_event_name === 'SessionStart' || value.hook_event_name === 'SessionResume') {
18
+ try {
19
+ const context = await automaticSessionStart('claude', value.session_id, value.cwd, env);
20
+ return context === undefined ? '' : `${JSON.stringify({ hookSpecificOutput: { hookEventName: value.hook_event_name, additionalContext: context } })}\n`;
21
+ }
22
+ catch {
23
+ return '';
24
+ }
25
+ }
26
+ if (value.hook_event_name === 'SessionEnd') {
27
+ try {
28
+ await automaticSessionEnd('claude', value.session_id, value.cwd, env);
29
+ }
30
+ catch { /* end remains bounded */ }
31
+ return '';
32
+ }
33
+ return runClaudeHook(inputText, env);
34
+ }
35
+ export async function claudeHookResponse(input, lookup = sessionInbox, env = process.env) {
4
36
  if (typeof input.session_id !== 'string' || input.session_id === '')
5
37
  return undefined;
6
38
  if (input.hook_event_name !== 'PostToolBatch')
7
39
  return undefined;
8
40
  return presentPendingAtBoundary(input.session_id, (context) => ({ hookSpecificOutput: { hookEventName: 'PostToolBatch', additionalContext: context } }), lookup, env);
9
41
  }
10
- export function runClaudeHook(inputText, env = process.env) {
42
+ export async function runClaudeHook(inputText, env = process.env) {
11
43
  let input;
12
44
  try {
13
45
  input = JSON.parse(inputText);
@@ -17,6 +49,6 @@ export function runClaudeHook(inputText, env = process.env) {
17
49
  }
18
50
  if (input === null || typeof input !== 'object')
19
51
  return '';
20
- const response = claudeHookResponse(input, sessionInbox, env);
52
+ const response = await claudeHookResponse(input, sessionInbox, env);
21
53
  return response === undefined ? '' : `${JSON.stringify(response)}\n`;
22
54
  }