@astrosheep/square 0.3.58 → 0.3.62

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 (91) hide show
  1. package/claude-plugin/.claude-plugin/plugin.json +1 -1
  2. package/claude-plugin/skills/square/SKILL.md +5 -7
  3. package/codex-plugin/.codex-plugin/plugin.json +1 -1
  4. package/dist/activity.d.ts +6 -2
  5. package/dist/activity.js +170 -133
  6. package/dist/artifact.d.ts +4 -0
  7. package/dist/artifact.js +43 -14
  8. package/dist/attention-presentation.d.ts +1 -0
  9. package/dist/attention-presentation.js +21 -19
  10. package/dist/automatic-session.js +16 -26
  11. package/dist/boundary-presentation.d.ts +1 -2
  12. package/dist/boundary-presentation.js +4 -19
  13. package/dist/catch-decisions.js +1 -1
  14. package/dist/cli/context.d.ts +2 -0
  15. package/dist/cli/context.js +17 -33
  16. package/dist/cli/harness-command.js +1 -1
  17. package/dist/cli/mcp-server-command.d.ts +2 -0
  18. package/dist/cli/mcp-server-command.js +13 -0
  19. package/dist/cli/observation-commands.js +132 -64
  20. package/dist/cli/program.js +36 -9
  21. package/dist/cli/registry.js +2 -0
  22. package/dist/cli/square-commands.d.ts +1 -1
  23. package/dist/cli/square-commands.js +103 -133
  24. package/dist/decisions.d.ts +4 -0
  25. package/dist/decisions.js +82 -34
  26. package/dist/delivery-operations.d.ts +1 -2
  27. package/dist/delivery-operations.js +1 -4
  28. package/dist/file-changes.d.ts +12 -0
  29. package/dist/file-changes.js +235 -0
  30. package/dist/help.js +4 -8
  31. package/dist/host-ledger-file-adapter.d.ts +15 -13
  32. package/dist/host-ledger-file-adapter.js +131 -79
  33. package/dist/host-ledger-root.d.ts +2 -0
  34. package/dist/host-ledger-root.js +9 -0
  35. package/dist/host-ledger.d.ts +4 -24
  36. package/dist/inbox.d.ts +10 -5
  37. package/dist/inbox.js +152 -67
  38. package/dist/index.d.ts +3 -1
  39. package/dist/index.js +1 -0
  40. package/dist/list.js +12 -9
  41. package/dist/mcp.d.ts +7 -0
  42. package/dist/mcp.js +122 -0
  43. package/dist/mention-parse.d.ts +8 -0
  44. package/dist/mention-parse.js +107 -0
  45. package/dist/model.d.ts +5 -0
  46. package/dist/model.js +4 -1
  47. package/dist/notifications.d.ts +1 -0
  48. package/dist/notifications.js +25 -20
  49. package/dist/participant-host.js +2 -2
  50. package/dist/ports.d.ts +2 -9
  51. package/dist/presentation.d.ts +19 -1
  52. package/dist/presentation.js +111 -73
  53. package/dist/presented.js +2 -1
  54. package/dist/registry.d.ts +8 -4
  55. package/dist/registry.js +43 -76
  56. package/dist/routes.d.ts +19 -2
  57. package/dist/routes.js +48 -5
  58. package/dist/runtime.d.ts +10 -0
  59. package/dist/runtime.js +39 -0
  60. package/dist/square-actions.d.ts +26 -13
  61. package/dist/square-actions.js +203 -87
  62. package/dist/square-application.d.ts +58 -0
  63. package/dist/square-application.js +183 -0
  64. package/dist/square-facade.d.ts +24 -9
  65. package/dist/square-file-adapter.d.ts +5 -12
  66. package/dist/square-file-adapter.js +12 -64
  67. package/dist/square-projections.d.ts +3 -2
  68. package/dist/square-projections.js +12 -3
  69. package/dist/square-storage.js +27 -34
  70. package/dist/square-wiring.d.ts +12 -7
  71. package/dist/square-wiring.js +36 -32
  72. package/dist/state-cell.d.ts +1 -1
  73. package/dist/stream.js +31 -27
  74. package/dist/tty-style.d.ts +7 -0
  75. package/dist/tty-style.js +13 -0
  76. package/dist/views.d.ts +3 -3
  77. package/dist/views.js +10 -9
  78. package/dist/wake-attempts.js +2 -1
  79. package/dist/wake-evidence.js +2 -8
  80. package/dist/wakes.d.ts +1 -0
  81. package/dist/wakes.js +5 -5
  82. package/dist/watch.d.ts +6 -1
  83. package/dist/watch.js +43 -28
  84. package/extensions/square-pi.js +107 -109
  85. package/guides/application-boundary.md +32 -0
  86. package/guides/change-waits.md +28 -0
  87. package/guides/host-ledger.md +9 -0
  88. package/guides/mcp.md +9 -0
  89. package/guides/operation-cancellation.md +17 -0
  90. package/package.json +10 -4
  91. package/skills/square/SKILL.md +5 -7
@@ -1,4 +1,5 @@
1
1
  import { promises as fs } from 'node:fs';
2
+ import { hostLedgerRoot } from './host-ledger-root.js';
2
3
  import path from 'node:path';
3
4
  import { openSquare } from './square-file-adapter.js';
4
5
  import { closeOpenSquare } from './open-square.js';
@@ -9,7 +10,7 @@ import { SquareError } from './model.js';
9
10
  import { createHostLedgerPort } from './host-ledger-file-adapter.js';
10
11
  import { projectSessionBindings } from './square-projections.js';
11
12
  import { claimSessionParticipant, readParticipantOwner, releaseSessionParticipant } from './registry.js';
12
- import { publishWakeRoute, retireWakeRoutesForSessionFromArtifact, resolvePrimaryWakeRoute, defaultWakeRouteCapabilities } from './routes.js';
13
+ import { publishWakeRoute, resolvePrimaryWakeRoute, defaultWakeRouteCapabilities } from './routes.js';
13
14
  export { automaticParticipant } from './participant-identity.js';
14
15
  const providerEnv = {
15
16
  codex: 'CODEX_THREAD_ID',
@@ -29,10 +30,9 @@ function operationEnv(provider, sessionId, env) {
29
30
  };
30
31
  }
31
32
  function hostLedgerForEnv(env) {
32
- const root = env.SQUARE_REGISTRY === undefined ? undefined : path.dirname(env.SQUARE_REGISTRY);
33
+ const root = hostLedgerRoot(env);
33
34
  return createHostLedgerPort({
34
- userPath: env.SQUARE_HOST_LEDGER_USER ?? root,
35
- localPath: env.SQUARE_HOST_LEDGER_LOCAL ?? root,
35
+ rootPath: root,
36
36
  });
37
37
  }
38
38
  export function publicSquarePath(cwd) {
@@ -64,17 +64,17 @@ export async function automaticSessionStart(provider, sessionId, cwd, env = proc
64
64
  let bindings;
65
65
  let entry;
66
66
  try {
67
- bindings = await hostLedger.listPresence({ location: squarePath, participant: name, scopes: ['user', 'local'] });
67
+ bindings = await hostLedger.listPresence({ location: squarePath, participant: name });
68
68
  entry = await entryPresentation(reader, name);
69
69
  }
70
70
  finally {
71
71
  await closeOpenSquare(reader);
72
72
  }
73
73
  if (bindings.some((binding) => binding.session !== sessionId)) {
74
- throw new SquareError('already_joined', `${name} is already bound to another session`);
74
+ throw new SquareError('already_joined', `✕ ${name} already stands here — another session holds the name`);
75
75
  }
76
76
  if (entry.joined && !bindings.some((binding) => binding.session === sessionId)) {
77
- throw new SquareError('already_joined', `${name} is already joined by another session`);
77
+ throw new SquareError('already_joined', `✕ ${name} already stands here — another session holds the name`);
78
78
  }
79
79
  const scopedEnv = operationEnv(provider, sessionId, env);
80
80
  const claim = await claimSessionParticipant(squarePath, name, scopedEnv);
@@ -103,8 +103,7 @@ export async function automaticSessionStart(provider, sessionId, cwd, env = proc
103
103
  channel: provider === 'claude' ? 'claude-code' : provider,
104
104
  updatedAt: Date.now(),
105
105
  ...(claim?.epoch === undefined || claim.epoch <= 0 ? {} : { epoch: claim.epoch }),
106
- }, 'user');
107
- await square.reconcileBinding();
106
+ });
108
107
  return undefined;
109
108
  }
110
109
  catch (error) {
@@ -127,7 +126,6 @@ export async function automaticSessionEnd(provider, sessionId, cwd, env = proces
127
126
  hostLedger,
128
127
  sessionId,
129
128
  location: probe.location,
130
- scopes: ['user', 'local'],
131
129
  }))[0];
132
130
  const expectedEpoch = binding === undefined
133
131
  ? undefined
@@ -135,22 +133,14 @@ export async function automaticSessionEnd(provider, sessionId, cwd, env = proces
135
133
  await closeOpenSquare(probe);
136
134
  const square = await Square.at({ path: squarePath, hostLedger, env: scopedEnv });
137
135
  try {
138
- if (binding !== undefined) {
139
- const reader = await openSquare(squarePath);
140
- const joined = await entryPresentation(reader, binding.participant).finally(() => closeOpenSquare(reader));
141
- if (joined.joined) {
142
- const ended = await square.endOwnedSession(binding.participant, sessionId, expectedEpoch);
143
- if (ended !== null)
144
- await square.reconcileBinding();
145
- }
146
- }
147
- const cleanup = await openSquare(squarePath);
148
- try {
149
- await retireWakeRoutesForSessionFromArtifact(cleanup.artifact, { location: squarePath, sessionId });
150
- }
151
- finally {
152
- await closeOpenSquare(cleanup);
153
- }
136
+ // The ended owner's done, routes, and presence settle inside the ownership claim
137
+ // lock. A same-session rejoin or takeover that lands afterwards claims only after
138
+ // that cleanup, so this call never performs post-return ledger or route cleanup that
139
+ // could remove the replacement owner's fresh rows.
140
+ if (binding === undefined)
141
+ await square.retireEndedSessionRoutes(sessionId);
142
+ else
143
+ await square.endOwnedSession(binding.participant, sessionId, expectedEpoch);
154
144
  }
155
145
  finally {
156
146
  await square.close();
@@ -1,6 +1,5 @@
1
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[];
2
+ export { pendingAtBoundary } from './square-projections.js';
4
3
  export declare function renderPendingAtBoundary(inbox: InboxMembership[]): string;
5
4
  /** Serialize a boundary across hook processes so one stale inbox cannot render twice. */
6
5
  export declare function presentPendingAtBoundary<T>(sessionId: string, present: (context: string) => T | Promise<T>, lookup?: (sessionId: string, env?: NodeJS.ProcessEnv, signal?: AbortSignal) => Promise<InboxMembership[]> | InboxMembership[], env?: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<T | undefined>;
@@ -1,14 +1,14 @@
1
+ import { hostLedgerRoot } from './host-ledger-root.js';
1
2
  import { createHash } from 'node:crypto';
2
- import os from 'node:os';
3
3
  import path from 'node:path';
4
- import { leaseOwnsNotification } from './delivery.js';
5
4
  import { withFileLock } from './file-lock.js';
6
5
  import { openSquare } from './square-file-adapter.js';
7
6
  import { closeOpenSquare } from './open-square.js';
8
7
  import { presentPending } from './presentation-operations.js';
9
8
  import { hostLedgerForEnv, sessionInbox } from './inbox.js';
10
9
  import { attentionBodyIsClipped, renderAttentionPreview } from './attention-presentation.js';
11
- import { presentationSuppressesWake, projectPresentationEvidence } from './square-projections.js';
10
+ import { pendingAtBoundary, presentationSuppressesWake, projectPresentationEvidence } from './square-projections.js';
11
+ export { pendingAtBoundary } from './square-projections.js';
12
12
  import { formatActivityId } from './square-core.js';
13
13
  const CONTEXT_MAX = 1200;
14
14
  const presentationLocks = new Map();
@@ -38,20 +38,6 @@ async function awaitWithSignal(promise, signal) {
38
38
  function pendingCount(inbox) {
39
39
  return inbox.reduce((total, membership) => total + membership.notifications.length, 0);
40
40
  }
41
- /** A fresh blocking catch owns only the notifications admitted by its filter. */
42
- export function pendingAtBoundary(inbox) {
43
- return inbox
44
- .map((membership) => {
45
- const lease = membership.catchLease;
46
- if (lease === undefined)
47
- return membership;
48
- return {
49
- ...membership,
50
- notifications: membership.notifications.filter((notification) => !leaseOwnsNotification(lease, { ...notification, recipient: membership.name })),
51
- };
52
- })
53
- .filter((membership) => membership.notifications.length > 0);
54
- }
55
41
  export function renderPendingAtBoundary(inbox) {
56
42
  return renderBoundary(inbox).context;
57
43
  }
@@ -165,8 +151,7 @@ async function presentPendingAtBoundaryUnlocked(sessionId, present, lookup = ses
165
151
  }
166
152
  /** Serialize a boundary across hook processes so one stale inbox cannot render twice. */
167
153
  export async function presentPendingAtBoundary(sessionId, present, lookup = sessionInbox, env = process.env, signal) {
168
- const ledgerRoot = env.SQUARE_HOST_LEDGER_USER
169
- ?? (env.SQUARE_REGISTRY === undefined ? path.join(os.homedir(), '.square', 'host-ledger') : path.dirname(env.SQUARE_REGISTRY));
154
+ const ledgerRoot = hostLedgerRoot(env);
170
155
  const ownerKey = createHash('sha256').update(sessionId).digest('hex');
171
156
  return withFileLock(path.join(ledgerRoot, `presentation-boundary-${ownerKey}.lock`), { retryMs: 10, signal }, () => presentPendingAtBoundaryUnlocked(sessionId, present, lookup, env, signal));
172
157
  }
@@ -17,7 +17,7 @@ function resolveCatchName(state, requestedName) {
17
17
  validateName(requestedName);
18
18
  const known = resolveRosterName(state, requestedName);
19
19
  if (known === undefined) {
20
- throw new SquareError('invalid_args', `Unknown participant "${participantIdentity(requestedName)}".`);
20
+ throw new SquareError('invalid_args', `✕ ${participantIdentity(requestedName)} has never stepped into this square`);
21
21
  }
22
22
  return known;
23
23
  }
@@ -1,5 +1,7 @@
1
1
  import { type HardCap } from '../model.js';
2
2
  export interface CommandContext {
3
+ cwd: string;
4
+ env: NodeJS.ProcessEnv;
3
5
  squarePath?: string;
4
6
  name?: string;
5
7
  homeDir: string;
@@ -35,66 +35,66 @@ export function usage(command) {
35
35
  export function requireValue(args, index, flag) {
36
36
  const value = args[index + 1];
37
37
  if (value === undefined || value.startsWith('--'))
38
- fail(`Missing value for ${flag}.`);
38
+ fail(`✕ missing value for ${flag}`);
39
39
  return value;
40
40
  }
41
41
  export function parseDurationMs(value, flag) {
42
42
  const match = value.match(/^([1-9]\d*)(ms|s|m|h)$/);
43
43
  if (!match)
44
- fail(`Invalid ${flag}: expected a positive duration with unit ms, s, m, or h (for example 500ms, 30s, 3m, 1h).`);
44
+ fail(`✕ invalid ${flag}: expected a positive duration with unit ms, s, m, or h (for example 500ms, 30s, 3m, 1h)`);
45
45
  const amount = Number.parseInt(match[1], 10);
46
46
  const multiplier = { ms: 1, s: 1000, m: 60000, h: 3600000 };
47
47
  const milliseconds = amount * multiplier[match[2]];
48
48
  if (!Number.isSafeInteger(milliseconds))
49
- fail(`Invalid ${flag}: duration is too large.`);
49
+ fail(`✕ invalid ${flag}: duration is too large`);
50
50
  return milliseconds;
51
51
  }
52
52
  export function parsePositiveInteger(value, flag) {
53
53
  if (!/^[1-9]\d*$/.test(value))
54
- fail(`Invalid ${flag}: expected a positive integer.`);
54
+ fail(`✕ invalid ${flag}: expected a positive integer`);
55
55
  const parsed = Number(value);
56
56
  if (!Number.isSafeInteger(parsed))
57
- fail(`Invalid ${flag}: value is too large.`);
57
+ fail(`✕ invalid ${flag}: value is too large`);
58
58
  return parsed;
59
59
  }
60
60
  export function parseBoundedLimit(value, flag, max, retry) {
61
61
  if (value === undefined || value.startsWith('--') || !/^[1-9]\d*$/.test(value) || !Number.isSafeInteger(Number(value))) {
62
- fail(`✕ ${flag} needs a positive integer\n» ${retry}`);
62
+ fail(`✕ ${flag} needs a positive integer\n${retry}`);
63
63
  }
64
64
  const parsed = Number(value);
65
65
  if (parsed > max)
66
- fail(`✕ ${flag} is capped at ${max}\n» ${retry}`);
66
+ fail(`✕ ${flag} is capped at ${max}\n${retry}`);
67
67
  return parsed;
68
68
  }
69
69
  export function parseNonNegativeInteger(value, flag) {
70
70
  if (!/^\d+$/.test(value))
71
- fail(`Invalid ${flag}: expected a non-negative integer.`);
71
+ fail(`✕ invalid ${flag}: expected a non-negative integer`);
72
72
  const parsed = Number(value);
73
73
  if (!Number.isSafeInteger(parsed))
74
- fail(`Invalid ${flag}: value is too large.`);
74
+ fail(`✕ invalid ${flag}: value is too large`);
75
75
  return parsed;
76
76
  }
77
77
  export function parseHardCap(value) {
78
78
  if (value === 'unlimited')
79
79
  return null;
80
80
  if (!/^[1-9]\d*$/.test(value))
81
- fail('Invalid build option: --cap must be a positive integer or unlimited.');
81
+ fail('✕ invalid build option: --cap must be a positive integer or unlimited');
82
82
  const parsed = Number(value);
83
83
  if (!Number.isSafeInteger(parsed))
84
- fail('Invalid build option: --cap must be a positive integer or unlimited.');
84
+ fail('✕ invalid build option: --cap must be a positive integer or unlimited');
85
85
  return parsed;
86
86
  }
87
87
  export function parseNameList(value, flag) {
88
88
  const names = parseParticipantList(value);
89
89
  if (names.length === 0)
90
- fail(`Invalid ${flag}: expected at least one participant name.`);
90
+ fail(`✕ invalid ${flag}: expected at least one participant name`);
91
91
  for (const name of names)
92
92
  validateName(name);
93
93
  return names;
94
94
  }
95
95
  export function requireParticipant(name) {
96
96
  if (!name)
97
- fail('Missing required option: --as <name>.');
97
+ fail('✕ missing required option: --as <name>');
98
98
  validateName(name);
99
99
  return name;
100
100
  }
@@ -113,6 +113,7 @@ const LOCATION_REQUIRED_COMMANDS = new Set([
113
113
  'history',
114
114
  'status',
115
115
  'participants',
116
+ 'mcp-server',
116
117
  'doctor',
117
118
  ]);
118
119
  function configuredLocation() {
@@ -126,22 +127,10 @@ function configuredName() {
126
127
  validateName(value);
127
128
  return value;
128
129
  }
129
- function withoutAsCommand(rawArgs) {
130
- const args = [];
131
- for (let index = 0; index < rawArgs.length; index++) {
132
- if (rawArgs[index] === '--as') {
133
- index += 1;
134
- continue;
135
- }
136
- args.push(rawArgs[index]);
137
- }
138
- return `square ${args.map((arg) => /^[A-Za-z0-9_./:=@+,-]+$/.test(arg) ? arg : `'${arg.replace(/'/g, "'\\''")}'`).join(' ')}`;
139
- }
140
130
  export async function parseGlobalArgs(rawArgs) {
141
131
  const args = [...rawArgs];
142
132
  let requestedPath;
143
133
  let name;
144
- let hasExplicitName = false;
145
134
  for (let index = 0; index < args.length; index++) {
146
135
  if (args[index] === '--location') {
147
136
  requestedPath = requireValue(args, index, args[index]);
@@ -150,15 +139,11 @@ export async function parseGlobalArgs(rawArgs) {
150
139
  }
151
140
  else if (args[index] === '--as') {
152
141
  name = requireValue(args, index, args[index]);
153
- hasExplicitName = true;
154
142
  args.splice(index, 2);
155
143
  index -= 1;
156
144
  }
157
145
  }
158
146
  const command = args[0];
159
- if (command === 'history' && hasExplicitName) {
160
- fail(`✕ history is an archive and does not use --as\n» ${withoutAsCommand(rawArgs)}`);
161
- }
162
147
  if (name === undefined)
163
148
  name = configuredName();
164
149
  if (name !== undefined)
@@ -166,7 +151,7 @@ export async function parseGlobalArgs(rawArgs) {
166
151
  const explicitSquarePath = requestedPath !== undefined;
167
152
  const configured = configuredLocation();
168
153
  if (command !== undefined && !['--help', '-h'].includes(command) && locationIsRequired(command) && requestedPath === undefined && configured === undefined) {
169
- fail(`✕ ${command} needs a square location\n» square ls`);
154
+ fail(`✕ ${command} needs a square location\nsquare ls`);
170
155
  }
171
156
  const squarePath = requestedPath ?? configured;
172
157
  if (name === undefined && squarePath !== undefined && command !== undefined && locationIsRequired(command)) {
@@ -174,7 +159,6 @@ export async function parseGlobalArgs(rawArgs) {
174
159
  hostLedger: createHostLedgerPort(),
175
160
  location: squarePath,
176
161
  sessionId,
177
- scopes: ['user', 'local'],
178
162
  })))).flat();
179
163
  const names = new Set(bindings.map((binding) => binding.participant));
180
164
  name = names.size === 1 ? [...names][0] : undefined;
@@ -185,10 +169,10 @@ export function locationIsRequired(command) {
185
169
  return LOCATION_REQUIRED_COMMANDS.has(command);
186
170
  }
187
171
  export function defaultContext(command, squarePath, name) {
188
- return { command, squarePath, name, homeDir: os.homedir() };
172
+ return { command, cwd: process.cwd(), env: { ...process.env }, squarePath, name, homeDir: os.homedir() };
189
173
  }
190
174
  export function requireSquarePath(context) {
191
175
  if (context.squarePath === undefined)
192
- fail(`✕ ${context.command} needs a square location\n» square ls`);
176
+ fail(`✕ ${context.command} needs a square location\nsquare ls`);
193
177
  return context.squarePath;
194
178
  }
@@ -102,7 +102,7 @@ export function formatHarnessResult(result) {
102
102
  return `${[...result.notes, ...result.lines, ...result.failures].join('\n')}\n`;
103
103
  }
104
104
  export function runHarnessCommand(argv, squarePath) {
105
- const context = { homeDir: os.homedir(), ...(squarePath === undefined ? {} : { squarePath }), command: 'harness' };
105
+ const context = { cwd: process.cwd(), env: { ...process.env }, homeDir: os.homedir(), ...(squarePath === undefined ? {} : { squarePath }), command: 'harness' };
106
106
  const intent = harnessCommand.parse(argv, context);
107
107
  return Promise.resolve(harnessCommand.execute(intent, context)).then(formatHarnessResult);
108
108
  }
@@ -0,0 +1,2 @@
1
+ import { type CommandSpec } from './context.js';
2
+ export declare const mcpServerCommand: CommandSpec<undefined, void>;
@@ -0,0 +1,13 @@
1
+ import { fail } from './context.js';
2
+ export const mcpServerCommand = {
3
+ parse(argv) {
4
+ if (argv.length > 0)
5
+ fail('Usage: square [--location <square>] [--as <name>] mcp-server');
6
+ return undefined;
7
+ },
8
+ async execute(_intent, context) {
9
+ const { runSquareMcpStdio } = await import('../mcp.js');
10
+ await runSquareMcpStdio({ cwd: context.cwd, env: context.env, squarePath: context.squarePath, participant: context.name });
11
+ },
12
+ present() { },
13
+ };