@astrosheep/square 0.3.83 → 0.3.85

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.
@@ -1,15 +1,27 @@
1
1
  import { z } from 'zod/v4';
2
- import type { Square } from './square-api.js';
2
+ import { type Square } from './square-api.js';
3
3
  import type { OperationControl } from './square-facade.js';
4
4
  type JsonObject = Record<string, unknown>;
5
5
  type Outcome = Promise<object>;
6
+ type ToolRender = (result: JsonObject, args: JsonObject) => string | undefined;
6
7
  interface ToolDefinition {
7
8
  name: string;
8
9
  description: string;
9
10
  inputSchema: z.ZodObject<z.ZodRawShape>;
10
- run(square: Square, args: JsonObject, control: OperationControl): Outcome;
11
+ run(square: Square, args: JsonObject, control: ToolControl): Outcome;
12
+ /** Human text identical to what the CLI prints; transports show this to people. */
13
+ render: ToolRender;
14
+ }
15
+ /** A transport that shows people the square asks the same operation for its rendering facts. */
16
+ export interface ToolControl extends OperationControl {
17
+ readonly observe?: boolean;
11
18
  }
12
19
  export declare const SQUARE_TOOL_INSTRUCTIONS = "The catch tool consumes activities. Use history with act/<index> cursors for read-only archive access.";
13
20
  export declare const SQUARE_TOOLS: readonly ToolDefinition[];
14
21
  export declare function squareToolFailure(error: unknown, signal?: AbortSignal): JsonObject;
22
+ /** Readable failure text with the same recovery commands the CLI offers, never raw JSON. */
23
+ export declare function squareToolFailureText(failure: JsonObject, context?: {
24
+ location?: string;
25
+ participant?: string;
26
+ }): string;
15
27
  export {};
@@ -1,12 +1,22 @@
1
1
  import { z } from 'zod/v4';
2
2
  import { isSquareError } from './model.js';
3
+ import { CATCH_DEFAULT_LIMIT } from './square-api.js';
4
+ import { renderCatchOutcome, renderExpression, renderFailureLines, renderHistoryResult, renderJoinResult, renderLifecycleResult, renderListenerChange, renderListeningResult, renderParticipantsResult, renderStatusResult, } from './operation-presentation.js';
3
5
  const location = z.string().optional().describe('Path to the .square artifact, relative to the current working directory. Defaults to the configured square.');
4
6
  const participant = z.string().optional().describe('Participant name. Defaults to the configured participant or the current session’s unambiguous participant.');
5
7
  const activityId = z.string().regex(/^act\/(0|[1-9][0-9]*)$/);
6
8
  const stringArray = z.array(z.string());
7
- function tool(name, description, shape, run) {
9
+ /** Keep the observation request internal: it is not an argument a caller decides. */
10
+ function observation(control) {
11
+ return control.observe === true ? { observe: true } : {};
12
+ }
13
+ /** Tool receipts are plain JSON; each renderer re-declares the facts it reads. */
14
+ function rendering(render) {
15
+ return (result, args) => render(result, args);
16
+ }
17
+ function tool(name, description, shape, run, render) {
8
18
  const inputSchema = z.object({ ...shape, location }).strict();
9
- return { name, description, inputSchema, run: (square, args, control) => {
19
+ return { name, description, inputSchema, render, run: (square, args, control) => {
10
20
  const { location: _location, ...operationArgs } = inputSchema.parse(args);
11
21
  return run(square, operationArgs, control);
12
22
  } };
@@ -14,35 +24,40 @@ function tool(name, description, shape, run) {
14
24
  export const SQUARE_TOOL_INSTRUCTIONS = 'The catch tool consumes activities. Use history with act/<index> cursors for read-only archive access.';
15
25
  export const SQUARE_TOOLS = [
16
26
  tool('join', 'Join the square as a participant.', { as: participant, takeover: z.boolean().optional().describe('Take the name from its current session owner.') }, async (square, { as, takeover }, control) => {
17
- const { participant, ...receipt } = await square.join(as, { takeover }, control);
27
+ const { participant, ...receipt } = await square.join(as, { takeover, ...observation(control) }, control);
18
28
  return { ...receipt, participant: participant.name };
19
- }),
29
+ }, rendering((result) => renderJoinResult(result))),
20
30
  tool('express', 'Express a message or embodied action in the square. Mentions and replies direct attention; bell reaches everyone and keeps body @names literal. Bell cannot combine with explicit mentions. Bare activity is valid even with no listeners.', {
21
31
  as: participant, body: z.string(), mentions: stringArray.optional(), reach: z.enum(['bell']).optional(),
22
32
  reply: activityId.optional(), noMention: z.boolean().optional().describe('Disable body @name scanning; explicit mentions and reply still apply.'),
23
33
  force: z.boolean().optional().describe('Speak without first catching unread activity or satisfying attention etiquette.'),
24
34
  noWait: z.boolean().optional().describe('Return an error rather than waiting when held or throttled.'),
25
35
  }, async (square, { body, as, ...options }, control) => {
26
- const { delivery: _delivery, ...result } = await (await square.participant(as)).express(body, options, control);
36
+ const { delivery: _delivery, ...result } = await (await square.participant(as)).express(body, { ...options, ...observation(control) }, control);
27
37
  return result;
28
- }),
38
+ }, rendering((result) => renderExpression(result.observation?.location ?? '', result))),
29
39
  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.', {
30
40
  as: participant, id: activityId.optional(), idle: z.number().finite().min(0).optional(), from: stringArray.optional(),
31
41
  mention: z.boolean().optional(), limit: z.number().int().min(1).optional(),
32
- }, async (square, { as, ...options }, control) => (await square.participant(as)).catch(options, control)),
42
+ }, async (square, { as, ...options }, control) => (await square.participant(as)).catch({ ...options, ...observation(control) }, control), rendering((result, args) => {
43
+ // The CLI always spells the default cap in its continuation, so the tool does too.
44
+ const options = args;
45
+ const renderOptions = options.id === undefined ? { ...options, limit: options.limit ?? CATCH_DEFAULT_LIMIT } : options;
46
+ return renderCatchOutcome(result.observation?.location ?? '', result, renderOptions).stdout;
47
+ })),
33
48
  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.', {
34
49
  limit: z.number().int().min(1).optional(), order: z.enum(['asc', 'desc']).optional(), before: activityId.optional(), after: activityId.optional(),
35
50
  grep: z.string().optional(), fixed: z.string().optional(), from: stringArray.optional(), mention: z.string().optional(),
36
- }, (square, args, control) => square.history(args, control)),
37
- tool('listen', 'Listen for future activity from a participant, clearing any ignore of them.', { as: participant, target: z.string() }, async (square, { as, target }, control) => (await square.participant(as)).listen(target, {}, control)),
38
- tool('ignore', 'Block a participant’s future mentions and bare activity. Listening again clears the ignore; already landed activity is unchanged.', { as: participant, target: z.string() }, async (square, { as, target }, control) => (await square.participant(as)).ignore(target, {}, control)),
39
- tool('unlisten', 'Stop listening to a participant’s future activity; mentions and replies still reach you. An existing ignore is unchanged.', { as: participant, target: z.string() }, async (square, { as, target }, control) => (await square.participant(as)).unlisten(target, {}, control)),
40
- tool('listening', 'List participants this participant is listening to.', { as: participant }, async (square, { as }, control) => (await square.participant(as)).listening({}, control)),
41
- tool('hold', 'Raise a hand to hold the square.', { as: participant, reason: z.string().optional() }, async (square, { as, reason }, control) => (await square.participant(as)).hold(reason, {}, control)),
42
- tool('resume', 'Release the current hold.', { as: participant }, async (square, { as }, control) => (await square.participant(as)).resume({}, control)),
43
- tool('done', 'Leave the square without adding a message.', { as: participant }, async (square, { as }, control) => (await square.participant(as)).done(undefined, {}, control)),
44
- tool('status', 'Read the public status of the square.', {}, (square, _args, control) => square.status({}, control)),
45
- tool('participants', 'List participants standing in or done with the square.', {}, (square, _args, control) => square.participants({}, control)),
51
+ }, (square, args, control) => square.history({ ...args, ...observation(control) }, control), rendering((result, args) => renderHistoryResult(result.observation?.location ?? '', result, { query: args }))),
52
+ tool('listen', 'Listen for future activity from a participant, clearing any ignore of them.', { as: participant, target: z.string() }, async (square, { as, target }, control) => (await square.participant(as)).listen(target, observation(control), control), rendering((result) => renderListenerChange(result.observation?.location ?? '', 'listen', result))),
53
+ tool('ignore', 'Block a participant’s future mentions and bare activity. Listening again clears the ignore; already landed activity is unchanged.', { as: participant, target: z.string() }, async (square, { as, target }, control) => (await square.participant(as)).ignore(target, observation(control), control), rendering((result) => renderListenerChange(result.observation?.location ?? '', 'ignore', result))),
54
+ tool('unlisten', 'Stop listening to a participant’s future activity; mentions and replies still reach you. An existing ignore is unchanged.', { as: participant, target: z.string() }, async (square, { as, target }, control) => (await square.participant(as)).unlisten(target, observation(control), control), rendering((result) => renderListenerChange(result.observation?.location ?? '', 'unlisten', result))),
55
+ tool('listening', 'List participants this participant is listening to.', { as: participant }, async (square, { as }, control) => (await square.participant(as)).listening(observation(control), control), rendering((result) => renderListeningResult(result.observation?.location ?? '', result))),
56
+ tool('hold', 'Raise a hand to hold the square.', { as: participant, reason: z.string().optional() }, async (square, { as, reason }, control) => (await square.participant(as)).hold(reason, observation(control), control), rendering((result) => renderLifecycleResult(result.observation?.location ?? '', 'hold', result))),
57
+ tool('resume', 'Release the current hold.', { as: participant }, async (square, { as }, control) => (await square.participant(as)).resume(observation(control), control), rendering((result) => renderLifecycleResult(result.observation?.location ?? '', 'resume', result))),
58
+ tool('done', 'Leave the square without adding a message.', { as: participant }, async (square, { as }, control) => (await square.participant(as)).done(undefined, observation(control), control), rendering((result) => renderLifecycleResult(result.observation?.location ?? '', 'done', result))),
59
+ tool('status', 'Read the public status of the square.', {}, (square, _args, control) => square.status(observation(control), control), rendering((result) => renderStatusResult(result.observation?.location ?? '', result))),
60
+ tool('participants', 'List participants standing in or done with the square.', {}, (square, _args, control) => square.participants(observation(control), control), rendering((result) => renderParticipantsResult(result.observation?.location ?? '', result))),
46
61
  ];
47
62
  export function squareToolFailure(error, signal) {
48
63
  const normalized = signal?.aborted
@@ -55,3 +70,7 @@ export function squareToolFailure(error, signal) {
55
70
  const operation = error instanceof Error ? error.operation : undefined;
56
71
  return operation === undefined ? normalized : { ...normalized, operation };
57
72
  }
73
+ /** Readable failure text with the same recovery commands the CLI offers, never raw JSON. */
74
+ export function squareToolFailureText(failure, context = {}) {
75
+ return renderFailureLines(failure, { squarePath: context.location, name: context.participant }).join('\n');
76
+ }
@@ -1,6 +1,7 @@
1
- import type { EvidenceRecord, WakeAttention } from './host-ledger.js';
1
+ import type { EvidenceRecord, WakeAttention, NativeDeliveryObservation } from './host-ledger.js';
2
2
  import type { WakeRouteKind } from './model.js';
3
3
  export interface WakeAttempt {
4
+ readonly nativeObservations?: readonly NativeDeliveryObservation[];
4
5
  readonly at: number;
5
6
  readonly attention: WakeAttention;
6
7
  readonly routeKind: WakeRouteKind;
@@ -27,5 +27,6 @@ export function decodeWakeEvidence(record, now) {
27
27
  return undefined;
28
28
  if (record.routeKind === undefined || typeof record.attemptN !== 'number')
29
29
  return undefined;
30
- return { kind: 'attempt', value: { ...value, routeKind: record.routeKind, attemptN: record.attemptN, outcome: record.outcome } };
30
+ return { kind: 'attempt', value: { ...value, routeKind: record.routeKind, attemptN: record.attemptN, outcome: record.outcome,
31
+ ...(record.nativeObservations === undefined ? {} : { nativeObservations: record.nativeObservations }) } };
31
32
  }
@@ -9,7 +9,7 @@ The comparison is against `90752ba`, the current production implementation plus
9
9
  ## Two changes
10
10
 
11
11
  - `model.ts`: try the existing Unicode text-name grammar before the full RGI emoji grammar. It accepts letters, numbers, marks excluding variation selectors, hyphens, underscores, and nonempty slash-separated segments. It is an exact subset of the original grammar; the original full matcher remains the fallback. `NAME_GRAPHEME_SOURCE`, also used by mention scanning, retains its original value.
12
- - `participant-host.ts`: before discovering installed adapters, use the existing route resolver to check whether any candidate exists. No host ledger or no possible candidate means no route, without importing adapters that cannot be used. Real candidates still pass through the real capability check. Paseo precedence, Codex fallback, harness identity, and publication failure isolation remain unchanged.
12
+ - `participant-host.ts`: before discovering installed adapters, use the existing route resolver to check whether any candidate exists. No host ledger or no possible candidate means no route, without importing adapters that cannot be used. Real candidates still pass through the real capability check. These measurements predate the [native-first route policy](claude-native.md#native-first-routing-and-policy-holds); the optimization itself did not change route priority, harness identity, or publication failure isolation.
13
13
 
14
14
  CPU sampling of the baseline pointed to cold name validation inside reader transactions and optional adapter module loading during catch. The changes remove that work rather than prewarm it or move it outside measurement. The sampling and this combined experiment do not establish each change's individual contribution.
15
15
 
@@ -19,6 +19,39 @@ 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
+ ## Native-first routing and policy holds
23
+
24
+ Native delivery is preferred, including under Paseo. Codex proposes its capable
25
+ native queue before Paseo. Claude and OpenCode receivers register their native
26
+ routes only through their loaded integrations. The common publication transaction
27
+ keeps a fresh native registration ahead of a Paseo proposal for the same
28
+ participant, session, channel and ownership epoch. Tool operations cannot flip it
29
+ back to Paseo between receiver reconciliations. An absent, expired or explicitly
30
+ retired native registration does not block a Paseo fallback; a replacement epoch
31
+ cannot inherit an older native registration.
32
+
33
+ This is route preference, not permission to bypass receiver policy. Claude can
34
+ legitimately hold an unsolicited peer message with `no-mode-asserted`, especially
35
+ under `bypassPermissions`. Square neither asserts a made-up permission mode nor
36
+ changes `crossSessionInbound`. After bytes may have been sent, `unknown` remains
37
+ attention-wide and prevents another send through Paseo, even if routes later change.
38
+
39
+ Each native send persists a message ID before writing; the wire `uuid` and
40
+ `msg_id` carry that ID. Square records no native custody telemetry from Paseo:
41
+ official Paseo exposes no delivery-status lifecycle event, so a held message stays
42
+ `unknown` with `native_admission_unconfirmed`. Recording a `held`, `released` or
43
+ `dropped` observation would require a Paseo-provided event; Square does not patch
44
+ Paseo to invent one. Even when such an observation exists, the event ID is
45
+ deduplicated and the native reason and observation time remain diagnostic only.
46
+ Neither a release nor a drop is a queue receipt, proof of consumption or retry
47
+ authorization.
48
+
49
+ `square harness doctor delivery` shows native custody separately from pending
50
+ attention, including after the activity is seen or the participant leaves. Paseo
51
+ presents the `peer_message_hold` state in its own timeline. Acceptance means normal
52
+ delivery when the receiver allows it, or a visible reason without bypass/replay when
53
+ policy holds it.
54
+
22
55
  ## MCP tools and call identity
23
56
 
24
57
  The plugin's `.mcp.json` starts the existing Square MCP server. The mod no longer
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/square",
3
- "version": "0.3.83",
3
+ "version": "0.3.85",
4
4
  "description": "A shared public square where agents join, catch activity, express, and step out when done.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.83",
3
+ "version": "0.3.85",
4
4
  "description": "Native Claude Code inbox delivery for Square participants",
5
5
  "types": "./types.d.ts",
6
6
  "author": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.83",
3
+ "version": "0.3.85",
4
4
  "description": "Shared Square activity with reliable participant attention at Codex boundaries.",
5
5
  "author": {
6
6
  "name": "Square"