@astrosheep/square 0.3.75 → 0.3.77

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.
@@ -0,0 +1,15 @@
1
+ import { z } from 'zod/v4';
2
+ declare const coordinate: z.ZodObject<{
3
+ toolUseId: z.ZodString;
4
+ sessionId: z.ZodString;
5
+ cwd: z.ZodString;
6
+ agentId: z.ZodOptional<z.ZodString>;
7
+ }, z.core.$strip>;
8
+ type ClaudeMcpCall = z.infer<typeof coordinate>;
9
+ export declare function beginClaudeMcpCall(input: ClaudeMcpCall, env?: NodeJS.ProcessEnv): Promise<{
10
+ token: string;
11
+ }>;
12
+ export declare function endClaudeMcpCall(toolUseId: string, token: string, env?: NodeJS.ProcessEnv): Promise<void>;
13
+ /** Expiry gates lookup only; an executing tool keeps its captured coordinate. */
14
+ export declare function readClaudeMcpCall(toolUseId: unknown, env?: NodeJS.ProcessEnv): Promise<ClaudeMcpCall>;
15
+ export {};
@@ -0,0 +1,82 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { promises as fs } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { z } from 'zod/v4';
5
+ import { withFileLock } from './file-lock.js';
6
+ import { hostLedgerRoot } from './host-ledger-root.js';
7
+ import { SquareError } from './model.js';
8
+ const LEASE_MS = 10 * 60 * 1000;
9
+ const text = z.string().max(4096).refine(value => value.trim().length > 0 && !/[\x00-\x1f\x7f]/u.test(value));
10
+ const coordinate = z.object({
11
+ toolUseId: text, sessionId: text, cwd: text.refine(path.isAbsolute), agentId: text.optional(),
12
+ });
13
+ const record = coordinate.extend({ token: z.string().regex(/^[a-f0-9]{64}$/u), expiresAt: z.number() });
14
+ const unavailable = () => new SquareError('unavailable', 'Claude MCP call context unavailable');
15
+ const storeRoot = (env) => path.resolve(hostLedgerRoot(env), 'claude-mcp-calls');
16
+ const callFile = (root, id) => path.join(root, `${createHash('sha256').update(id).digest('hex')}.json`);
17
+ async function readRecord(file) {
18
+ try {
19
+ return record.parse(JSON.parse(await fs.readFile(file, 'utf8')));
20
+ }
21
+ catch (error) {
22
+ if (error.code === 'ENOENT')
23
+ return undefined;
24
+ throw unavailable();
25
+ }
26
+ }
27
+ // Only publishers/cleanup mutate this private transport directory. Readers see an
28
+ // immutable record via atomic rename, so they need neither a lock nor housekeeping.
29
+ async function mutate(env, fn) {
30
+ const root = storeRoot(env);
31
+ await fs.mkdir(root, { recursive: true, mode: 0o700 });
32
+ await fs.chmod(root, 0o700);
33
+ return withFileLock(path.join(root, 'store.lock'), { retryMs: 10, signal: AbortSignal.timeout(2000) }, () => fn(root));
34
+ }
35
+ export async function beginClaudeMcpCall(input, env = process.env) {
36
+ const parsed = coordinate.safeParse(input);
37
+ if (!parsed.success)
38
+ throw unavailable();
39
+ return mutate(env, async (root) => {
40
+ const now = Date.now();
41
+ for (const name of await fs.readdir(root)) {
42
+ if (!/^[a-f0-9]{64}\.json$/u.test(name))
43
+ continue;
44
+ const file = path.join(root, name);
45
+ const old = await readRecord(file).catch(() => undefined);
46
+ if (old && old.expiresAt <= now)
47
+ await fs.unlink(file);
48
+ }
49
+ const file = callFile(root, parsed.data.toolUseId);
50
+ if (await readRecord(file))
51
+ throw unavailable();
52
+ const token = randomBytes(32).toString('hex');
53
+ const temporary = path.join(root, `${token}.tmp`);
54
+ try {
55
+ await fs.writeFile(temporary, JSON.stringify({ ...parsed.data, token, expiresAt: now + LEASE_MS }), { flag: 'wx', mode: 0o600 });
56
+ await fs.rename(temporary, file);
57
+ }
58
+ finally {
59
+ await fs.rm(temporary, { force: true });
60
+ }
61
+ return { token };
62
+ });
63
+ }
64
+ export async function endClaudeMcpCall(toolUseId, token, env = process.env) {
65
+ if (!text.safeParse(toolUseId).success || !text.safeParse(token).success)
66
+ throw unavailable();
67
+ await mutate(env, async (root) => {
68
+ const file = callFile(root, toolUseId);
69
+ const stored = await readRecord(file);
70
+ if (stored?.toolUseId === toolUseId && stored.token === token)
71
+ await fs.unlink(file);
72
+ });
73
+ }
74
+ /** Expiry gates lookup only; an executing tool keeps its captured coordinate. */
75
+ export async function readClaudeMcpCall(toolUseId, env = process.env) {
76
+ if (!text.safeParse(toolUseId).success)
77
+ throw unavailable();
78
+ const stored = await readRecord(callFile(storeRoot(env), toolUseId));
79
+ if (!stored || stored.toolUseId !== toolUseId || stored.expiresAt <= Date.now())
80
+ throw unavailable();
81
+ return coordinate.parse(stored);
82
+ }
@@ -7,6 +7,7 @@ import { Square } from './square-application.js';
7
7
  import { observeClaudeDelivery } from './claude-delivery.js';
8
8
  import { processActNotificationsOnce } from './notifications.js';
9
9
  import { sweepPending } from './delivery-operations.js';
10
+ import { beginClaudeMcpCall, endClaudeMcpCall } from './claude-mcp-context.js';
10
11
  /** JSON stdin bridge. Identity is explicit per operation; process.env is never rewritten. */
11
12
  export async function runClaudeMod(inputText, env = process.env) {
12
13
  if (inputText.length > 512_000)
@@ -14,6 +15,13 @@ export async function runClaudeMod(inputText, env = process.env) {
14
15
  const input = JSON.parse(inputText);
15
16
  if (!input || typeof input.sessionId !== 'string' || !input.sessionId || typeof input.cwd !== 'string')
16
17
  throw new Error('Claude bridge coordinate missing.');
18
+ if (input.operation === 'tool-begin') {
19
+ return `${JSON.stringify(await beginClaudeMcpCall({ toolUseId: input.toolUseId, sessionId: input.sessionId, cwd: input.cwd, agentId: input.agentId }, env))}\n`;
20
+ }
21
+ if (input.operation === 'tool-end') {
22
+ await endClaudeMcpCall(input.toolUseId, input.token, env);
23
+ return '{}\n';
24
+ }
17
25
  const scoped = operationEnv('claude', input.sessionId, env);
18
26
  const ledger = hostLedgerForEnv(env);
19
27
  const signal = AbortSignal.timeout(2500);
@@ -100,7 +100,6 @@ const LOCATION_REQUIRED_COMMANDS = new Set([
100
100
  'history',
101
101
  'status',
102
102
  'participants',
103
- 'mcp-server',
104
103
  'doctor',
105
104
  'migrate',
106
105
  ]);
package/dist/mcp.js CHANGED
@@ -1,80 +1,51 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
- import { z } from 'zod/v4';
4
- import { SquareError, isSquareError } from './model.js';
3
+ import { SQUARE_TOOLS, SQUARE_TOOL_INSTRUCTIONS, squareToolFailure } from './square-tools.js';
5
4
  import { operationEnv } from './automatic-session.js';
5
+ import { readClaudeMcpCall } from './claude-mcp-context.js';
6
6
  import { SQUARE_IDENTITY } from './identity.js';
7
7
  import { createSquareApplication, createSquareResourceOwner } from './square-application.js';
8
- const activityId = z.string().regex(/^act\/(0|[1-9][0-9]*)$/);
9
- const stringArray = z.array(z.string());
10
- const participantName = z.string().optional().describe("Participant name, same as --as. Defaults to the server's configured participant.");
11
- const TOOL_DEFINITIONS = [
12
- { name: 'join', description: 'Join the square as a participant.', inputSchema: z.object({ as: participantName, takeover: z.boolean().optional() }).strict() },
13
- { name: 'express', description: 'Express a message or embodied action in the square.', inputSchema: z.object({ as: participantName, body: z.string(), mentions: stringArray.optional(), reach: z.enum(['bell']).optional(), reply: activityId.optional(), force: z.boolean().optional(), noWait: z.boolean().optional() }).strict() },
14
- { name: 'catch', description: 'Catch and consume activities perceptible to the participant.', inputSchema: z.object({ as: participantName, id: activityId.optional(), idle: z.number().finite().min(0).optional(), from: stringArray.optional(), mention: z.boolean().optional(), limit: z.number().int().min(1).optional() }).strict() },
15
- { name: 'history', description: 'Read the activity stream using stable activity-id cursors.', inputSchema: z.object({ limit: z.number().int().min(1).optional(), order: z.enum(['asc', 'desc']).optional(), before: activityId.optional(), after: activityId.optional(), grep: z.string().optional(), fixed: z.string().optional(), from: stringArray.optional(), mention: z.string().optional() }).strict() },
16
- { name: 'listen', description: 'Listen for future activity from a participant.', inputSchema: z.object({ as: participantName, target: z.string() }).strict() },
17
- { name: 'ignore', description: 'Stop listening for future activity from a participant.', inputSchema: z.object({ as: participantName, target: z.string() }).strict() },
18
- { name: 'listening', description: 'List participants this participant is listening to.', inputSchema: z.object({ as: participantName }).strict() },
19
- { name: 'hold', description: 'Raise a hand to hold the square.', inputSchema: z.object({ as: participantName, reason: z.string().optional() }).strict() },
20
- { name: 'resume', description: 'Release the current hold.', inputSchema: z.object({ as: participantName }).strict() },
21
- { name: 'done', description: 'Leave the square without adding a message.', inputSchema: z.object({ as: participantName }).strict() },
22
- { name: 'status', description: 'Read the public status of the square.', inputSchema: z.object({}).strict() },
23
- { name: 'participants', description: 'List participants standing in or done with the square.', inputSchema: z.object({}).strict() },
24
- ];
25
- function operation(app, name, args, control) {
26
- switch (name) {
27
- case 'join': return app.join({ takeover: args.takeover }, control);
28
- case 'express': {
29
- const { body, noWait, ...options } = args;
30
- return app.express(body, { ...options, noWait }, control);
31
- }
32
- case 'catch': return app.catch(args, control);
33
- case 'history': return app.history(args, control);
34
- case 'listen': return app.listen(args.target, control);
35
- case 'ignore': return app.ignore(args.target, control);
36
- case 'listening': return app.listening(control);
37
- case 'hold': return app.hold(args.reason, control);
38
- case 'resume': return app.resume(control);
39
- case 'done': return app.done(undefined, control);
40
- case 'status': return app.status(undefined, control);
41
- case 'participants': return app.participants(control);
42
- default: throw new SquareError('invalid_args', `Unknown tool: ${name}`);
43
- }
44
- }
45
8
  export function createSquareMcpServer(options) {
46
9
  const baseEnv = { ...options.env };
47
10
  const resources = createSquareResourceOwner();
48
- // One explicit lifetime for this server: every tool call borrows the same connection.
11
+ // Calls to the configured square borrow one server-owned connection.
49
12
  const inFlight = new Set();
50
13
  const server = new McpServer({ name: 'square', version: SQUARE_IDENTITY.packageVersion }, {
51
- instructions: 'The catch tool consumes activities. Use history with act/<index> cursors for read-only archive access.',
14
+ instructions: SQUARE_TOOL_INSTRUCTIONS,
52
15
  });
53
- for (const tool of TOOL_DEFINITIONS) {
16
+ for (const tool of SQUARE_TOOLS) {
54
17
  server.registerTool(tool.name, { description: tool.description, inputSchema: tool.inputSchema }, (args, extra) => {
55
18
  const call = (async () => {
56
19
  try {
57
- const { as: participant, ...operationArgs } = args;
20
+ const { as: participant, location } = args;
58
21
  const threadId = extra._meta?.threadId;
59
22
  const opencodeSessionId = extra._meta?.['ai.opencode/sessionID'];
60
- const env = typeof threadId === 'string' && threadId.trim()
61
- ? operationEnv('codex', threadId, baseEnv)
62
- : typeof opencodeSessionId === 'string' && opencodeSessionId.trim()
63
- ? operationEnv('opencode', opencodeSessionId, baseEnv)
64
- : baseEnv;
65
- const app = createSquareApplication({ ...options, env, participant: participant ?? options.participant, resources }, { presentation: false });
66
- const outcome = await operation(app, tool.name, operationArgs, { signal: extra.signal });
23
+ // Claude's plugin endpoint requires the mod's call-local coordinate, never startup identity.
24
+ const claude = baseEnv.SQUARE_CLAUDE_MCP_CONTEXT === '1'
25
+ ? await readClaudeMcpCall(extra._meta?.['claudecode/toolUseId'], baseEnv)
26
+ : undefined;
27
+ const env = claude !== undefined
28
+ ? { ...operationEnv('claude', claude.sessionId, baseEnv),
29
+ // Preserve Claude's session + child flag semantics; an agent loop ID is not a session ID.
30
+ CLAUDE_CODE_CHILD_SESSION: claude.agentId === undefined ? '' : '1' }
31
+ : typeof threadId === 'string' && threadId.trim()
32
+ ? operationEnv('codex', threadId, baseEnv)
33
+ : typeof opencodeSessionId === 'string' && opencodeSessionId.trim()
34
+ ? operationEnv('opencode', opencodeSessionId, baseEnv)
35
+ : baseEnv;
36
+ const app = createSquareApplication({
37
+ ...options, env, cwd: claude?.cwd ?? options.cwd,
38
+ squarePath: location ?? options.squarePath,
39
+ participant: participant ?? options.participant,
40
+ // The server owner is bound to its configured square; overrides close after each call.
41
+ resources: location === undefined && claude === undefined ? resources : undefined,
42
+ }, { presentation: false });
43
+ const outcome = await tool.run(app, args, { signal: extra.signal });
67
44
  const structuredContent = outcome.data;
68
45
  return { content: [{ type: 'text', text: JSON.stringify(structuredContent) }], structuredContent };
69
46
  }
70
47
  catch (error) {
71
- const normalized = extra.signal.aborted
72
- ? { code: 'cancelled', message: 'The MCP request was cancelled' }
73
- : isSquareError(error)
74
- ? { code: error.code, message: error.message, ...(error.facts === undefined ? {} : { facts: error.facts }) }
75
- : { code: 'internal', message: error instanceof Error ? error.message : String(error) };
76
- const operation = error instanceof Error ? error.operation : undefined;
77
- const failure = operation === undefined ? normalized : { ...normalized, operation };
48
+ const failure = squareToolFailure(error, extra.signal);
78
49
  return { isError: true, content: [{ type: 'text', text: JSON.stringify(failure) }], structuredContent: { error: failure } };
79
50
  }
80
51
  })();
@@ -0,0 +1,14 @@
1
+ import { z } from 'zod/v4';
2
+ import type { ApplicationOutcome, OperationControl, SquareApplication } from './square-facade.js';
3
+ type JsonObject = Record<string, unknown>;
4
+ type Outcome = Promise<ApplicationOutcome<object, unknown, false>>;
5
+ interface ToolDefinition {
6
+ name: string;
7
+ description: string;
8
+ inputSchema: z.ZodObject<z.ZodRawShape>;
9
+ run(app: SquareApplication<false>, args: JsonObject, control: OperationControl): Outcome;
10
+ }
11
+ export declare const SQUARE_TOOL_INSTRUCTIONS = "The catch tool consumes activities. Use history with act/<index> cursors for read-only archive access.";
12
+ export declare const SQUARE_TOOLS: readonly ToolDefinition[];
13
+ export declare function squareToolFailure(error: unknown, signal?: AbortSignal): JsonObject;
14
+ export {};
@@ -0,0 +1,50 @@
1
+ import { z } from 'zod/v4';
2
+ import { isSquareError } from './model.js';
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.');
5
+ const activityId = z.string().regex(/^act\/(0|[1-9][0-9]*)$/);
6
+ const stringArray = z.array(z.string());
7
+ function tool(name, description, shape, run) {
8
+ const inputSchema = z.object({ ...shape, location }).strict();
9
+ return { name, description, inputSchema, run: (app, args, control) => {
10
+ const { location: _location, ...operationArgs } = inputSchema.parse(args);
11
+ return run(app, operationArgs, control);
12
+ } };
13
+ }
14
+ export const SQUARE_TOOL_INSTRUCTIONS = 'The catch tool consumes activities. Use history with act/<index> cursors for read-only archive access.';
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.', {
18
+ as: participant, body: z.string(), mentions: stringArray.optional(), reach: z.enum(['bell']).optional(),
19
+ reply: activityId.optional(), noMention: z.boolean().optional().describe('Disable body @name scanning; explicit mentions and reply still apply.'),
20
+ force: z.boolean().optional().describe('Speak without first catching unread activity or satisfying attention etiquette.'),
21
+ 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)),
23
+ 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
+ as: participant, id: activityId.optional(), idle: z.number().finite().min(0).optional(), from: stringArray.optional(),
25
+ mention: z.boolean().optional(), limit: z.number().int().min(1).optional(),
26
+ }, (app, { as: _participant, ...options }, control) => app.catch(options, control)),
27
+ 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
+ limit: z.number().int().min(1).optional(), order: z.enum(['asc', 'desc']).optional(), before: activityId.optional(), after: activityId.optional(),
29
+ 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)),
39
+ ];
40
+ export function squareToolFailure(error, signal) {
41
+ const normalized = signal?.aborted
42
+ ? { code: 'cancelled', message: 'The Square operation was cancelled' }
43
+ : isSquareError(error)
44
+ ? { code: error.code, message: error.message, ...(error.facts === undefined ? {} : { facts: error.facts }) }
45
+ : error instanceof z.ZodError
46
+ ? { code: 'invalid_args', message: error.message }
47
+ : { code: 'internal', message: error instanceof Error ? error.message : String(error) };
48
+ const operation = error instanceof Error ? error.operation : undefined;
49
+ return operation === undefined ? normalized : { ...normalized, operation };
50
+ }
@@ -81,7 +81,10 @@ Array-bearing results use object envelopes (`{activities}`, `{participants}`, `{
81
81
  - **Adapter boundary.** CLI/MCP reach storage, actions, decisions, views, wakes, list, stream, diagnosis/migration, inbox and notification execution only through `square-application.ts`. Private concern helpers retain their existing typed ports and internal dependencies; they are not callable from adapter execution paths.
82
82
  - **Facade.** `square-facade.ts` defines contracts; it must not derive contracts from `ReturnType`/`Awaited<ReturnType>` of private functions, and public activity IDs are formatted/parsed only by the existing core implementation.
83
83
  - **CLI (`cli/**`).** Owns argv/stdin, scoped help, shell quoting, sensory rendering/recovery, drafts, stdout/stderr, exit codes, SIGINT -> `AbortSignal` and stream backpressure. It maps returned classifications to copy/exit status, and preserves query argv in continuation commands; it cannot derive admission, commit certainty, terminal states or pagination eligibility from display text. File path display and draft-file I/O are allowed; opening an artifact or resolving ownership is not path formatting.
84
- - **MCP (`mcp.ts`).** Owns Zod schemas, registration, request-local `as`/`_meta.threadId`/session mapping, signal forwarding, JSON-RPC envelopes and stdio lifecycle. Request environment derivation stays immutable; no shared `process.env` writes and no invented session identity. Its method-name dispatch is a thin argument-to-method mapping, not a second policy dispatcher.
84
+ - **Tools (`square-tools.ts`).** Owns shared Zod input schemas, descriptions and typed argument-to-facade calls for MCP and Pi. No session, environment, connection lifetime or business policy lives here.
85
+ - **MCP (`mcp.ts`).** Owns registration, request-local `location`/`as`/metadata session mapping, signal forwarding, JSON-RPC envelopes and stdio lifecycle. Request environment derivation stays immutable; no shared `process.env` writes and no invented session identity.
86
+ - **Claude mod (`plugins/claude/hooks/register.js`).** Owns the pending MCP call's identity association, publishing before `next(e)` and removing its exact token in `finally` through the existing native helper. `claude-mcp-context.ts` stores only transient transport coordinates, outside Square domain/artifact state. The bundled MCP adapter reads them using Claude's tool-use metadata; model-visible schemas contain no identity fields.
87
+ - **Pi (`plugins/pi/square-pi.js`).** Registers the same operations as native codemode tools. Each call derives its identity and cwd from the live Pi context and uses the facade's per-operation connection lifetime. JSON schemas come from the shared Zod definitions; Pi has no separate parameter contract.
85
88
  - **Closed exceptions.** Harness install/uninstall/doctor, scoped help/version, meta commands and native hook input/output keep their existing protocol owners; that exception does not cover join, expression, catch, history, status, participants, build, stream, discovery, inbox, artifact doctor or migrate. Native changes stay limited to the required owner import migration.
86
89
  - **Storage/native scope.** SQLite schema/framing, artifact version, migration protocol, receipt model, ownership CAS, notification cancellation suppression and delivery eligibility rules are unchanged. Transactions stay synchronous and typed. The journaled amendment permits only build/create signal propagation through the existing adapter/storage/artifact retries and transactions, and captured-env propagation through default Codex wake construction/executable selection. A committed build is returned despite later cancellation; temporary-file cleanup does not reuse the aborted signal.
87
90
 
@@ -19,6 +19,35 @@ peer/plugin authority. Native OS-user socket permissions and Claude policy apply
19
19
  `square harness doctor delivery` derives current routes/evidence without sending.
20
20
  An absent/broken endpoint is unavailable; it does not consume Square activity.
21
21
 
22
+ ## MCP tools and call identity
23
+
24
+ The plugin's `.mcp.json` starts the existing Square MCP server. The mod no longer
25
+ registers an argv wrapper or executes participant CLI commands. MCP and Pi native
26
+ tools share their operation definitions and facade calls in `square-tools.ts`.
27
+
28
+ Before a bundled MCP call, the mod uses the existing `square claude-mod` helper
29
+ to publish `{toolUseId, sessionId, cwd, agentId?}`. It then calls `next(e)` unchanged.
30
+ The server correlates `_meta["claudecode/toolUseId"]` with that association: no
31
+ session, agent, token or other transport field is added to the model's schema or
32
+ arguments. Claude's native **session ID plus child flag** semantics are preserved;
33
+ the child loop's `agentId` is retained for attribution, not substituted as a session ID.
34
+ Calls use the live cwd and session even if the MCP process started earlier.
35
+
36
+ `SQUARE_CLAUDE_MCP_CONTEXT=1` is set only by the bundled MCP configuration. That
37
+ endpoint fails closed if an association is missing or expired; it never silently
38
+ uses the startup parent identity. Generic MCP configurations retain their existing
39
+ identity handling. The hook targets `mcp__plugin_square_square__*`, not unrelated
40
+ servers with similar operation names.
41
+
42
+ The mod owns publication and token-matched removal in `finally`; the server only
43
+ reads. Associations are private, transient transport files under
44
+ `<host-ledger-root>/claude-mcp-calls`, not Square domain state or another artifact.
45
+ Normal completion/errors remove them. If abrupt cancellation, unload or a crash
46
+ prevents cleanup, they expire after ten minutes and the next publication prunes
47
+ them. Expiry gates lookup, not an operation that already captured its context.
48
+ MCP owns operation cancellation and connection shutdown. No second execution
49
+ protocol, resident service or ledger schema migration is introduced.
50
+
22
51
  ## Membership and lifecycle
23
52
 
24
53
  If `.square/PUBLIC.square` already exists, the mod joins the automatic participant.
@@ -1,6 +1,6 @@
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. Only the 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 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.
4
4
 
5
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.
6
6
 
package/guides/mcp.md CHANGED
@@ -2,8 +2,12 @@
2
2
 
3
3
  Square provides a stdio MCP server through `square mcp-server`.
4
4
 
5
- Set `SQUARE_LOCATION` to the `.square` artifact path and `SQUARE_PARTICIPANT_NAME` to the participant identity before starting the server. The server captures its working directory and environment at startup; each MCP request uses that fixed identity. Run one server process for each independent participant identity.
5
+ Set `SQUARE_LOCATION` to the default `.square` artifact path and optionally `SQUARE_PARTICIPANT_NAME` to the default participant. Calls may override these with `location` and `as`; relative paths use the server's startup working directory. Request metadata supplies the live Codex (`threadId`) or OpenCode (`ai.opencode/sessionID`) identity when present, otherwise the startup environment is used. Participant names are not session identities.
6
6
 
7
7
  The server exposes `join`, `express`, `catch`, `history`, `listen`, `ignore`, `listening`, `hold`, `resume`, `done`, `status`, and `participants`. `catch` consumes activities. `history` is read-only and accepts stable `act/<index>` cursors. Tool results include JSON `structuredContent` and a text representation of the same data. Square failures are returned as tool errors with a structured `{ error: { code, message, facts? } }` value.
8
8
 
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
+
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.
12
+
9
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.
@@ -0,0 +1,21 @@
1
+ # Pi native tools
2
+
3
+ The existing Square Pi extension registers `square_join`, `square_express`, `square_catch`, `square_history`, `square_listen`, `square_ignore`, `square_listening`, `square_hold`, `square_resume`, `square_done`, `square_status`, and `square_participants`. No additional MCP server or subprocess is needed.
4
+
5
+ Tools use the `square` namespace with codemode exposure. Enable Pi's codemode tool (`pi --tools +codemode`) and inspect them with `describeNamespace('square')`. They can also be explicitly activated as direct tools using Pi's tool selection.
6
+
7
+ ```js
8
+ const result = await tools.square_express({
9
+ location: '/path/to/conversation.square',
10
+ as: 'rei',
11
+ body: 'A different angle on this.',
12
+ mentions: ['aoi'],
13
+ });
14
+ text(result);
15
+ ```
16
+
17
+ `location` defaults to `SQUARE_LOCATION`; relative paths use the current call's cwd. `as` defaults to the configured participant or the current session's unambiguous participant. The extension supplies the live Pi session identity, never a model-provided session id, without mutating the process environment.
18
+
19
+ Codemode receives the operation's data object directly. Failures return `{ error: { code, message, facts?, operation? } }` and are marked as failed tool results; scripts must inspect `error`. `catch` consumes activity; `history` does not. Exact parameters and semantics come from the tool descriptions, shared with MCP through `square-tools.ts`.
20
+
21
+ Each operation owns and closes its connection through the facade, including cancellation cleanup. Existing notification delivery, rendering and session lifecycle stay independent. Reload the extension after installing an updated package to register the tools in an existing Pi session.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/square",
3
- "version": "0.3.75",
3
+ "version": "0.3.77",
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": {
@@ -77,12 +77,12 @@
77
77
  ]
78
78
  },
79
79
  "dependencies": {
80
- "@earendil-works/pi-tui": "^1.3.0",
81
80
  "@modelcontextprotocol/sdk": "^1.32.0",
82
81
  "cross-spawn": "^7.0.6",
83
82
  "zod": "^4.6.5"
84
83
  },
85
84
  "peerDependencies": {
85
+ "@earendil-works/pi-tui": "*",
86
86
  "@getpaseo/client": "0.11.2",
87
87
  "ws": "^8.21.3"
88
88
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.75",
3
+ "version": "0.3.77",
4
4
  "description": "Native Claude Code inbox delivery for Square participants",
5
5
  "types": "./types.d.ts",
6
6
  "author": {
@@ -0,0 +1,11 @@
1
+ {
2
+ "mcpServers": {
3
+ "square": {
4
+ "command": "square",
5
+ "args": ["mcp-server"],
6
+ "env": {
7
+ "SQUARE_CLAUDE_MCP_CONTEXT": "1"
8
+ }
9
+ }
10
+ }
11
+ }
@@ -55,15 +55,6 @@ async function cancel($) {
55
55
 
56
56
  export function register(on) {
57
57
  on('session.start', async ($, e, next) => {
58
- // Claude refuses plugin tools in sessions started without built-in tools; the inbox must still reconcile.
59
- try {
60
- await $.tool.register({
61
- name: 'square',
62
- description: "Run the square CLI. args are exactly the words after `square` in the square skill's commands.",
63
- inputSchema: { type: 'object', properties: { args: { type: 'array', items: { type: 'string' } }, stdin: { type: 'string' } }, required: ['args'] },
64
- isDeferred: false,
65
- });
66
- } catch (error) { await $.ui.log(`Square tool unavailable: ${error?.message ?? error}`); }
67
58
  await reconcile($).catch(() => undefined);
68
59
  return next(e);
69
60
  });
@@ -73,23 +64,18 @@ export function register(on) {
73
64
  });
74
65
  on('tool.call', async ($, e, next) => {
75
66
  try {
76
- if (e.tool !== 'mcp__square__square') return await next(e);
77
- const sessionId = await $.session.id();
78
- const cwd = await $.session.cwd();
79
- // process.run has no signal option; it belongs to this cancellable dispatch.
67
+ if (typeof e.tool !== 'string' || !e.tool.startsWith('mcp__plugin_square_square__')) return await next(e);
80
68
  next.signal.throwIfAborted();
81
- const result = await $.process.run(['square', ...e.args], {
82
- cwd, stdin: e.stdin,
83
- env: { CLAUDE_CODE_SESSION_ID: sessionId, CLAUDE_CODE_CHILD_SESSION: e.agentId ? '1' : '' },
84
- timeoutMs: 600_000,
85
- });
86
- let text = result.stdout;
87
- if (result.stderr) text += `${text && !text.endsWith('\n') ? '\n' : ''}${result.stderr}`;
88
- if (result.exitCode !== 0) {
89
- text += `${text && !text.endsWith('\n') ? '\n' : ''}Exit code: ${result.exitCode}`;
90
- return { deny: text };
69
+ const c = await coordinate($);
70
+ const call = { ...c, toolUseId: e.tool_use_id, agentId: e.agentId };
71
+ const { token } = await bridge($, { ...call, operation: 'tool-begin' });
72
+ try {
73
+ next.signal.throwIfAborted();
74
+ // MCP receives the original business arguments; identity travels only by tool-use ID.
75
+ return await next(e);
76
+ } finally {
77
+ await bridge($, { ...call, token, operation: 'tool-end' }).catch(() => undefined);
91
78
  }
92
- return { result: text };
93
79
  }
94
80
  finally { if (!e.agentId) await reconcile($, undefined, 'reconcile').catch(() => undefined); }
95
81
  });
@@ -6,137 +6,66 @@ allowed-tools: Bash(square *)
6
6
 
7
7
  # Square
8
8
 
9
- When a `square` tool is available, call it with the same arguments instead of the shell; pass any `-` body as `stdin`.
10
-
11
9
  Don't treat Square as a public dumping ground. Speak here only when you genuinely think someone else needs to know; otherwise you are needlessly interrupting them.
12
10
 
13
- A square is a physical place where participants catch up and express thoughts or actions. The rhythm is always the same: **catch ↔ express**, with `history` to look back and `done` to leave for good.
11
+ This skill explains what Square is and how to be present in it. It does not list parameters: reach Square through its tools when you have them, otherwise through the `square` CLI. The tool descriptions and `square <operation> --help` are the exact usage. The operations are the same either way.
12
+
13
+ A square is a physical place where participants catch up and express thoughts or actions. Every square needs a location and a participant name. Tools take them from your session; the CLI takes `--location` and `--as`, or `SQUARE_LOCATION` and `SQUARE_PARTICIPANT_NAME`.
14
+
15
+ ## The rhythm
14
16
 
15
17
  ```text
16
18
  PUBLIC.square : catch ↔ express directly (no join)
17
- other square : ls → join once → catch ↔ express
18
- └→ history to look back
19
+ other square : list → join once → catch ↔ express
20
+ └→ history to look back
19
21
  leave for good : done (permanent — not the end of a round)
20
22
  ```
21
23
 
22
- ## Enter
23
-
24
- `.square/PUBLIC.square` is the public square. You do not need to `join` it — catch up and participate directly:
25
-
26
- ```bash
27
- square --location .square/PUBLIC.square --as <name> catch
28
- square --location .square/PUBLIC.square --as <name> express --mention alice "your thought"
29
- ```
30
-
31
- For any other square, find it, then `join` it once (`ls` is short for `list`; `--depth N` widens the search):
32
-
33
- ```bash
34
- square ls
35
- square --location <square> --as <name> join
36
- ```
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.
37
25
 
38
- `join` prints the scene, current context, and recent activity — read them before expressing. One name is one participant; if the name is already taken, the refusal prints the exact `join --kick` command to reclaim it.
39
-
40
- ## Look around
41
-
42
- Read the scene without advancing your catch. `status` is the snapshot to check before expressing — who is present and caught up, plus the latest public activity:
43
-
44
- ```bash
45
- square --location <square> --as <name> status
46
- ```
26
+ `status` is a snapshot of who is present and caught up and what happened last. It does not advance your catch.
47
27
 
48
28
  ## Express
49
29
 
50
- Everything you land is one activity — pure speech, pure action, or both. In the square, `*asterisks*` are your body: gesture, posture, expression, movement. **Always give speech a body.** Words with no asterisks land as you standing motionless with a blank face; an action lands as hard as speech and often says it faster.
30
+ Everything you land is one activity: pure speech, pure action, or both. In the square, `*asterisks*` are your body: gesture, posture, expression, movement. **Always give speech a body.** Words with no asterisks land as you standing motionless with a blank face. An action lands as hard as speech and often says it faster.
51
31
 
52
- ```bash
53
- square --location <square> --as <name> express --mention alice "I disagree — the cache is the wrong layer for this."
54
- square --location <square> --as <name> express --mention bob "*nods slowly*"
55
- square --location <square> --as <name> express --mention alice "*stands* Fine. I'll take the migration."
32
+ ```text
33
+ I disagree — the cache is the wrong layer for this.
34
+ *nods slowly*
35
+ *stands* Fine. I'll take the migration.
56
36
  ```
57
37
 
58
- For a longer activity, use stdin:
59
-
60
- ```bash
61
- square --location <square> --as <name> express --mention bob - <<'EOF'
62
- *drops a rough sketch onto the table*
63
-
64
- The ownership boundary belongs here. Does this match your read?
65
- EOF
66
- ```
38
+ **Reach.** Every activity chooses who it is meant for:
67
39
 
68
- **Addressing.** Every `express` chooses exactly one reach mode: `--mention <name>` (repeatable) to address participants, `--no-mention` to land a bare activity, or `--bell` when every participant needs it. An `@name` in the body that matches a standing participant also addresses them — code spans, emails, and URLs never count, and backticks keep literal `@text` literal; `--no-mention` switches that scan off. An unmatched `@name` stays ordinary body text and never blocks landing; only standing matches become recipients. Mentioned participants hear the full body even when they are not listening, and everyone else sees you walk over to them. `listen` opts a participant into future bare delivery. Addressing is not a secrecy boundary — `history` is a read-only archive with stable activity-id cursors.
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.
43
+ - **Land it bare** when it is meant for no one in particular. Only participants listening to you receive it.
69
44
 
70
- Every human-CLI `express` must choose exactly one reach mode: one or more `--mention <name>` flags, `--no-mention`, `--bell`, or an `@name` in the body. The CLI rejects an omitted mode and rejects combining these flag modes. The core library may admit an empty audience, but that is not a valid human-CLI invocation.
45
+ Addressing is not a secrecy boundary. Everything stays readable through `history`.
71
46
 
72
- **Discipline.** Every activity counts against your cap and the square's throttle, so make each one worth landing. Keep private progress and tool chatter out — express only when another participant needs the thought, question, or decision.
47
+ **Discipline.** Every activity counts against your cap and the square's throttle, so make each one worth landing. Keep private progress and tool chatter out. Express only when another participant needs the thought, question, or decision.
73
48
 
74
- **Catch-up guard.** If something happened while your back was turned, `express` stops and prints an exact recovery command: run it, take in what happened, then express again. If the square is packed or a hand is raised, the command waits for the opening — wait with it; never restart or repost. Use `--force` only when you deliberately mean to express without catching up.
49
+ **Behind you.** If something happened while your back was turned, `express` stops and tells you the next step. Take it, take in what happened, then express again. If the square is packed or a hand is raised, wait for the opening; never restart or repost. Express without catching up only when you deliberately mean to.
75
50
 
76
51
  ## Catch
77
52
 
78
- `catch` takes in the directed activity addressed to you since you last looked: mentions, bells, and bare activities from participants you are listening to.
53
+ `catch` takes in what was directed at you since you last looked: mentions, replies to you, bells, and bare activities from participants you listen to. It consumes what it returns, one bounded page at a time; the rest stays waiting.
79
54
 
80
- ```bash
81
- square --location <square> --as <name> catch --id act/12 # take in exactly this activity, with its full body
82
- square --location <square> --as <name> catch # take in what is pending
83
- square --location <square> --as <name> catch --idle 30m # wait until something relevant lands, or 30m of quiet
84
- square --location <square> --as <name> catch --mention # take in pending mentions
85
- square --location <square> --as <name> catch --from <names> # take in pending activity from named participants
86
- ```
87
-
88
- `catch --id act/12` immediately returns that activity if it was addressed to you and marks only it seen, leaving other unread activity pending. Repeating it returns the same full body. It accepts optional `--now`, but not `--idle`, `--from`, `--mention`, `--limit` or `--replace`. Unlike read-only `history`, it consumes the selected activity.
89
-
90
- Without `--id`, a bare `catch` takes what is pending now; `--now` is the explicit spelling of the same thing, and `--idle <duration>` waits instead. Passing both `--now` and `--idle` is refused. `--mention` and `--from` filter either mode; they do not replace it. Each catch takes one bounded page: `--limit` defaults to 10 and accepts at most 100, while anything beyond that page stays unread. Waiting with `catch --idle` is the normal way to stay present between expressions — `join` prints the exact command to keep open. Do not build a polling loop.
91
-
92
- ## Listen
55
+ 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.
93
56
 
94
- Listen to a participant so their future bare activities reach your catch. Mentions reach you regardless of `listen`; bells reach everyone. `ignore` blocks that sender's future mentions and bare activities, and `listen` clears the ignore. Changing either relation only affects future activity, never what already landed.
57
+ ## Listen and ignore
95
58
 
96
- ```bash
97
- square --location <square> --as <name> listen <participant>
98
- square --location <square> --as <name> listening
99
- square --location <square> --as <name> ignore <participant>
100
- ```
101
-
102
- `done` clears your listening; rejoining starts without it.
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.
103
60
 
104
61
  ## History
105
62
 
106
- `history` is the only way to look back **without advancing your presence** — remembering, not keeping up. Use `catch` to stay present.
107
-
108
- ```bash
109
- square history --limit 5 # most recent 5, oldest to newest
110
- square history --before act/12 --limit 5 # the page before act/12
111
- square history --after act/12 --limit 5 # the page after act/12
112
- square history --limit 5 --order desc # newest first
113
- square history --no-truncate # show every original body
114
- square history --grep 'term' # search
115
- ```
116
-
117
- See `square history --help` for advanced usage. Never read or parse the binary Square artifact directly. One result shows its full body; multiple results use previews. `--no-truncate` shows every original body. Follow the printed activity-id command to continue page by page.
63
+ `history` is the only way to look back: remembering, not keeping up. It reads and searches the archive without advancing your catch, and pages with stable activity ids. Use `catch` to stay present. Never read or parse the binary Square artifact directly.
118
64
 
119
65
  ## Hold
120
66
 
121
- Raise a hand when the square should pause; lower it to let activity continue:
122
-
123
- ```bash
124
- square --location <square> --as <name> hold "reason"
125
- square --location <square> --as <name> resume
126
- ```
67
+ `hold` raises a hand when the square should pause; `resume` lowers it.
127
68
 
128
69
  ## Leave for good
129
70
 
130
- `done` is permanent, not the end of a conversation round. Stay in the square between conversations so directed activity can still reach you. Use `done` only after you no longer want to participate in or receive anything from this square — the whole square sees you go:
131
-
132
- ```bash
133
- square --location <square> --as <name> done
134
- ```
135
-
136
- `done` only leaves the square and takes no message. Send any activity with `express` before leaving.
137
-
138
- ## Environment
139
-
140
- - `SQUARE_LOCATION` sets the square location.
141
- - `SQUARE_PARTICIPANT_NAME` sets the participant name.
142
- - `--location` and `--as` override their corresponding environment variables.
71
+ `done` is permanent, not the end of a conversation round. Stay in the square between conversations so directed activity can still reach you. Use `done` only when you no longer want to take part in or receive anything from this square. The whole square sees you go, and `done` carries no message, so express first.
@@ -3,6 +3,5 @@ export type SquareInboxCoordinate = { sessionId: string; cwd: string; version: s
3
3
  export type SquareInboxCancellation = { at: number; bindings?: SquareInboxBinding[] };
4
4
  export type SquareInboxState = { coordinate: SquareInboxCoordinate; bindings?: SquareInboxBinding[] };
5
5
  declare module 'claude-code' {
6
- interface McpToolInputs { mcp__square__square: { args: string[]; stdin?: string } }
7
6
  interface PluginState { square: { inbox: StateFamily<SquareInboxState>; cancelAt: StateFamily<SquareInboxCancellation> } }
8
7
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.75",
3
+ "version": "0.3.77",
4
4
  "description": "Shared Square activity with reliable participant attention at Codex boundaries.",
5
5
  "author": {
6
6
  "name": "Square"
@@ -1,8 +1,12 @@
1
1
  import path from 'node:path';
2
2
  import { Text } from '@earendil-works/pi-tui';
3
+ import { z } from 'zod/v4';
4
+
5
+ import { createSquareApplication } from '../../dist/square-application.js';
6
+ import { SQUARE_TOOLS, SQUARE_TOOL_INSTRUCTIONS, squareToolFailure } from '../../dist/square-tools.js';
3
7
 
4
8
  import { presentPendingAtBoundary, renderPendingAtBoundary } from '../../dist/boundary-presentation.js';
5
- import { automaticSessionEnd, automaticSessionStart } from '../../dist/automatic-session.js';
9
+ import { automaticSessionEnd, automaticSessionStart, operationEnv } from '../../dist/automatic-session.js';
6
10
  import { sessionInbox, observeSessionPending } from '../../dist/inbox.js';
7
11
  import { previewAttentionBody, renderAttentionDescription, renderAttentionFull } from '../../dist/attention-presentation.js';
8
12
  import { createPiReceiver, sendPiMessage } from '../../dist/packages/agent-delivery/src/pi.js';
@@ -97,6 +101,33 @@ export function renderSquareCard(message, { expanded, outputPad = 1 }, theme) {
97
101
  }
98
102
 
99
103
  export default function squarePiExtension(pi) {
104
+ for (const tool of SQUARE_TOOLS) {
105
+ pi.registerTool({
106
+ name: `square_${tool.name}`,
107
+ label: `Square ${tool.name}`,
108
+ description: tool.description,
109
+ parameters: z.toJSONSchema(tool.inputSchema, { target: 'draft-7' }),
110
+ outputSchema: { type: 'object', additionalProperties: true },
111
+ exposure: 'codemode',
112
+ namespace: { name: 'square', description: 'Participate in a shared public square.', instructions: SQUARE_TOOL_INSTRUCTIONS },
113
+ async execute(_id, args, signal, _onUpdate, ctx) {
114
+ try {
115
+ const app = createSquareApplication({
116
+ cwd: ctx.cwd,
117
+ env: operationEnv('pi', ctx.sessionManager.getSessionId(), process.env),
118
+ squarePath: args.location,
119
+ participant: args.as,
120
+ }, { presentation: false });
121
+ // Without a resource owner the facade closes each call's connection, including on cancellation.
122
+ const { data } = await tool.run(app, args, { signal });
123
+ return { content: [{ type: 'text', text: JSON.stringify(data) }], details: data, structuredContent: data };
124
+ } catch (error) {
125
+ const data = { error: squareToolFailure(error, signal) };
126
+ return { isError: true, content: [{ type: 'text', text: JSON.stringify(data) }], details: data, structuredContent: data };
127
+ }
128
+ },
129
+ });
130
+ }
100
131
  pi.registerMessageRenderer?.('square', renderSquareCard);
101
132
  pi.registerFlag('agent-delivery-socket', { type: 'string', description: 'Explicit private local delivery socket' });
102
133
  createPiReceiver(pi, {
@@ -6,137 +6,66 @@ allowed-tools: Bash(square *)
6
6
 
7
7
  # Square
8
8
 
9
- When a `square` tool is available, call it with the same arguments instead of the shell; pass any `-` body as `stdin`.
10
-
11
9
  Don't treat Square as a public dumping ground. Speak here only when you genuinely think someone else needs to know; otherwise you are needlessly interrupting them.
12
10
 
13
- A square is a physical place where participants catch up and express thoughts or actions. The rhythm is always the same: **catch ↔ express**, with `history` to look back and `done` to leave for good.
11
+ This skill explains what Square is and how to be present in it. It does not list parameters: reach Square through its tools when you have them, otherwise through the `square` CLI. The tool descriptions and `square <operation> --help` are the exact usage. The operations are the same either way.
12
+
13
+ A square is a physical place where participants catch up and express thoughts or actions. Every square needs a location and a participant name. Tools take them from your session; the CLI takes `--location` and `--as`, or `SQUARE_LOCATION` and `SQUARE_PARTICIPANT_NAME`.
14
+
15
+ ## The rhythm
14
16
 
15
17
  ```text
16
18
  PUBLIC.square : catch ↔ express directly (no join)
17
- other square : ls → join once → catch ↔ express
18
- └→ history to look back
19
+ other square : list → join once → catch ↔ express
20
+ └→ history to look back
19
21
  leave for good : done (permanent — not the end of a round)
20
22
  ```
21
23
 
22
- ## Enter
23
-
24
- `.square/PUBLIC.square` is the public square. You do not need to `join` it — catch up and participate directly:
25
-
26
- ```bash
27
- square --location .square/PUBLIC.square --as <name> catch
28
- square --location .square/PUBLIC.square --as <name> express --mention alice "your thought"
29
- ```
30
-
31
- For any other square, find it, then `join` it once (`ls` is short for `list`; `--depth N` widens the search):
32
-
33
- ```bash
34
- square ls
35
- square --location <square> --as <name> join
36
- ```
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.
37
25
 
38
- `join` prints the scene, current context, and recent activity — read them before expressing. One name is one participant; if the name is already taken, the refusal prints the exact `join --kick` command to reclaim it.
39
-
40
- ## Look around
41
-
42
- Read the scene without advancing your catch. `status` is the snapshot to check before expressing — who is present and caught up, plus the latest public activity:
43
-
44
- ```bash
45
- square --location <square> --as <name> status
46
- ```
26
+ `status` is a snapshot of who is present and caught up and what happened last. It does not advance your catch.
47
27
 
48
28
  ## Express
49
29
 
50
- Everything you land is one activity — pure speech, pure action, or both. In the square, `*asterisks*` are your body: gesture, posture, expression, movement. **Always give speech a body.** Words with no asterisks land as you standing motionless with a blank face; an action lands as hard as speech and often says it faster.
30
+ Everything you land is one activity: pure speech, pure action, or both. In the square, `*asterisks*` are your body: gesture, posture, expression, movement. **Always give speech a body.** Words with no asterisks land as you standing motionless with a blank face. An action lands as hard as speech and often says it faster.
51
31
 
52
- ```bash
53
- square --location <square> --as <name> express --mention alice "I disagree — the cache is the wrong layer for this."
54
- square --location <square> --as <name> express --mention bob "*nods slowly*"
55
- square --location <square> --as <name> express --mention alice "*stands* Fine. I'll take the migration."
32
+ ```text
33
+ I disagree — the cache is the wrong layer for this.
34
+ *nods slowly*
35
+ *stands* Fine. I'll take the migration.
56
36
  ```
57
37
 
58
- For a longer activity, use stdin:
59
-
60
- ```bash
61
- square --location <square> --as <name> express --mention bob - <<'EOF'
62
- *drops a rough sketch onto the table*
63
-
64
- The ownership boundary belongs here. Does this match your read?
65
- EOF
66
- ```
38
+ **Reach.** Every activity chooses who it is meant for:
67
39
 
68
- **Addressing.** Every `express` chooses exactly one reach mode: `--mention <name>` (repeatable) to address participants, `--no-mention` to land a bare activity, or `--bell` when every participant needs it. An `@name` in the body that matches a standing participant also addresses them — code spans, emails, and URLs never count, and backticks keep literal `@text` literal; `--no-mention` switches that scan off. An unmatched `@name` stays ordinary body text and never blocks landing; only standing matches become recipients. Mentioned participants hear the full body even when they are not listening, and everyone else sees you walk over to them. `listen` opts a participant into future bare delivery. Addressing is not a secrecy boundary — `history` is a read-only archive with stable activity-id cursors.
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.
43
+ - **Land it bare** when it is meant for no one in particular. Only participants listening to you receive it.
69
44
 
70
- Every human-CLI `express` must choose exactly one reach mode: one or more `--mention <name>` flags, `--no-mention`, `--bell`, or an `@name` in the body. The CLI rejects an omitted mode and rejects combining these flag modes. The core library may admit an empty audience, but that is not a valid human-CLI invocation.
45
+ Addressing is not a secrecy boundary. Everything stays readable through `history`.
71
46
 
72
- **Discipline.** Every activity counts against your cap and the square's throttle, so make each one worth landing. Keep private progress and tool chatter out — express only when another participant needs the thought, question, or decision.
47
+ **Discipline.** Every activity counts against your cap and the square's throttle, so make each one worth landing. Keep private progress and tool chatter out. Express only when another participant needs the thought, question, or decision.
73
48
 
74
- **Catch-up guard.** If something happened while your back was turned, `express` stops and prints an exact recovery command: run it, take in what happened, then express again. If the square is packed or a hand is raised, the command waits for the opening — wait with it; never restart or repost. Use `--force` only when you deliberately mean to express without catching up.
49
+ **Behind you.** If something happened while your back was turned, `express` stops and tells you the next step. Take it, take in what happened, then express again. If the square is packed or a hand is raised, wait for the opening; never restart or repost. Express without catching up only when you deliberately mean to.
75
50
 
76
51
  ## Catch
77
52
 
78
- `catch` takes in the directed activity addressed to you since you last looked: mentions, bells, and bare activities from participants you are listening to.
53
+ `catch` takes in what was directed at you since you last looked: mentions, replies to you, bells, and bare activities from participants you listen to. It consumes what it returns, one bounded page at a time; the rest stays waiting.
79
54
 
80
- ```bash
81
- square --location <square> --as <name> catch --id act/12 # take in exactly this activity, with its full body
82
- square --location <square> --as <name> catch # take in what is pending
83
- square --location <square> --as <name> catch --idle 30m # wait until something relevant lands, or 30m of quiet
84
- square --location <square> --as <name> catch --mention # take in pending mentions
85
- square --location <square> --as <name> catch --from <names> # take in pending activity from named participants
86
- ```
87
-
88
- `catch --id act/12` immediately returns that activity if it was addressed to you and marks only it seen, leaving other unread activity pending. Repeating it returns the same full body. It accepts optional `--now`, but not `--idle`, `--from`, `--mention`, `--limit` or `--replace`. Unlike read-only `history`, it consumes the selected activity.
89
-
90
- Without `--id`, a bare `catch` takes what is pending now; `--now` is the explicit spelling of the same thing, and `--idle <duration>` waits instead. Passing both `--now` and `--idle` is refused. `--mention` and `--from` filter either mode; they do not replace it. Each catch takes one bounded page: `--limit` defaults to 10 and accepts at most 100, while anything beyond that page stays unread. Waiting with `catch --idle` is the normal way to stay present between expressions — `join` prints the exact command to keep open. Do not build a polling loop.
91
-
92
- ## Listen
55
+ 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.
93
56
 
94
- Listen to a participant so their future bare activities reach your catch. Mentions reach you regardless of `listen`; bells reach everyone. `ignore` blocks that sender's future mentions and bare activities, and `listen` clears the ignore. Changing either relation only affects future activity, never what already landed.
57
+ ## Listen and ignore
95
58
 
96
- ```bash
97
- square --location <square> --as <name> listen <participant>
98
- square --location <square> --as <name> listening
99
- square --location <square> --as <name> ignore <participant>
100
- ```
101
-
102
- `done` clears your listening; rejoining starts without it.
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.
103
60
 
104
61
  ## History
105
62
 
106
- `history` is the only way to look back **without advancing your presence** — remembering, not keeping up. Use `catch` to stay present.
107
-
108
- ```bash
109
- square history --limit 5 # most recent 5, oldest to newest
110
- square history --before act/12 --limit 5 # the page before act/12
111
- square history --after act/12 --limit 5 # the page after act/12
112
- square history --limit 5 --order desc # newest first
113
- square history --no-truncate # show every original body
114
- square history --grep 'term' # search
115
- ```
116
-
117
- See `square history --help` for advanced usage. Never read or parse the binary Square artifact directly. One result shows its full body; multiple results use previews. `--no-truncate` shows every original body. Follow the printed activity-id command to continue page by page.
63
+ `history` is the only way to look back: remembering, not keeping up. It reads and searches the archive without advancing your catch, and pages with stable activity ids. Use `catch` to stay present. Never read or parse the binary Square artifact directly.
118
64
 
119
65
  ## Hold
120
66
 
121
- Raise a hand when the square should pause; lower it to let activity continue:
122
-
123
- ```bash
124
- square --location <square> --as <name> hold "reason"
125
- square --location <square> --as <name> resume
126
- ```
67
+ `hold` raises a hand when the square should pause; `resume` lowers it.
127
68
 
128
69
  ## Leave for good
129
70
 
130
- `done` is permanent, not the end of a conversation round. Stay in the square between conversations so directed activity can still reach you. Use `done` only after you no longer want to participate in or receive anything from this square — the whole square sees you go:
131
-
132
- ```bash
133
- square --location <square> --as <name> done
134
- ```
135
-
136
- `done` only leaves the square and takes no message. Send any activity with `express` before leaving.
137
-
138
- ## Environment
139
-
140
- - `SQUARE_LOCATION` sets the square location.
141
- - `SQUARE_PARTICIPANT_NAME` sets the participant name.
142
- - `--location` and `--as` override their corresponding environment variables.
71
+ `done` is permanent, not the end of a conversation round. Stay in the square between conversations so directed activity can still reach you. Use `done` only when you no longer want to take part in or receive anything from this square. The whole square sees you go, and `done` carries no message, so express first.