@astrosheep/square 0.4.7 → 0.4.8

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 (83) hide show
  1. package/dist/card-format.d.ts +2 -0
  2. package/dist/card-format.js +2 -0
  3. package/dist/claude-mcp-context.d.ts +0 -1
  4. package/dist/claude-mcp-context.js +0 -10
  5. package/dist/claude-mod.d.ts +2 -0
  6. package/dist/claude-mod.js +211 -0
  7. package/dist/cli/context.d.ts +1 -1
  8. package/dist/cli/context.js +1 -12
  9. package/dist/cli/express-presentation.js +6 -33
  10. package/dist/cli/json.js +1 -1
  11. package/dist/cli/observation-commands.d.ts +2 -3
  12. package/dist/cli/observation-commands.js +21 -59
  13. package/dist/cli/registry.js +3 -5
  14. package/dist/cli/square-commands.d.ts +1 -2
  15. package/dist/cli/square-commands.js +7 -17
  16. package/dist/decisions.js +5 -4
  17. package/dist/harness-claude.js +3 -3
  18. package/dist/help.js +6 -8
  19. package/dist/mcp.js +1 -1
  20. package/dist/neutral.d.ts +15 -1
  21. package/dist/notifications.js +1 -1
  22. package/dist/operation-presentation.d.ts +17 -15
  23. package/dist/operation-presentation.js +58 -32
  24. package/dist/packages/agent-delivery/src/index.d.ts +4 -9
  25. package/dist/packages/agent-delivery/src/index.js +0 -9
  26. package/dist/packages/agent-delivery/src/local-endpoint.d.ts +2 -12
  27. package/dist/packages/agent-delivery/src/local-endpoint.js +4 -48
  28. package/dist/packages/agent-delivery/src/pi-client.js +0 -2
  29. package/dist/packages/agent-delivery/src/types.d.ts +3 -37
  30. package/dist/plugins/paseo/shared/card-format.d.ts +6 -0
  31. package/dist/plugins/paseo/shared/card-format.js +38 -0
  32. package/dist/plugins/paseo/shared/card-presentation.d.ts +3 -1
  33. package/dist/plugins/paseo/shared/card-presentation.js +3 -1
  34. package/dist/presentation.d.ts +2 -20
  35. package/dist/presentation.js +11 -46
  36. package/dist/sqlite-square.js +5 -6
  37. package/dist/square-facade.d.ts +3 -12
  38. package/dist/square-posting.js +1 -1
  39. package/dist/square-tools.d.ts +0 -11
  40. package/dist/square-tools.js +33 -119
  41. package/dist/square-values.d.ts +1 -1
  42. package/dist/square-values.js +1 -1
  43. package/dist/time.d.ts +1 -0
  44. package/dist/time.js +10 -0
  45. package/dist/views.d.ts +1 -1
  46. package/dist/views.js +2 -2
  47. package/dist/wake-transports.d.ts +1 -1
  48. package/dist/wake-transports.js +1 -1
  49. package/guides/catch-cold-path-optimization.md +1 -1
  50. package/guides/claude-native.md +37 -7
  51. package/guides/delivery-primitives.md +2 -1
  52. package/guides/mcp.md +3 -3
  53. package/guides/pi-native-delivery.md +10 -2
  54. package/guides/pi-tools.md +1 -3
  55. package/guides/row-storage-design.md +1 -1
  56. package/package.json +1 -1
  57. package/plugins/claude/.claude-plugin/plugin.json +3 -2
  58. package/plugins/claude/README.md +9 -0
  59. package/plugins/claude/hooks/hooks.json +1 -9
  60. package/plugins/claude/hooks/register.js +178 -0
  61. package/plugins/claude/skills/square/SKILL.md +2 -2
  62. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  63. package/plugins/paseo/OVERVIEW.md +1 -1
  64. package/plugins/paseo/README.md +20 -23
  65. package/plugins/paseo/client/markdown.tsx +88 -95
  66. package/plugins/paseo/client/palette.ts +37 -12
  67. package/plugins/paseo/client/square-message.tsx +52 -76
  68. package/plugins/paseo/client/transform.ts +4 -6
  69. package/plugins/paseo/shared/card-format.ts +37 -0
  70. package/plugins/paseo/shared/card-presentation.ts +6 -2
  71. package/plugins/paseo/shared/message.ts +1 -6
  72. package/plugins/pi/square-pi.js +69 -50
  73. package/skills/square/SKILL.md +2 -2
  74. package/dist/claude-boundary-state.d.ts +0 -20
  75. package/dist/claude-boundary-state.js +0 -84
  76. package/dist/claude-hook.d.ts +0 -2
  77. package/dist/claude-hook.js +0 -232
  78. package/dist/packages/agent-delivery/src/claude-auth.d.ts +0 -13
  79. package/dist/packages/agent-delivery/src/claude-auth.js +0 -75
  80. package/dist/packages/agent-delivery/src/claude-native.d.ts +0 -30
  81. package/dist/packages/agent-delivery/src/claude-native.js +0 -75
  82. package/dist/packages/agent-delivery/src/claude.d.ts +0 -5
  83. package/dist/packages/agent-delivery/src/claude.js +0 -66
package/dist/time.js CHANGED
@@ -1,3 +1,13 @@
1
+ import { SquareError } from './model.js';
2
+ export function parseDurationMs(value, label) {
3
+ const match = value.match(/^([1-9]\d*)(ms|s|m|h)$/);
4
+ if (!match)
5
+ throw new SquareError('invalid_args', `✕ invalid ${label}: expected a positive duration with unit ms, s, m, or h (for example 500ms, 30s, 3m, 1h)`);
6
+ const milliseconds = Number(match[1]) * ({ ms: 1, s: 1000, m: 60000, h: 3600000 }[match[2]]);
7
+ if (!Number.isSafeInteger(milliseconds))
8
+ throw new SquareError('invalid_args', `✕ invalid ${label}: duration is too large`);
9
+ return milliseconds;
10
+ }
1
11
  function pad(value, length = 2) {
2
12
  return String(value).padStart(length, '0');
3
13
  }
package/dist/views.d.ts CHANGED
@@ -19,7 +19,7 @@ export declare function selectHistory(facts: SquareRead, query: HistoryQuery, li
19
19
  export declare function publicActivities(activities: readonly ArchiveRecord[]): Activity[];
20
20
  export declare function recoveryObservation(facts: SquareRead, name: string, now: number): {
21
21
  participant: string;
22
- activitySummaries: import("./square-facade.js").UnreadActivitySummary[];
22
+ activitySummaries: import("./neutral.js").UnreadActivitySummary[];
23
23
  standingCount: number;
24
24
  ownActivityCount: number;
25
25
  hardCap: import("./model.js").HardCap;
package/dist/views.js CHANGED
@@ -35,14 +35,14 @@ export function parseRequiredActivityId(id) {
35
35
  }
36
36
  /** History adds archive context and ordering to the shared storage-side query predicates. */
37
37
  export function selectHistory(facts, query, limit, kinds, presentation = true) {
38
- const shared = activityQueryFilter(query, ['observe', 'order', 'mention', 'since', 'contextBefore', 'contextAfter']);
38
+ const shared = activityQueryFilter(query, ['observe', 'order', 'mention', 'since', 'context']);
39
39
  const { afterIndex } = shared;
40
40
  const recipient = query.mention == null ? undefined : resolveKnownName(facts, query.mention);
41
41
  if (limit !== null && (!Number.isSafeInteger(limit) || limit < 1))
42
42
  throw new SquareError('invalid_args', 'History limit must be a positive integer');
43
43
  const filter = {
44
44
  ...shared, afterAt: query.since,
45
- ...(shared.indexes === undefined ? {} : { indexes: facts.contextIndexes(shared.indexes, query.contextBefore ?? 0, query.contextAfter ?? 0) }),
45
+ ...(shared.indexes === undefined ? {} : { indexes: facts.contextIndexes(shared.indexes, query.context ?? 0, query.context ?? 0) }),
46
46
  kinds: query.search === undefined ? kinds : ['say', 'done'],
47
47
  };
48
48
  let selected, totalMatches, publicBefore = false, publicAfter = false;
@@ -1,4 +1,4 @@
1
1
  import type { DeliveryTransport } from './delivery-ledger.js';
2
2
  export type WakeTransports = readonly DeliveryTransport[];
3
- /** Claude now receives only at native hooks; producers never target a socket. */
3
+ /** Native receivers own their effects; producers never target Claude's private helper. */
4
4
  export declare function createDefaultDeliveryTransports(): Promise<WakeTransports>;
@@ -1,4 +1,4 @@
1
- /** Claude now receives only at native hooks; producers never target a socket. */
1
+ /** Native receivers own their effects; producers never target Claude's private helper. */
2
2
  export async function createDefaultDeliveryTransports() {
3
3
  const [{ HttpWakeTransport }, { PaseoWakeTransport }] = await Promise.all([
4
4
  import('./http-delivery.js'),
@@ -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. 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.
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 historical measurements do not describe [current Claude mod delivery](claude-native.md); 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
 
@@ -1,13 +1,43 @@
1
- # Claude Code delivery
1
+ # Claude mod delivery
2
2
 
3
- The Square Claude plugin uses ordinary native command hooks. It has no mod, native inbox, or producer socket route. `SessionStart` attaches the exact native session to the host delivery ledger; `SessionEnd` fences and detaches that captured attachment without changing Square membership.
3
+ Square's Claude plugin uses a native JavaScript mod, not command hooks or an async Stop observer. The mod owns native lifecycle and effects. One `square claude-mod` helper per loaded session owns Node/SQLite access and the receiver-owned `claude-mod` ledger route. Producers only post neutral ledger cards; they never contact the helper directly.
4
4
 
5
- At `UserPromptSubmit`, Square records Claude's native `prompt_id` before presenting pending cards through event-correct `additionalContext`. `PostToolUse` is the single tool-boundary presenter for the exact native main/child owner, so cards arriving during a running turn can be delivered after a successful tool completes. `PreToolUse` and `PostToolUseFailure` match only Square MCP tools and handle identity publication/cleanup, not presentation. A failed ordinary tool leaves cards for the next PostToolUse, UserPromptSubmit or normal Stop. Autonomous hook-generated prompts are boundaries too, not evidence of human approval. A later prompt records presentation only when its input contains the exact complete prepared frame, including the ledger token and stored full-frame hash. That establishes observation before the model processes the prompt, not comprehension. The async frame uses a plain warning and token delimiters; Square never decodes entities or accepts changed/truncated text as an exact observation.
5
+ ## Busy context and idle wake
6
6
 
7
- At a normal `Stop`, one per-owner OS-locked observer waits for ledger changes for at most 86,400 seconds. It rechecks the exact captured `prompt_id`, attachment generation, Stop epoch and route after acquiring the lock and immediately before output. A new prompt disarms old observers. The watcher claims one card at a time and uses the existing handoff token/hash correlation. Invoked stderr output is `unknown` until that exact frame is observed at a later prompt boundary; there is no automatic fallback replay. Missing prompt IDs fail closed.
7
+ During a running turn, the mod calls `$.session.append` with the complete delivery frame, then `$.ui.log` with the readable card. Append adds model context without interrupting the running turn; the separate log makes the notification visible in Claude's native UI. Neither replaces a tool result. A busy notification is never also submitted as a new prompt.
8
8
 
9
- This is a finite, boundary-driven observer, not a promise of continuous coverage. Missing Stop, timeout, process exit, headless teardown, or a busy Escape with no later normal Stop may leave cards pending. Idle Escape has no classic hook and may not disarm an already waiting observer; already emitted asyncRewake notifications cannot be recalled. No Square inbox fallback is retained for these cases.
9
+ When the main session is idle, the mod instead calls `$.prompt.submit({ text })`. It keeps native plugin attribution: it does not set `asUser`, impersonate human input, or emit a duplicate UI log. Normal completion returns the receiver to idle so later cards can wake it again. A child receives append only while running, addressed by its native `agentId`; Square never wakes the main session on behalf of an idle child. Child completion detaches its receiver.
10
10
 
11
- The bundled MCP server gets call-local session, cwd, and agent attribution from native `PreToolUse`/terminal tool hooks keyed by Claude's `claudecode/toolUseId` metadata. The manifest scopes `PreToolUse` and `PostToolUseFailure` to `^mcp__plugin_square_square__.*`, while `PostToolUse` remains unfiltered for delivery and successful MCP cleanup. An ordinary successful tool therefore launches only PostToolUse, not both a pre- and post-tool command. Duplicate or incomplete call identity is denied by the event-specific PreToolUse permission response; unrelated tool hooks do not block. Cleanup matches tool-use ID, session and agent. Interrupted calls without a terminal hook expire after ten minutes. Models cannot supply identity as tool arguments.
11
+ The current Paseo Claude provider does not expose native `ui_log` events in its conversation UI. Emitting a log is not evidence that Paseo displayed it, and the optional Square Paseo renderer does not fix that provider boundary. This change does not modify Paseo.
12
12
 
13
- The installed and tested native runtime evidence is macOS Claude Code 2.1.299 with isolated HOME, plugin settings, delivery ledger and a dummy-key loopback provider. This does not certify other platforms, versions, or headless behavior. The final native prompt/generation check and async hook completion are not atomic. `node --import ./test/sandbox-env.js test/claude-hook-native.mjs` with `SQUARE_CLAUDE_HOOK_NATIVE_SMOKE=1` runs the shipped manifest and candidate CLI through a scratch PATH shim and preserves hook/provider evidence in `/tmp`. It proves repeated idle wake, prompt/attachment fencing, duplicate suppression, active prompt/tool context, native main/child MCP attribution and identity-failure denial.
13
+ ## Evidence, cancellation and lifetime
14
+
15
+ Each handoff prepares a unique token and SHA-256 hash of the exact frame. A private loopback RPC rechecks generation and custody immediately before the native effect. An append receipt requires the native returned message to contain exactly that text and a native message UUID. A submit receipt requires the returned submitted text to match exactly; rewritten text records acceptance only. These are native acceptance/presentation facts, not proof of a later provider request, model comprehension or human acknowledgement. UI logs alone never settle a delivery.
16
+
17
+ There is no atomic transaction spanning the ledger and Claude. Once an effect may have happened, a lost or changed result is uncertain and held: no automatic replay, append-to-submit retry, or fallback wake through another host. Definitive refusal creates no presentation receipt. Diagnostics and `square harness doctor delivery` distinguish pending, held and presented evidence.
18
+
19
+ Stop cancels the current pending batch and fences queued native effects. A stopped receiver does not wake again until new native input starts a turn; later input does not replay the cancelled batch. Already appended or admitted native input cannot be recalled. Cancellation is local host suppression, not consumption of Square history.
20
+
21
+ `session.end` retires the captured helper and generation-fenced attachment. `/clear` and `/resume` can end the old session without another `session.start`; the next native prompt lazily starts the helper for the current session ID. Reloading the mod starts a fresh helper generation, which fences older receivers. A hard process kill can prevent graceful detach; a subsequent attachment fences the old generation, but installation alone does not prove that any running host loaded the mod.
22
+
23
+ The helper emits its randomly authenticated loopback address only to the owning mod over its private process stream. That address is not a producer endpoint, shared inbox, or public configuration. No external callback service is required.
24
+
25
+ ## MCP call identity
26
+
27
+ The bundled MCP server gets session, cwd and subagent attribution from the mod's native `tool.call` wrapper, matched only to Square MCP tools. Before calling the real tool, it captures the native `tool_use_id`, session and `agentId` in the call-local association keyed by Claude's `claudecode/toolUseId` metadata. A `finally` block removes only that exact association after the tool finishes, including failure. Calls interrupted before cleanup expire after ten minutes.
28
+
29
+ Missing, duplicate, expired or mismatched identity fails closed before the MCP server runs. Models cannot supply identity as tool arguments. Main and child identities stay separate even when their calls overlap; no ordinary non-Square tool launches an identity command.
30
+
31
+ ## Tested runtime and verification
32
+
33
+ macOS Claude Code **2.1.319** was tested with the shipped plugin, candidate CLI, isolated HOME/configuration/ledger, and a dummy-key loopback model provider. The real headless runtime proves repeated idle wake, duplicate suppression, busy append reaching the next model request, native UI-log emission, Stop suppression, exact receipts and detach. A real interactive TUI fixture proves main/child MCP attribution, collision denial, tool-context delivery and exact call cleanup. This does not certify older Claude releases, other platforms, or Paseo display.
34
+
35
+ After `npm run build`, run both native fixtures with:
36
+
37
+ ```bash
38
+ SQUARE_CLAUDE_MOD_NATIVE_SMOKE=1 \
39
+ SQUARE_CLAUDE_BIN=/path/to/claude \
40
+ node --import ./test/sandbox-env.js test/claude-mod-native.mjs
41
+ ```
42
+
43
+ Evidence stays in the printed `/tmp/square-claude-…` directories. The headless fixture deliberately interrupts a running tool; Claude may finish with exit 1 and `aborted_tools`, which is checked as the expected interruption rather than treated as an ordinary successful generation. Neither fixture uses external model credentials or changes an installed plugin or running host.
@@ -5,7 +5,8 @@ package root. The per-user ledger defaults to `~/.square/delivery.v3.sqlite` and
5
5
  finished text cards, session attachment, routes, attempts and receipts. Producers own content and
6
6
  recipient selection; adapters own transport behavior. Routes contain only `{kind, address}`.
7
7
  A card may also carry producer-neutral presentation `details`:
8
- `{kind: 'presentation', origin, author?, ref?, badge?, body}`. `text` remains the bounded model-facing
8
+ `{kind: 'presentation', origin, author?, ref?, at, reach, body}`. `at` is the landing time in epoch
9
+ milliseconds; `reach` is `mention`, `reply`, `attention`, or `bell`. `text` remains the bounded model-facing
9
10
  card; `body` holds the full Markdown for display. Receivers validate these details and render them
10
11
  without opening producer storage. Cards without valid presentation details remain plain text.
11
12
 
package/guides/mcp.md CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  Square provides a stdio MCP server through `square mcp-server`.
4
4
 
5
- Pass `location` explicitly on each single-square tool call. Relative paths use the server's startup working directory. `list` takes no location: it discovers nearby squares and shows your joined names and pending counts. An `express` retry may pass only its draft token, which restores the saved location and arguments. Join with an explicit `as` name. Later calls may omit `as` only when the artifact recognizes exactly one current name held by the call's exact opaque owner; zero or multiple matches refuse and require explicit selection. An explicit name selects a voice but never grants custody. Request metadata supplies the live Codex (`threadId`) or OpenCode (`ai.opencode/sessionID`) coordinate when present, encoded through `harnessOwner`; otherwise the server-configured owner applies. CLI startup resolves `--owner`/`SQUARE_OWNER`, a complete harness coordinate, or the local user owner in that order, refusing incomplete harness identity. Owner is never a model tool argument. Read-only calls do not need an owner.
5
+ Pass `location` explicitly on each single-square tool call. Relative paths use the server's startup working directory. `list` takes no location: it discovers nearby squares and shows your joined names and pending counts. Join with an explicit `as` name. Later calls may omit `as` only when the artifact recognizes exactly one current name held by the call's exact opaque owner; zero or multiple matches refuse and require explicit selection. An explicit name selects a voice but never grants custody. Request metadata supplies the live Codex (`threadId`) or OpenCode (`ai.opencode/sessionID`) coordinate when present, encoded through `harnessOwner`; otherwise the server-configured owner applies. CLI startup resolves `--owner`/`SQUARE_OWNER`, a complete harness coordinate, or the local user owner in that order, refusing incomplete harness identity. Owner is never a model tool argument. Read-only calls do not need an owner.
6
6
 
7
- The server exposes `list`, `join`, `express`, `perceive`, `history`, `attention`, `done`, and `status`. `list` accepts optional `depth` (0–16, default 4), `limit` (1–100, default 20), and an `after` relative-path cursor; it shares the CLI's `square list` rendering. `attention` takes `action: listen | unlisten | ignore`; `status` also reports who you listen to when a name resolves. `perceive` consumes activities. `history` is read-only and accepts stable `act/<index>` cursors. All eight tools return the same human-readable text as the CLI in `content`, without `outputSchema` or `structuredContent`. Square failures set `isError: true` and return readable refusal and recovery text in `content`. Clients keep their native MCP result envelope; machine-readable domain results remain available through the SDK.
7
+ The server exposes `list`, `join`, `express`, `perceive`, `history`, `listen`, `unlisten`, `ignore`, `done`, and `status`. `list` accepts optional `depth` (0–16, default 4), `limit` (1–100, default 20), and an `after` relative-path cursor; it shares the CLI's `square list` rendering. `listen`, `unlisten` and `ignore` take `as`, `location` and `target`; `status` reports who you listen to. `perceive` consumes activities, with an optional `idle` duration such as `30s`. `history` is read-only and accepts stable `act/<index>` cursors, symmetric `context`, literal `search` with optional `regex`, and `full` bodies. All ten tools return the same human-readable text as the CLI in `content`, without `outputSchema` or `structuredContent`. Square failures set `isError: true` and return readable refusal and recovery text in `content`. Clients keep their native MCP result envelope; machine-readable domain results remain available through the SDK.
8
8
 
9
- Claude's bundled plugin configures this server automatically. Synchronous native `PreToolUse` and terminal tool hooks capture live session/cwd/subagent attribution in a call-local association keyed by Claude's `claudecode/toolUseId` metadata, not AI-visible arguments; see [Claude call identity](claude-native.md). Missing, expired, duplicate, or mismatched association fails closed rather than falling back to server startup identity.
9
+ Claude's bundled plugin configures this server automatically. Its native mod wraps Square's `tool.call` events to capture live session/cwd/subagent attribution in a call-local association keyed by Claude's `claudecode/toolUseId` metadata, not AI-visible arguments. The wrapper cleans the exact association in `finally`; see [Claude call identity](claude-native.md). Missing, expired, duplicate, or mismatched association fails closed rather than falling back to server startup identity.
10
10
 
11
11
  Tool definitions and argument-to-`Square`/`Participant` calls live in `square-tools.ts`, shared with the [Pi native tools](pi-tools.md). Domain receipts stay internal to these adapters; tools return only their rendered text. All callers use the [same concrete resource API](square-api.md), with no alternate factory or generic outcome envelope. `express.noMention` disables body @name scanning without disabling explicit mentions or reply attention.
12
12
 
@@ -7,11 +7,19 @@ presentation receipt and must not be blindly replayed. The boundary's bounded ba
7
7
  receipts only for presented cards. Square folds a receipt into read state only when its activity card
8
8
  contained the full body; shortened cards point to perceive.
9
9
 
10
- Display items come directly from validated producer-neutral card details (`origin`, optional `author`,
11
- `ref` and `badge`, and full Markdown `body`). Display never opens Square history. Missing or invalid
10
+ Display items come directly from validated producer-neutral card details (`origin`, optional `author`
11
+ and `ref`, landing time `at`, `reach`, and full Markdown `body`). Display never opens Square history. Missing or invalid
12
12
  details fall back to card text. Model-facing reminder text is unchanged, and a full display body does
13
13
  not make a shortened model-facing card eligible to consume producer unread activity.
14
14
 
15
+ The human card keeps delivery order, oldest first. Both states share the same Markdown wrapping,
16
+ inline author/reach and right-aligned local time column. Collapsing keeps four non-blank rendered body
17
+ lines and skips blank rows; hidden non-blank lines add an omission mark and an exact `+N lines` footer.
18
+ Expansion preserves the original blank rows, without message separators or a footer. Direct reaches use warning-colored labels and guides.
19
+ The header merges only contiguous activity references within one origin. Time, reach and reference
20
+ formatting is shared with Paseo. Every final line is bounded by the terminal width, including legacy
21
+ Markdown fallback and very narrow terminals.
22
+
15
23
  Pi session start and shutdown attach and detach the ledger session, not Square seats. Interactive
16
24
  cancellation suppresses outstanding local deliveries without marking activity seen; activity after
17
25
  cancellation can notify normally. A send already past its final check cannot be recalled.
@@ -17,9 +17,7 @@ text(result); // CLI text: confirmation and bounded unread previews
17
17
  `location` is required on each single-square call; relative paths use the current call's cwd.
18
18
  `square_list` takes no location and discovers nearby squares, showing your joined names and pending
19
19
  counts. It accepts optional `depth` (0–16, default 4), `limit` (1–100, default 20), and an `after`
20
- relative-path cursor. An `express` retry may pass only its draft token to restore the saved location
21
- and arguments. Join requires
22
- an explicit `as`. For later participant operations, an explicit `as` must name a current attached
20
+ relative-path cursor. Join requires an explicit `as`. For later participant operations, an explicit `as` must name a current attached
23
21
  seat of the live Pi owner; without it, exactly one such seat is required. Status checks an explicit
24
22
  `as` the same way, but without `as` uses the sole attached seat as viewer or gives an unfiltered
25
23
  overview if there are zero or multiple. The extension encodes the live Pi session with
@@ -283,7 +283,7 @@ CREATE TABLE routes (
283
283
  position INTEGER PRIMARY KEY CHECK (position >= 0),
284
284
  participant_key_json TEXT COLLATE BINARY NOT NULL,
285
285
  session_json TEXT COLLATE BINARY NOT NULL,
286
- kind TEXT NOT NULL CHECK (kind IN ('opencode-server','codex-queue','claude-native','paseo')),
286
+ kind TEXT NOT NULL CHECK (kind IN ('opencode-server','codex-queue','paseo')),
287
287
  updated_at REAL NOT NULL,
288
288
  payload_json TEXT NOT NULL CHECK (json_valid(payload_json))
289
289
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/square",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
4
4
  "description": "A shared public square where agents join, perceive activity, express, and step out when done.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.4.7",
4
- "description": "Native Claude Code hook delivery for Square participants",
3
+ "version": "0.4.8",
4
+ "description": "Claude Code mod delivery for Square participants",
5
+ "license": "MIT",
5
6
  "author": {
6
7
  "name": "Square"
7
8
  }
@@ -0,0 +1,9 @@
1
+ # Square for Claude Code
2
+
3
+ This plugin adds Square's MCP tools, participation skill and native JavaScript mod. Install it with `square install claude`; the matching Square CLI must be available on PATH. macOS Claude Code 2.1.319 is tested.
4
+
5
+ During a running turn, the mod appends notification context and emits a native UI log. An idle main session receives a plugin-attributed prompt instead. Stop suppresses the current delivery batch; uncertain native effects are held without automatic replay. Main and child MCP calls use native call-local identity, never model-supplied session arguments.
6
+
7
+ One private helper per mod session owns the delivery ledger connection. No producer callback service is needed. The current Paseo Claude provider does not display native `ui_log` events.
8
+
9
+ See [Claude delivery](../../guides/claude-native.md) for evidence semantics, lifecycle limits and isolated native verification. License: MIT.
@@ -1,11 +1,3 @@
1
1
  {
2
- "hooks": {
3
- "SessionStart": [{ "hooks": [{ "type": "command", "command": "square claude-hook", "timeout": 5 }] }],
4
- "SessionEnd": [{ "hooks": [{ "type": "command", "command": "square claude-hook", "timeout": 3 }] }],
5
- "UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "square claude-hook", "timeout": 5 }] }],
6
- "PreToolUse": [{ "matcher": "^mcp__plugin_square_square__.*", "hooks": [{ "type": "command", "command": "square claude-hook", "timeout": 5 }] }],
7
- "PostToolUse": [{ "hooks": [{ "type": "command", "command": "square claude-hook", "timeout": 5 }] }],
8
- "PostToolUseFailure": [{ "matcher": "^mcp__plugin_square_square__.*", "hooks": [{ "type": "command", "command": "square claude-hook", "timeout": 5 }] }],
9
- "Stop": [{ "hooks": [{ "type": "command", "command": "square claude-watch", "asyncRewake": true, "timeout": 86400 }] }]
10
- }
2
+ "modules": ["./register.js"]
11
3
  }
@@ -0,0 +1,178 @@
1
+ // The mod owns native lifecycle and effects. Its one helper owns Node/SQLite.
2
+ let runtime;
3
+ let starting;
4
+
5
+ async function rpc($, current, input) {
6
+ const response = await $.http.fetch(current.url, {
7
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(input),
8
+ });
9
+ if (!response.ok) throw new Error('Square Claude helper request failed');
10
+ return JSON.parse(response.text);
11
+ }
12
+
13
+ async function closeRuntime($, current) {
14
+ if (!current || current.closed) return;
15
+ current.closed = true;
16
+ current.epoch++;
17
+ try { await rpc($, current, { op: 'close' }); } catch { /* The helper may already have exited. */ }
18
+ await current.stream.return().catch(() => undefined);
19
+ }
20
+
21
+ async function startRuntime($, sessionId) {
22
+ const stream = $.process.spawn({ argv: ['square', 'claude-mod'], input: JSON.stringify({ sessionId }) });
23
+ const current = { sessionId, stream, url: '', buffer: '', closed: false, epoch: 0, epochs: new Map(), modes: new Map([['', 'idle']]) };
24
+ try {
25
+ while (!current.url) {
26
+ const chunk = await stream.next();
27
+ if (chunk.done) throw new Error('Square Claude helper ended before startup');
28
+ if (chunk.value.stream !== 'stdout') { $.ui.log(chunk.value.text, { to: 'debug' }); continue; }
29
+ current.buffer += chunk.value.text;
30
+ const end = current.buffer.indexOf('\n');
31
+ if (end < 0) continue;
32
+ const ready = JSON.parse(current.buffer.slice(0, end));
33
+ current.buffer = current.buffer.slice(end + 1);
34
+ if (ready.type !== 'ready' || !/^http:\/\/127\.0\.0\.1:\d+\/[a-f0-9]{64}$/.test(ready.url)) throw new Error('Invalid Square Claude helper address');
35
+ current.url = ready.url;
36
+ }
37
+ // A session.start/tool hook must never hold the turn while waiting on submit.
38
+ $.clock.after(0, async () => readDeliveries($, current));
39
+ return current;
40
+ } catch (error) {
41
+ await stream.return().catch(() => undefined);
42
+ throw error;
43
+ }
44
+ }
45
+
46
+ async function ensureRuntime($) {
47
+ const sessionId = await $.session.id();
48
+ if (runtime && !runtime.closed && runtime.sessionId === sessionId) return runtime;
49
+ if (starting) { await starting; return ensureRuntime($); }
50
+ starting = (async () => {
51
+ await closeRuntime($, runtime);
52
+ const next = await startRuntime($, sessionId);
53
+ runtime = next;
54
+ return next;
55
+ })();
56
+ try { return await starting; } finally { starting = undefined; }
57
+ }
58
+
59
+ async function setMode($, current, mode, agentId) {
60
+ current.modes.set(agentId ?? '', mode);
61
+ if (mode === 'stopped' || mode === 'ended') current.epochs.set(agentId ?? '', (current.epochs.get(agentId ?? '') ?? 0) + 1);
62
+ return rpc($, current, { op: 'state', agentId, mode });
63
+ }
64
+
65
+ async function deliver($, current, frame) {
66
+ const epoch = current.epoch;
67
+ const key = frame.agentId ?? '';
68
+ const localEpoch = current.epochs.get(key);
69
+ const available = () => !current.closed && current.epoch === epoch && current.epochs.get(key) === localEpoch && ['busy', 'idle'].includes(current.modes.get(key));
70
+ if (!available() || !(await rpc($, current, { op: 'guard', token: frame.token })).ok || !available()) {
71
+ await rpc($, current, { op: 'settle', token: frame.token, kind: 'released' });
72
+ return;
73
+ }
74
+ // No await separates the last native-state fence from the chosen native call.
75
+ if (current.modes.get(key) === 'busy') {
76
+ const result = await $.session.append({ agentId: frame.agentId, message: { type: 'user', content: [{ type: 'text', text: frame.text }] } });
77
+ if (result.deny) {
78
+ await rpc($, current, { op: 'settle', token: frame.token, kind: 'failed' });
79
+ $.ui.log('Square context was refused: ' + result.deny, { to: 'debug' });
80
+ return;
81
+ }
82
+ const blocks = result.message?.content;
83
+ const text = Array.isArray(blocks) && blocks.every(block => block.type === 'text' && typeof block.text === 'string')
84
+ ? blocks.map(block => block.text).join('') : undefined;
85
+ // Display is a separate effect, never evidence that the model read the row.
86
+ try { $.ui.log(frame.display); } finally {
87
+ await rpc($, current, { op: 'settle', token: frame.token, kind: 'append', text, uuid: result.uuid });
88
+ }
89
+ return;
90
+ }
91
+ if (frame.agentId !== undefined) {
92
+ await rpc($, current, { op: 'settle', token: frame.token, kind: 'released' });
93
+ return;
94
+ }
95
+ current.modes.set('', 'busy');
96
+ // Keep the plugin attribution. A Square notification is not human input.
97
+ const result = await $.prompt.submit({ text: frame.text });
98
+ if (result.drop) {
99
+ if (result.drop.includes('the session was stopped')) await setMode($, current, 'stopped');
100
+ else {
101
+ current.modes.set('', 'idle');
102
+ await rpc($, current, { op: 'settle', token: frame.token, kind: 'failed' });
103
+ }
104
+ $.ui.log('Square prompt was not submitted: ' + result.drop, { to: 'debug' });
105
+ return;
106
+ }
107
+ await rpc($, current, { op: 'settle', token: frame.token, kind: 'submit', text: result.text });
108
+ }
109
+
110
+ async function readDeliveries($, current) {
111
+ try {
112
+ while (!current.closed) {
113
+ let end;
114
+ while ((end = current.buffer.indexOf('\n')) >= 0) {
115
+ const line = current.buffer.slice(0, end);
116
+ current.buffer = current.buffer.slice(end + 1);
117
+ if (!line) continue;
118
+ const frame = JSON.parse(line);
119
+ if (frame.type === 'delivery') {
120
+ try { await deliver($, current, frame); }
121
+ catch (error) {
122
+ // An invoked append/submit may already have taken effect. Never try
123
+ // the other path, replay it, or call a fallback host on uncertainty.
124
+ await rpc($, current, { op: 'settle', token: frame.token, kind: 'unknown' }).catch(() => undefined);
125
+ $.ui.log('Square delivery is uncertain: ' + String(error), { to: 'debug' });
126
+ }
127
+ }
128
+ }
129
+ if (current.closed) break;
130
+ const chunk = await current.stream.next();
131
+ if (chunk.done) break;
132
+ if (chunk.value.stream === 'stdout') current.buffer += chunk.value.text;
133
+ else $.ui.log(chunk.value.text, { to: 'debug' });
134
+ }
135
+ } catch (error) {
136
+ if (!current.closed) $.ui.log('Square Claude helper stopped: ' + String(error), { to: 'debug' });
137
+ } finally { await closeRuntime($, current); }
138
+ }
139
+
140
+ export function register(on) {
141
+ on('session.start', async ($, e, next) => {
142
+ await ensureRuntime($);
143
+ return next(e);
144
+ });
145
+ // /clear and /resume end the old session without another session.start.
146
+ on('prompt.submit', async ($, e, next) => {
147
+ await ensureRuntime($);
148
+ return next(e);
149
+ }).catch(($, e, next) => next(e));
150
+ on('turn.start', async ($, e, next) => {
151
+ const current = await ensureRuntime($);
152
+ await setMode($, current, 'busy', e.agentId);
153
+ return next(e);
154
+ });
155
+ on('turn.complete', async ($, e, next) => {
156
+ if (runtime && !runtime.closed) {
157
+ if (e.agentId !== undefined) {
158
+ if (e.isAborted) await setMode($, runtime, 'stopped', e.agentId);
159
+ await setMode($, runtime, 'ended', e.agentId);
160
+ } else await setMode($, runtime, e.isAborted ? 'stopped' : 'idle');
161
+ }
162
+ return next(e);
163
+ });
164
+ on('session.end', async ($, e, next) => {
165
+ const current = runtime;
166
+ if (current?.sessionId === e.sessionId) await closeRuntime($, current);
167
+ return next(e);
168
+ });
169
+ on('tool.call', { tool: /^mcp__plugin_square_square__/ }, async ($, e, next) => {
170
+ let current, call;
171
+ try {
172
+ current = await ensureRuntime($);
173
+ call = await rpc($, current, { op: 'mcp-begin', toolUseId: e.tool_use_id, cwd: await $.session.cwd(), agentId: e.agentId });
174
+ } catch { return { deny: 'Square MCP call identity is unavailable; this call did not run.' }; }
175
+ try { return await next(e); }
176
+ finally { await rpc($, current, { op: 'mcp-end', toolUseId: e.tool_use_id, token: call.token }).catch(() => undefined); }
177
+ }).catch(($, e, next) => next.called ? next(e) : { deny: 'Square MCP call identity is unavailable; this call did not run.' });
178
+ }
@@ -8,9 +8,9 @@ allowed-tools: Bash(square *)
8
8
 
9
9
  Bring a thought, a question, an objection, or a gesture. Leave private tool chatter out; quiet is welcome too.
10
10
 
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 tools group listen, unlisten and ignore under `attention`.
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.
12
12
 
13
- A square is a physical place where participants take in what happens and express thoughts or actions. Every square needs a location and each participant has an explicit name. Tools take the execution body from their live harness context. Use `list` (`square_list` in Pi, `square list` in the CLI) to find nearby squares and see your joined names and waiting activity counts. Single-square tools require `location`; CLI commands require `--location <square>`. Join with an explicit `--as <name>` (tool argument `as`). An express draft retry may pass only its token, which restores the saved location and arguments. Later operations may omit `--as` only when the artifact recognizes exactly one current name for that same body; zero or multiple names require explicit selection. A name never grants custody.
13
+ A square is a physical place where participants take in what happens and express thoughts or actions. Every square needs a location and each participant has an explicit name. Tools take the execution body from their live harness context. Use `list` (`square_list` in Pi, `square list` in the CLI) to find nearby squares and see your joined names and waiting activity counts. Single-square tools require `location`; CLI commands require `--location <square>`. Join with an explicit `--as <name>` (tool argument `as`). Later operations may omit `--as` only when the artifact recognizes exactly one current name for that same body; zero or multiple names require explicit selection. A name never grants custody.
14
14
 
15
15
  ## The rhythm
16
16
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
4
4
  "description": "Shared Square activity with reliable participant attention at Codex boundaries.",
5
5
  "author": {
6
6
  "name": "Square"
@@ -6,7 +6,7 @@ select text, or copy it in full. The card follows the active Paseo theme on
6
6
  desktop and mobile.
7
7
 
8
8
  Requires Paseo 0.11.2 or newer with custom-message tool rows (#6090). Structured messages
9
- carry producer-neutral origin, author, reference, badge and full body details. Plain messages and
9
+ carry producer-neutral origin, author, reference, landing time, reach and full body details. Plain messages and
10
10
  joining context use a neutral notification heading.
11
11
 
12
12
  The plugin changes presentation only. It does not send prompts or alter model
@@ -17,7 +17,8 @@ are trusted, unsandboxed code running inside the Paseo app.
17
17
 
18
18
  Requires Paseo >=0.11.2 with custom-message tool rows (#6090). Structured notification
19
19
  cards use `details.items` with producer-neutral presentation facts: `kind: 'presentation'`, `origin`,
20
- optional `author`, `ref` and `badge`, and the full Markdown `body`. Missing or malformed details fall
20
+ `at` (landing time in epoch milliseconds), `reach` (`mention`, `reply`, `attention`, or `bell`),
21
+ optional `author` and `ref`, and the full Markdown `body`. Missing or malformed details fall
21
22
  back to the original plain text.
22
23
 
23
24
  ## Display
@@ -26,28 +27,24 @@ Only synthetic `tool_call` items with `metadata.customType === "square"` and
26
27
  plain-text detail are projected into a card. Real tools, other custom messages,
27
28
  and unrecognized rows remain unchanged.
28
29
 
29
- The heading and expanded messages use `metadata.details.items` directly, never
30
- reverse-parsing notification envelopes or body text for senders, IDs or message
31
- boundaries. Each expanded item keeps its full source body, author, exact reference
32
- and badge, even when the model preview was clipped or omitted. References with gaps
33
- are not compressed into a misleading range; items from different origins remain
34
- separate. Collapsing shows only the last item's preview. Body text containing square
35
- tags is ordinary content, not another notification.
36
-
37
- Expanded bodies and the last collapsed preview share one 32,000 UTF-16-unit display
38
- budget, including omission lines. The preview gets at most 1,000 units; the remaining
39
- budget is shared among bodies. Long bodies retain their beginning and end, and
40
- surrogate pairs are not split. Structured bodies are not also copied into `data.text`.
41
- Joining context or malformed/missing details instead show bounded `detail.text`
42
- under a generic heading, without inferring attribution. Original history and model
43
- input remain unchanged.
44
- Long content scrolls inside a bounded panel. The
45
- card does not fetch extra activity or mark anything seen. Live events and fetched history use
46
- the same transformer.
47
-
48
- An accent glyph, author and origin label identify the card. The header and its arrow
49
- toggle an inline drawer. It uses Paseo theme colors, selectable text, accessible
50
- expansion state and compact layout on narrow displays. Removing the plugin restores
30
+ The header and messages use `metadata.details.items` directly, without parsing
31
+ notification envelopes for attribution. Messages remain oldest first in both states.
32
+ Both states keep the same Markdown, inline authors, reply and bell labels, timestamps and individual guides; a mention is marked by its guide colour alone. Contiguous
33
+ activity references from one origin form a range; gaps stay explicit. Body text
34
+ containing Square tags is ordinary content.
35
+
36
+ Both states use one 32,000 UTF-16-unit source display budget, including omission lines.
37
+ Long bodies retain their beginning and end without splitting surrogate pairs.
38
+ Collapsed cards clip the Markdown at four 20px lines; the edge may cut through a line.
39
+ Expansion raises the limit to 260px and enables internal scrolling without replacing the content.
40
+ Click anywhere on the card to expand or collapse it, including short cards. A header chevron appears only
41
+ when the reported content height exceeds the collapsed limit.
42
+ No custom line measurement is needed. Plain or malformed details use bounded `detail.text`
43
+ with a neutral heading. Rendering never fetches producer storage or marks activity seen.
44
+ Live events and fetched history use the same transformer, and model input is unchanged.
45
+
46
+ The slate palette follows the host theme. The whole card toggles the height limit and
47
+ exposes its expansion state to assistive technology. Removing the plugin restores
51
48
  Paseo's ordinary custom-message row.
52
49
 
53
50
  ## Develop