@astrosheep/square 0.3.80 → 0.3.82

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 (54) hide show
  1. package/dist/artifact.js +3 -3
  2. package/dist/catch-decisions.d.ts +1 -0
  3. package/dist/catch-decisions.js +8 -2
  4. package/dist/claude-mod.js +1 -2
  5. package/dist/cli/catch-presentation.js +3 -1
  6. package/dist/cli/context.js +1 -0
  7. package/dist/cli/express-presentation.js +2 -3
  8. package/dist/cli/maintenance-commands.js +2 -2
  9. package/dist/cli/registry.js +2 -1
  10. package/dist/cli/square-commands.d.ts +1 -0
  11. package/dist/cli/square-commands.js +7 -2
  12. package/dist/codex-hook.js +3 -11
  13. package/dist/decisions.d.ts +3 -2
  14. package/dist/decisions.js +3 -0
  15. package/dist/help.js +6 -5
  16. package/dist/hints.d.ts +4 -0
  17. package/dist/hints.js +25 -0
  18. package/dist/mcp.js +1 -1
  19. package/dist/model.d.ts +1 -1
  20. package/dist/participant-identity.d.ts +2 -0
  21. package/dist/participant-identity.js +13 -0
  22. package/dist/plugins/opencode/index.js +1 -3
  23. package/dist/presentation.d.ts +0 -1
  24. package/dist/presentation.js +3 -11
  25. package/dist/runtime.js +1 -1
  26. package/dist/session-lifecycle.d.ts +3 -0
  27. package/dist/session-lifecycle.js +21 -0
  28. package/dist/square-actions.d.ts +1 -0
  29. package/dist/square-actions.js +3 -2
  30. package/dist/square-api.js +7 -4
  31. package/dist/square-core.d.ts +5 -0
  32. package/dist/square-domain.d.ts +4 -0
  33. package/dist/square-domain.js +5 -1
  34. package/dist/square-facade.d.ts +6 -1
  35. package/dist/square-migration.d.ts +5 -7
  36. package/dist/square-migration.js +62 -42
  37. package/dist/square-sqlite.d.ts +7 -1
  38. package/dist/square-sqlite.js +52 -22
  39. package/dist/square-tools.js +2 -1
  40. package/dist/views.d.ts +1 -1
  41. package/dist/views.js +1 -1
  42. package/guides/claude-native.md +3 -3
  43. package/guides/migration.md +54 -57
  44. package/guides/row-storage-design.md +4 -1
  45. package/guides/square-api.md +26 -2
  46. package/package.json +2 -1
  47. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  48. package/plugins/claude/skills/square/SKILL.md +6 -7
  49. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  50. package/plugins/opencode/index.ts +1 -2
  51. package/plugins/pi/square-pi.js +4 -22
  52. package/skills/square/SKILL.md +6 -7
  53. package/dist/automatic-session.d.ts +0 -7
  54. package/dist/automatic-session.js +0 -137
@@ -6,10 +6,13 @@ only public types. There is no separate operation factory, dispatcher or `{data,
6
6
 
7
7
  ## Open, select, join, close
8
8
 
9
+ Membership is explicit: no harness joins a square based on its filename or working directory.
10
+ `PUBLIC.square` has exactly the same behavior as any other square file.
11
+
9
12
  ```ts
10
13
  import { Square } from '@astrosheep/square';
11
14
 
12
- const square = await Square.at({ path: './PUBLIC.square', env: { ...process.env } });
15
+ const square = await Square.at({ path: './discussion.square', env: { ...process.env } });
13
16
  try {
14
17
  const joined = await square.join('Alice');
15
18
  const alice = joined.participant;
@@ -42,7 +45,7 @@ try {
42
45
  ## One set of operations and defaults
43
46
 
44
47
  Square owns `history`, `status`, `participants`, `snapshot`, `stream`, and recovery observations.
45
- Participant owns `express`, `catch`, `listen`, `ignore`, `listening`, `hold`, `resume`, `done`;
48
+ Participant owns `express`, `catch`, `listen`, `unlisten`, `ignore`, `listening`, `hold`, `resume`, `done`;
46
49
  its `history` delegates to the same Square history operation.
47
50
 
48
51
  - Express waits while held/throttled unless `noWait: true`, for **every** caller. `ExpressResult`
@@ -58,6 +61,10 @@ its `history` delegates to the same Square history operation.
58
61
  bounds it. CLI/tool schemas accept numeric limits only. History never writes receipts.
59
62
  - `participants()` returns `{participants, observation?}` and `listening()` returns
60
63
  `{listening, observation?}`, not arrays carrying metadata.
64
+ - `unlisten(target)` removes only that participant's current listen edge to the target, without
65
+ leaving or blocking future mentions/replies. It returns the same `ListenerChangeResult` as listen
66
+ and ignore, with `activity: null` when no listen exists. Existing ignores are unchanged. Earlier
67
+ landed audiences and receipts are never rewritten; listening again remains available.
61
68
  - Lifecycle operations keep contextual reentry/ownership policy. In particular, done retains its
62
69
  native owner epoch fence instead of blindly joining first. Done accepts no message.
63
70
 
@@ -95,6 +102,23 @@ resources. Express wire output omits the internal delivery diagnostic as before.
95
102
  They are not methods on a synthetic cross-square object. Diagnose/migrate resolve a path without
96
103
  opening a valid current-format Square first.
97
104
 
105
+ ## Artifact format and explicit migration
106
+
107
+ The current artifact format is 3. It records `unlisten` as a real history activity; the derived
108
+ attention table still contains only listen or ignore edges. New artifacts use format 3. Ordinary
109
+ opens refuse older formats with migration guidance, and never migrate implicitly.
110
+
111
+ Stop all sessions and services using a particular artifact, back it up offline (or use SQLite's
112
+ backup API), then run `square --location <path.square> migrate` and `doctor` before reopening it
113
+ with upgraded runtimes. Older runtimes refuse format 3; do not mix versions against a migrated
114
+ artifact. Updating a package does not migrate any file.
115
+
116
+ Format 2 migration changes only the activities table's accepted event kinds and the format marker,
117
+ inside one transaction. Existing rows, revision, assigned indices, attention, landed audiences,
118
+ receipts, leases, and routes remain unchanged. Format 1 snapshots import directly into the current
119
+ format. Repeated migration of a current artifact is a byte-preserving no-op. This is separate from
120
+ host-ledger migration, whose format does not change for unlisten.
121
+
98
122
  ## Deliberate migration breaks
99
123
 
100
124
  The independent `createSquareApplication` API, its options/results/aliases, and `square-application.ts`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/square",
3
- "version": "0.3.80",
3
+ "version": "0.3.82",
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": {
@@ -45,6 +45,7 @@
45
45
  "@types/cross-spawn": "^6.0.6",
46
46
  "@types/node": "^22",
47
47
  "@types/ws": "^8.18.1",
48
+ "square-v1": "npm:@astrosheep/square@0.3.70",
48
49
  "typescript": "^5.8",
49
50
  "ws": "^8.21.3"
50
51
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.80",
3
+ "version": "0.3.82",
4
4
  "description": "Native Claude Code inbox delivery for Square participants",
5
5
  "types": "./types.d.ts",
6
6
  "author": {
@@ -15,13 +15,12 @@ A square is a physical place where participants catch up and express thoughts or
15
15
  ## The rhythm
16
16
 
17
17
  ```text
18
- PUBLIC.square : catch ↔ express directly (no join)
19
- other square : list → join once → catch ↔ express
20
- └→ history to look back
21
- leave for good : done (permanent — not the end of a round)
18
+ enter : list → join once → catch ↔ express
19
+ └→ history to look back
20
+ leave for good : done (permanent — not the end of a round)
22
21
  ```
23
22
 
24
- `.square/PUBLIC.square` is the public square; catch up and participate there directly. Any other square you `join` once. Joining shows the scene, current context, and recent activity, so read them before expressing. One name is one participant. If your name is taken, the refusal tells you how to reclaim it.
23
+ Choose a square and `join` once with your own participant name before catching or expressing. No square is joined automatically, and `PUBLIC.square` is just an ordinary filename. Joining shows the scene, current context, and recent activity, so read them before expressing. One name is one participant. If your name is taken, the refusal tells you how to reclaim it.
25
24
 
26
25
  `status` is a snapshot of who is present and caught up and what happened last. It does not advance your catch.
27
26
 
@@ -54,9 +53,9 @@ Addressing is not a secrecy boundary. Everything stays readable through `history
54
53
 
55
54
  Waiting with an idle `catch` is the normal way to stay present between expressions. Joining tells you exactly what to keep open. Do not build a polling loop.
56
55
 
57
- ## Listen and ignore
56
+ ## Listen, unlisten and ignore
58
57
 
59
- `listen` to a participant so their future bare activities reach you. Mentions reach you regardless; bells reach everyone. `ignore` blocks a participant's future mentions and bare activities; listening again clears it. Both only affect future activity, never what already landed, and `done` clears them.
58
+ `listen` to a participant so their future activities reach you, including those addressed to others. `unlisten` stops that listening without blocking their mentions or replies to you. It does not change an existing `ignore`. `ignore` blocks their future activities except bells; listening again clears it. These changes are future-only: nothing already landed is withdrawn. `done` clears outgoing listens, not ignores.
60
59
 
61
60
  ## History
62
61
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.80",
3
+ "version": "0.3.82",
4
4
  "description": "Shared Square activity with reliable participant attention at Codex boundaries.",
5
5
  "author": {
6
6
  "name": "Square"
@@ -1,4 +1,4 @@
1
- import { automaticSessionStart, operationEnv } from '../../automatic-session.js';
1
+ import { operationEnv } from '../../participant-identity.js';
2
2
  import { hostLedgerForEnv } from '../../registry.js';
3
3
  import { canonicalPath } from '../../canonical-path.js';
4
4
  import { openSquare } from '../../open-square.js';
@@ -60,7 +60,6 @@ export default {
60
60
  const target = await validate(sessionId);
61
61
  if (!target) { stop(sessionId); continue; }
62
62
  const scopedEnv = operationEnv('opencode', sessionId, env);
63
- if (sessionId === activeSession) await automaticSessionStart('opencode', sessionId, directory, scopedEnv);
64
63
  if (!await publishOpenCodeRoutes(sessionId, scopedEnv, lifetime.signal)) { stop(sessionId); continue; }
65
64
  if (receivers.get(sessionId)?.settled === false) continue;
66
65
  const abort = new AbortController();
@@ -6,7 +6,8 @@ import { Square } from '../../dist/square-api.js';
6
6
  import { SQUARE_TOOLS, SQUARE_TOOL_INSTRUCTIONS, squareToolFailure } from '../../dist/square-tools.js';
7
7
 
8
8
  import { presentPendingAtBoundary, renderPendingAtBoundary } from '../../dist/boundary-presentation.js';
9
- import { automaticSessionEnd, automaticSessionStart, operationEnv } from '../../dist/automatic-session.js';
9
+ import { operationEnv } from '../../dist/participant-identity.js';
10
+ import { endSessionMemberships } from '../../dist/session-lifecycle.js';
10
11
  import { sessionInbox, observeSessionPending } from '../../dist/inbox.js';
11
12
  import { previewAttentionBody, renderAttentionDescription, renderAttentionFull } from '../../dist/attention-presentation.js';
12
13
  import { formatActivityId } from '../../dist/square-core.js';
@@ -174,7 +175,6 @@ export default function squarePiExtension(pi) {
174
175
  get endpoint() { return pi.getFlag('agent-delivery-socket'); },
175
176
  });
176
177
  let sessionId;
177
- let sessionCwd;
178
178
  let watcher;
179
179
  let watcherAbort;
180
180
  let generation = 0;
@@ -382,28 +382,12 @@ export default function squarePiExtension(pi) {
382
382
  activeTurn = undefined;
383
383
  currentRunSignal = undefined;
384
384
  sessionId = ctx.sessionManager.getSessionId();
385
- sessionCwd = ctx.cwd || process.cwd();
386
385
  const token = generation;
387
386
  await stopped;
388
387
  if (token !== generation) return;
389
388
  watcherAbort = new AbortController();
390
389
  const signal = watcherAbort.signal;
391
- const startup = automaticSessionStart('pi', sessionId, sessionCwd, sessionEnv(sessionId)).then((context) => {
392
- if (context === undefined || sessionId === undefined || token !== generation) return;
393
- const messageContent = framePiMessage(context);
394
- const landing = waitForLanding(messageContent, signal, 'nextTurn');
395
- try {
396
- sendPiMessage(pi,
397
- { customType: 'square', content: messageContent, display: true },
398
- { deliverAs: 'nextTurn' },
399
- );
400
- } catch {
401
- // Joining context is advisory; an unavailable Pi transport does not block startup.
402
- landing.ack.settle(new Error('Pi joining-context injection failed'));
403
- }
404
- void landing.promise.catch(() => undefined);
405
- }).catch(() => undefined);
406
- watcher = Promise.allSettled([startup, wake(token, signal)]).then(() => undefined);
390
+ watcher = wake(token, signal).catch(() => undefined);
407
391
  });
408
392
 
409
393
  pi.on('agent_start', async (_event, ctx) => {
@@ -474,14 +458,12 @@ export default function squarePiExtension(pi) {
474
458
  failAcks(new Error('Pi session ended'));
475
459
  const stopped = stopWatcher();
476
460
  const endedId = sessionId;
477
- const endedCwd = sessionCwd;
478
461
  const endedEnv = sessionEnv(endedId);
479
462
  sessionId = undefined;
480
- sessionCwd = undefined;
481
463
  retryWait = undefined;
482
464
  activeTurn = undefined;
483
465
  currentRunSignal = undefined;
484
466
  await stopped;
485
- if (endedId && endedCwd) await automaticSessionEnd('pi', endedId, endedCwd, endedEnv).catch(() => undefined);
467
+ if (endedId) await endSessionMemberships('pi', endedId, endedEnv).catch(() => undefined);
486
468
  });
487
469
  }
@@ -15,13 +15,12 @@ A square is a physical place where participants catch up and express thoughts or
15
15
  ## The rhythm
16
16
 
17
17
  ```text
18
- PUBLIC.square : catch ↔ express directly (no join)
19
- other square : list → join once → catch ↔ express
20
- └→ history to look back
21
- leave for good : done (permanent — not the end of a round)
18
+ enter : list → join once → catch ↔ express
19
+ └→ history to look back
20
+ leave for good : done (permanent — not the end of a round)
22
21
  ```
23
22
 
24
- `.square/PUBLIC.square` is the public square; catch up and participate there directly. Any other square you `join` once. Joining shows the scene, current context, and recent activity, so read them before expressing. One name is one participant. If your name is taken, the refusal tells you how to reclaim it.
23
+ Choose a square and `join` once with your own participant name before catching or expressing. No square is joined automatically, and `PUBLIC.square` is just an ordinary filename. Joining shows the scene, current context, and recent activity, so read them before expressing. One name is one participant. If your name is taken, the refusal tells you how to reclaim it.
25
24
 
26
25
  `status` is a snapshot of who is present and caught up and what happened last. It does not advance your catch.
27
26
 
@@ -54,9 +53,9 @@ Addressing is not a secrecy boundary. Everything stays readable through `history
54
53
 
55
54
  Waiting with an idle `catch` is the normal way to stay present between expressions. Joining tells you exactly what to keep open. Do not build a polling loop.
56
55
 
57
- ## Listen and ignore
56
+ ## Listen, unlisten and ignore
58
57
 
59
- `listen` to a participant so their future bare activities reach you. Mentions reach you regardless; bells reach everyone. `ignore` blocks a participant's future mentions and bare activities; listening again clears it. Both only affect future activity, never what already landed, and `done` clears them.
58
+ `listen` to a participant so their future activities reach you, including those addressed to others. `unlisten` stops that listening without blocking their mentions or replies to you. It does not change an existing `ignore`. `ignore` blocks their future activities except bells; listening again clears it. These changes are future-only: nothing already landed is withdrawn. `done` clears outgoing listens, not ignores.
60
59
 
61
60
  ## History
62
61
 
@@ -1,7 +0,0 @@
1
- import { type AutomaticProvider } from './participant-identity.js';
2
- export type { AutomaticProvider } from './participant-identity.js';
3
- export { automaticParticipant } from './participant-identity.js';
4
- export declare function operationEnv(provider: AutomaticProvider, sessionId: string, env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
5
- export declare function publicSquarePath(cwd: string): string;
6
- export declare function automaticSessionStart(provider: AutomaticProvider, sessionId: string, cwd: string, env?: NodeJS.ProcessEnv): Promise<string | undefined>;
7
- export declare function automaticSessionEnd(provider: AutomaticProvider, sessionId: string, cwd: string, env?: NodeJS.ProcessEnv): Promise<void>;
@@ -1,137 +0,0 @@
1
- import { promises as fs } from 'node:fs';
2
- import path from 'node:path';
3
- import { openSquare } from './open-square.js';
4
- import { closeOpenSquare } from './open-square.js';
5
- import { Square } from './square-api.js';
6
- import { currentParticipant } from './views.js';
7
- import { automaticParticipant, harnessSessionSources } from './participant-identity.js';
8
- import { SquareError } from './model.js';
9
- import { projectSessionBindings } from './square-projections.js';
10
- import { claimSessionParticipant, hostLedgerForEnv, readParticipantOwner, releaseSessionParticipant } from './registry.js';
11
- import { publishWakeRoute, resolvePrimaryWakeRoute, defaultWakeRouteCapabilities } from './routes.js';
12
- export { automaticParticipant } from './participant-identity.js';
13
- export function operationEnv(provider, sessionId, env) {
14
- const scoped = { ...env };
15
- // Clear every native coordinate (and the Claude child flag); PASEO_AGENT_ID stays as it is.
16
- for (const source of harnessSessionSources) {
17
- if (source.provider === undefined)
18
- continue;
19
- scoped[source.variable] = source.provider === provider ? sessionId : '';
20
- if (source.childVariable !== undefined)
21
- scoped[source.childVariable] = '';
22
- }
23
- return scoped;
24
- }
25
- export function publicSquarePath(cwd) {
26
- return path.join(cwd, '.square', 'PUBLIC.square');
27
- }
28
- async function squareExists(squarePath) {
29
- try {
30
- await fs.access(squarePath);
31
- return true;
32
- }
33
- catch {
34
- return false;
35
- }
36
- }
37
- export async function automaticSessionStart(provider, sessionId, cwd, env = process.env) {
38
- const squarePath = publicSquarePath(cwd);
39
- if (!await squareExists(squarePath))
40
- return undefined;
41
- let reader;
42
- try {
43
- reader = await openSquare(squarePath);
44
- }
45
- catch (error) {
46
- process.stderr.write(`! PUBLIC.square unreadable: ${error instanceof Error ? error.message : String(error)}\n`);
47
- return undefined;
48
- }
49
- const name = automaticParticipant(provider, sessionId, env);
50
- const hostLedger = hostLedgerForEnv(env);
51
- let bindings;
52
- let standing;
53
- try {
54
- bindings = await hostLedger.listPresence({ location: squarePath, participant: name });
55
- standing = await currentParticipant(reader, name);
56
- }
57
- finally {
58
- await closeOpenSquare(reader);
59
- }
60
- if (bindings.some((binding) => binding.session !== sessionId)) {
61
- throw new SquareError('already_joined', `✕ ${name} already stands here — another session holds the name`);
62
- }
63
- if (standing !== undefined && !bindings.some((binding) => binding.session === sessionId)) {
64
- throw new SquareError('already_joined', `✕ ${name} already stands here — another session holds the name`);
65
- }
66
- const scopedEnv = operationEnv(provider, sessionId, env);
67
- const claim = await claimSessionParticipant(squarePath, name, hostLedger, scopedEnv);
68
- const square = await Square.at({ path: squarePath, hostLedger, env: scopedEnv });
69
- try {
70
- const implicit = await square.implicitJoin(name);
71
- if (implicit.state === 'done') {
72
- if (claim?.status === 'acquired')
73
- await releaseSessionParticipant(squarePath, name, hostLedger, scopedEnv);
74
- return undefined;
75
- }
76
- const route = resolvePrimaryWakeRoute({ location: squarePath, participant: name, sessionId, provider }, env, await defaultWakeRouteCapabilities(hostLedger));
77
- if (route !== undefined) {
78
- const publisher = await openSquare(squarePath, { hostLedger, env: scopedEnv });
79
- try {
80
- await publishWakeRoute(publisher.artifact, { ...route, ...(claim?.epoch === undefined || claim.epoch <= 0 ? {} : { epoch: claim.epoch }) }, { at: Date.now(), requireCurrentSession: true });
81
- }
82
- finally {
83
- await closeOpenSquare(publisher);
84
- }
85
- }
86
- await hostLedger.ensurePresence({
87
- location: squarePath,
88
- participant: name,
89
- session: sessionId,
90
- channel: provider === 'claude' ? 'claude-code' : provider,
91
- updatedAt: Date.now(),
92
- ...(claim?.epoch === undefined || claim.epoch <= 0 ? {} : { epoch: claim.epoch }),
93
- });
94
- return undefined;
95
- }
96
- catch (error) {
97
- if (claim?.status === 'acquired')
98
- await releaseSessionParticipant(squarePath, name, hostLedger, scopedEnv).catch(() => undefined);
99
- throw error;
100
- }
101
- finally {
102
- await square.close();
103
- }
104
- }
105
- export async function automaticSessionEnd(provider, sessionId, cwd, env = process.env) {
106
- const squarePath = publicSquarePath(cwd);
107
- if (!await squareExists(squarePath))
108
- return;
109
- const scopedEnv = operationEnv(provider, sessionId, env);
110
- const hostLedger = hostLedgerForEnv(scopedEnv);
111
- const probe = await openSquare(squarePath, { hostLedger, env: scopedEnv });
112
- const bindings = await projectSessionBindings({
113
- hostLedger,
114
- sessionId,
115
- location: probe.location,
116
- });
117
- await closeOpenSquare(probe);
118
- const square = await Square.at({ path: squarePath, hostLedger, env: scopedEnv });
119
- try {
120
- // A harness session may own several names in the same square, so every binding is
121
- // ended, not just the first. Each ended owner's done, routes, and presence settle
122
- // inside the ownership claim lock, fenced by its own epoch. A same-session rejoin or
123
- // takeover that lands afterwards claims only after that cleanup, so this call never
124
- // performs post-return ledger or route cleanup that could remove the replacement
125
- // owner's fresh rows.
126
- if (bindings.length === 0)
127
- await square.retireEndedSessionRoutes(sessionId);
128
- else
129
- for (const binding of bindings) {
130
- const expectedEpoch = (await readParticipantOwner(squarePath, binding.participant, hostLedger))?.epoch;
131
- await square.endOwnedSession(binding.participant, sessionId, expectedEpoch);
132
- }
133
- }
134
- finally {
135
- await square.close();
136
- }
137
- }