@astrosheep/square 0.3.9 → 0.3.11

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 (43) hide show
  1. package/codex-plugin/.codex-plugin/plugin.json +1 -1
  2. package/dist/activity.js +4 -0
  3. package/dist/artifact.js +33 -2
  4. package/dist/cli/context.js +1 -1
  5. package/dist/cli/maintenance-commands.js +12 -1
  6. package/dist/cli/observation-commands.js +3 -16
  7. package/dist/cli/program.js +0 -4
  8. package/dist/cli/square-commands.js +23 -3
  9. package/dist/cmd/notify-once.js +5 -15
  10. package/dist/decisions.js +10 -2
  11. package/dist/delivery-health.js +55 -136
  12. package/dist/doctor.js +1 -0
  13. package/dist/file-lock.js +112 -0
  14. package/dist/harness-codex.js +35 -29
  15. package/dist/harness-links.js +0 -3
  16. package/dist/harness-pi.js +57 -0
  17. package/dist/harness.js +10 -15
  18. package/dist/help.js +8 -8
  19. package/dist/index.js +5 -1
  20. package/dist/model.js +4 -0
  21. package/dist/notifications.js +205 -28
  22. package/dist/paseo-connection.js +135 -0
  23. package/dist/paseo-delivery.js +73 -144
  24. package/dist/paseo-state.js +1 -1
  25. package/dist/paseo-timeline.js +32 -42
  26. package/dist/presentation.js +2 -2
  27. package/dist/presented.js +10 -72
  28. package/dist/registry.js +23 -24
  29. package/dist/routes.js +153 -0
  30. package/dist/square-application.js +47 -49
  31. package/dist/stream.js +1 -1
  32. package/dist/wake-attempts.js +171 -0
  33. package/dist/wake-evidence.js +35 -0
  34. package/dist/wake-port.js +22 -0
  35. package/dist/wake-sink.js +45 -6
  36. package/dist/watch.js +1 -2
  37. package/guides/participant.md +1 -1
  38. package/package.json +6 -1
  39. package/skills/brainstorm/SKILL.md +24 -24
  40. package/skills/square/.claude-plugin/plugin.json +1 -1
  41. package/skills/square/SKILL.md +4 -3
  42. package/skills/square-feedback/SKILL.md +2 -2
  43. package/dist/notification-failures.js +0 -54
@@ -0,0 +1,171 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { withFileLockSync } from './file-lock.js';
5
+ import { isWakeRouteKind, nameKey } from './model.js';
6
+ import { canonicalSquarePath } from './registry.js';
7
+ const RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
8
+ const LOCK_STALE_MS = 5 * 60 * 1000;
9
+ const LOCK_RETRY_MS = 10;
10
+ const VALID_OUTCOMES = new Set(['accepted', 'unknown', 'failed']);
11
+ export function wakeAttemptsPath(env = process.env) {
12
+ return env.SQUARE_WAKE_ATTEMPTS || path.join(os.homedir(), '.square', 'wake-attempts.ndjsonl');
13
+ }
14
+ export function wakeAttentionKey(attention) {
15
+ return JSON.stringify([canonicalSquarePath(attention.squarePath), `act_${attention.actIndex}`, nameKey(attention.recipient)]);
16
+ }
17
+ function parseRow(raw, now) {
18
+ let value;
19
+ try {
20
+ value = JSON.parse(raw);
21
+ }
22
+ catch {
23
+ return undefined;
24
+ }
25
+ if (value === null || typeof value !== 'object')
26
+ return undefined;
27
+ const row = value;
28
+ if (row.v !== 1 || typeof row.ts !== 'number' || !Number.isFinite(row.ts) || row.ts > now || now - row.ts > RETENTION_MS ||
29
+ row.attention === undefined || typeof row.attention.square_path !== 'string' || row.attention.square_path === '' ||
30
+ typeof row.attention.act_id !== 'string' || !/^act_\d+$/.test(row.attention.act_id) ||
31
+ typeof row.attention.recipient !== 'string' || row.attention.recipient === '' ||
32
+ typeof row.outcome !== 'string' || !VALID_OUTCOMES.has(row.outcome) ||
33
+ typeof row.attempt_n !== 'number' || !Number.isInteger(row.attempt_n) || row.attempt_n <= 0 ||
34
+ !isWakeRouteKind(row.route_kind) ||
35
+ (row.signature !== undefined && typeof row.signature !== 'string') ||
36
+ (row.outcome !== 'accepted' && (typeof row.signature !== 'string' || row.signature === '')) ||
37
+ (row.message !== undefined && typeof row.message !== 'string'))
38
+ return undefined;
39
+ return row;
40
+ }
41
+ function readRowsFromFile(filePath, now) {
42
+ let raw;
43
+ try {
44
+ raw = fs.readFileSync(filePath, 'utf8');
45
+ }
46
+ catch (error) {
47
+ if (error.code === 'ENOENT')
48
+ return [];
49
+ throw error;
50
+ }
51
+ return raw.split('\n').filter(Boolean).map((line) => parseRow(line, now)).filter((row) => row !== undefined);
52
+ }
53
+ function readRows(env, now) {
54
+ return readRowsFromFile(wakeAttemptsPath(env), now);
55
+ }
56
+ function writeRows(filePath, rows) {
57
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
58
+ const temporary = `${filePath}.${process.pid}.${Date.now()}.tmp`;
59
+ fs.writeFileSync(temporary, rows.map((row) => JSON.stringify(row)).join('\n') + (rows.length ? '\n' : ''), {
60
+ mode: 0o600,
61
+ });
62
+ fs.renameSync(temporary, filePath);
63
+ }
64
+ function fromRow(row) {
65
+ return {
66
+ at: row.ts,
67
+ attention: {
68
+ squarePath: canonicalSquarePath(row.attention.square_path),
69
+ actIndex: Number(row.attention.act_id.slice(4)),
70
+ recipient: row.attention.recipient,
71
+ },
72
+ routeKind: row.route_kind,
73
+ outcome: row.outcome,
74
+ ...(row.signature === undefined ? {} : { signature: row.signature }),
75
+ attemptN: row.attempt_n,
76
+ ...(row.message === undefined ? {} : { message: row.message }),
77
+ ...(row.diagnostic === undefined ? {} : { diagnostic: row.diagnostic }),
78
+ };
79
+ }
80
+ export function readWakeAttempts(opts = {}) {
81
+ const now = opts.now ?? Date.now();
82
+ const expected = opts.attention === undefined ? undefined : wakeAttentionKey(opts.attention);
83
+ return readRows(opts.env ?? process.env, now)
84
+ .map(fromRow)
85
+ .filter((attempt) => expected === undefined || wakeAttentionKey(attempt.attention) === expected);
86
+ }
87
+ export function terminalWakeEvidence(attempts) {
88
+ return attempts.findLast((attempt) => attempt.outcome === 'accepted' || attempt.outcome === 'unknown');
89
+ }
90
+ export function terminalWakeAttempt(attention, opts = {}) {
91
+ return terminalWakeEvidence(readWakeAttempts({ attention, ...opts }));
92
+ }
93
+ export function isWakeRouteAttemptable(route, attempts) {
94
+ if (terminalWakeEvidence(attempts) !== undefined)
95
+ return false;
96
+ const failed = attempts.findLast((attempt) => attempt.routeKind === route.kind && attempt.outcome === 'failed');
97
+ return failed === undefined || route.updatedAt > failed.at;
98
+ }
99
+ export function hasAttemptableWakeRoute(routes, attempts) {
100
+ return routes.some((route) => isWakeRouteAttemptable(route, attempts));
101
+ }
102
+ export function nextWakeAttemptNumber(attention, opts = {}) {
103
+ return readWakeAttempts({ attention, ...opts }).reduce((highest, attempt) => Math.max(highest, attempt.attemptN), 0) + 1;
104
+ }
105
+ function redact(value, secret) {
106
+ if (typeof value === 'string') {
107
+ const withoutKnownSecret = secret ? value.split(secret).join('[redacted]') : value;
108
+ return withoutKnownSecret.replace(/([?&]password=)[^&\s]+/gi, '$1[redacted]');
109
+ }
110
+ if (Array.isArray(value))
111
+ return value.map((item) => redact(item, secret));
112
+ if (value !== null && typeof value === 'object') {
113
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redact(item, secret)]));
114
+ }
115
+ return value;
116
+ }
117
+ function toRow(attempt, env) {
118
+ const safe = redact(attempt, env.PASEO_PASSWORD);
119
+ return {
120
+ v: 1,
121
+ ts: safe.at,
122
+ attention: {
123
+ square_path: canonicalSquarePath(safe.attention.squarePath),
124
+ act_id: `act_${safe.attention.actIndex}`,
125
+ recipient: safe.attention.recipient,
126
+ },
127
+ route_kind: safe.routeKind,
128
+ outcome: safe.outcome,
129
+ ...(safe.signature === undefined ? {} : { signature: safe.signature }),
130
+ attempt_n: safe.attemptN,
131
+ ...(safe.message === undefined ? {} : { message: safe.message }),
132
+ ...(safe.diagnostic === undefined ? {} : { diagnostic: safe.diagnostic }),
133
+ };
134
+ }
135
+ export function recordWakeAttempt(attempt, env = process.env) {
136
+ const value = { ...attempt, at: attempt.at ?? Date.now() };
137
+ if (!isWakeRouteKind(value.routeKind))
138
+ throw new Error('Wake attempts require a real adapter route kind.');
139
+ if (value.outcome !== 'accepted' && !value.signature) {
140
+ throw new Error(`${value.outcome} wake attempts require a transport signature.`);
141
+ }
142
+ const file = wakeAttemptsPath(env);
143
+ withFileLockSync(`${file}.lock`, { retryMs: LOCK_RETRY_MS, staleMs: LOCK_STALE_MS }, () => {
144
+ writeRows(file, [...readRowsFromFile(file, value.at), toRow(value, env)]);
145
+ });
146
+ return value;
147
+ }
148
+ export function recordRecoveredUnknown(attention, lease, env = process.env, at = Date.now()) {
149
+ const routeKind = lease.routeKind;
150
+ if (lease.attemptN === undefined || !isWakeRouteKind(routeKind))
151
+ return undefined;
152
+ const file = wakeAttemptsPath(env);
153
+ return withFileLockSync(`${file}.lock`, { retryMs: LOCK_RETRY_MS, staleMs: LOCK_STALE_MS }, () => {
154
+ const rows = readRowsFromFile(file, at);
155
+ const attempts = rows.map(fromRow).filter((attempt) => wakeAttentionKey(attempt.attention) === wakeAttentionKey(attention));
156
+ const terminal = terminalWakeEvidence(attempts);
157
+ if (terminal !== undefined)
158
+ return terminal;
159
+ const value = {
160
+ at,
161
+ attention,
162
+ routeKind,
163
+ outcome: 'unknown',
164
+ signature: 'worker_interrupted_during_dispatch',
165
+ attemptN: lease.attemptN,
166
+ message: 'The notification worker ended after dispatch began; transport acceptance is unknown.',
167
+ };
168
+ writeRows(file, [...rows, toRow(value, env)]);
169
+ return value;
170
+ });
171
+ }
@@ -0,0 +1,35 @@
1
+ import { loadSquare } from './artifact.js';
2
+ import { isDeliveryDelivered } from './delivery.js';
3
+ import { hasPresentedAttention } from './presented.js';
4
+ import { lookupParticipant } from './registry.js';
5
+ import { isCurrentlyJoined } from './runtime.js';
6
+ import { readWakeRoutes } from './routes.js';
7
+ import { isWakeRouteAttemptable, readWakeAttempts, terminalWakeEvidence, } from './wake-attempts.js';
8
+ export function joinedRecipients(doc) {
9
+ return [...new Set(doc.acts.filter((act) => act.kind === 'join').map((act) => act.actor))]
10
+ .filter((name) => isCurrentlyJoined(doc.acts, name));
11
+ }
12
+ /** Project every wake decision from the same primary evidence. */
13
+ export function wakeEvidence(squarePath, recipient, actIndex, now, env) {
14
+ const doc = loadSquare(squarePath);
15
+ const owners = new Set(lookupParticipant(squarePath, recipient, now).map((binding) => binding.ownerId));
16
+ const attempts = readWakeAttempts({ attention: { squarePath, recipient, actIndex }, env, now });
17
+ const terminal = terminalWakeEvidence(attempts);
18
+ const routes = readWakeRoutes({ freshOnly: true, now, env })
19
+ .filter((route) => owners.has(route.ownerId));
20
+ return {
21
+ delivered: isDeliveryDelivered(doc, recipient, actIndex),
22
+ presented: hasPresentedAttention(squarePath, recipient, actIndex, env, now),
23
+ attempts,
24
+ ...(terminal === undefined ? {} : { terminal }),
25
+ attemptableRoutes: terminal === undefined
26
+ ? routes.filter((route) => isWakeRouteAttemptable(route, attempts))
27
+ : [],
28
+ };
29
+ }
30
+ export function wakeIsEligible(evidence) {
31
+ return !evidence.delivered
32
+ && !evidence.presented
33
+ && evidence.terminal === undefined
34
+ && evidence.attemptableRoutes.length > 0;
35
+ }
@@ -0,0 +1,22 @@
1
+ /** Select routes globally; adapters own only transport-specific live proof and dispatch. */
2
+ export class WakePort {
3
+ adapters;
4
+ constructor(adapters) {
5
+ this.adapters = new Map(adapters.map((adapter) => [adapter.kind, adapter]));
6
+ }
7
+ async dispatch(routes, payload, hooks) {
8
+ for (const route of routes) {
9
+ const adapter = this.adapters.get(route.kind);
10
+ if (adapter === undefined)
11
+ continue;
12
+ const attemptN = hooks.nextAttemptN();
13
+ const result = await adapter.dispatch(route.address, payload, () => hooks.beforeSend(route, attemptN));
14
+ if (result.outcome === 'cancelled')
15
+ return result;
16
+ await hooks.record(route, attemptN, result);
17
+ if (result.outcome === 'accepted' || result.outcome === 'unknown')
18
+ return { outcome: result.outcome };
19
+ }
20
+ return { outcome: 'exhausted' };
21
+ }
22
+ }
package/dist/wake-sink.js CHANGED
@@ -1,8 +1,47 @@
1
1
  import { spawnSync } from 'node:child_process';
2
- export function sendPaseoWake({ agentId, prompt }) {
3
- const result = spawnSync(process.env.SQUARE_PASEO_BIN || 'paseo', ['send', agentId, '--prompt', prompt, '--no-wait'], { stdio: 'ignore', timeout: 5000, env: process.env });
4
- if (result.error)
5
- throw result.error;
6
- if (result.status !== 0)
7
- throw new Error(`paseo send exited with ${result.status ?? 'no status'}`);
2
+ export class PaseoWakeSendError extends Error {
3
+ kind;
4
+ constructor(message, kind) {
5
+ super(message);
6
+ this.kind = kind;
7
+ this.name = 'PaseoWakeSendError';
8
+ }
9
+ }
10
+ function commandError(output) {
11
+ try {
12
+ const parsed = JSON.parse(output);
13
+ if (parsed.error === undefined)
14
+ return undefined;
15
+ return {
16
+ ...(typeof parsed.error.code === 'string' ? { code: parsed.error.code } : {}),
17
+ message: typeof parsed.error.message === 'string' ? parsed.error.message : output.trim(),
18
+ };
19
+ }
20
+ catch {
21
+ return undefined;
22
+ }
23
+ }
24
+ function classifyCommandFailure(code, message) {
25
+ if (code === 'DAEMON_NOT_RUNNING' || /ECONNREFUSED|ENOENT|not found.*executable/i.test(message))
26
+ return 'transient';
27
+ if (/password|auth|unauthori[sz]ed|agent not found|rejected/i.test(message))
28
+ return 'rejected';
29
+ return 'unknown';
30
+ }
31
+ function redactUriPassword(value) {
32
+ return value.replace(/([?&]password=)[^&\s]+/gi, '$1[redacted]');
33
+ }
34
+ export function sendPaseoWake({ agentId, prompt }, opts = {}) {
35
+ const result = spawnSync(process.env.SQUARE_PASEO_BIN || 'paseo', ['send', agentId, '--prompt', prompt, '--no-wait', '--json'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: opts.timeoutMs ?? 5000, env: process.env });
36
+ if (result.error) {
37
+ const code = result.error.code;
38
+ const kind = code === 'ENOENT' || code === 'ECONNREFUSED' ? 'transient' : 'unknown';
39
+ throw new PaseoWakeSendError(result.error.message, kind);
40
+ }
41
+ if (result.status === 0)
42
+ return;
43
+ const output = `${result.stderr ?? ''}${result.stdout ?? ''}`;
44
+ const failure = commandError(output);
45
+ const message = redactUriPassword(failure?.message || output.trim() || `paseo send exited with ${result.status ?? 'no status'}`);
46
+ throw new PaseoWakeSendError(message, classifyCommandFailure(failure?.code, message));
8
47
  }
package/dist/watch.js CHANGED
@@ -3,12 +3,11 @@ import { loadSquare } from './artifact.js';
3
3
  import { SquareError, nameKey, } from './model.js';
4
4
  import { markDeliveredNotifications } from './delivery.js';
5
5
  import { SLEEP_MS, STALE_MS, WATCH_HEARTBEAT_MS, WATCH_STALE_MS, countSays, currentHold, doneNames, freshWatchLease, hasQuorum, inSquareCount, nowMs, touchPresenceCursor, writeWatchLease, } from './runtime.js';
6
- import { withSquareLock, writeSquareDoc } from './square-application.js';
6
+ import { execute, withSquareLock, writeSquareDoc } from './square-application.js';
7
7
  import { renderWatchForceTakeover, renderWatchAlreadyActive, renderWatchOutput, renderWatchReplaced, renderWatchStatus, participantCommandPrefix, withPathOutput, } from './presentation.js';
8
8
  import { ackPeerDelta, deliveryDelta, filteredPeerActivities, filteredRoomChanges, matchesFeedFilter, peerPublicActs, peerRoomChanges, } from './activity-feed.js';
9
9
  import { coreParticipants, resolveKnownName } from './decisions.js';
10
10
  import { hasAutomaticDeliveryIdentity, localParticipantOwner } from './registry.js';
11
- import { execute } from './square-application.js';
12
11
  function catchDelta(doc, name) {
13
12
  return deliveryDelta(doc, name);
14
13
  }
@@ -30,7 +30,7 @@
30
30
 
31
31
  "哦对。一个名字只在广场上占一个位置。换了 harness session,再跑一次 `join` 会把新 session 接回原来的位置,不会制造第二个你。"
32
32
 
33
- 默认广场在 `.square/SQUARE.md`。不在?所有命令自己带 `--square-path <path>`。别站着等人把路喂到你嘴边,恶心。
33
+ 默认广场在 `.square/SQUARE.md`。不在?所有命令自己带 `--location <path>`。别站着等人把路喂到你嘴边,恶心。
34
34
 
35
35
  ### 你的声音,和你的身体——express
36
36
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/square",
3
- "version": "0.3.9",
3
+ "version": "0.3.11",
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": {
@@ -38,6 +38,7 @@
38
38
  "license": "MIT",
39
39
  "devDependencies": {
40
40
  "@types/node": "^22",
41
+ "@types/ws": "^8.18.1",
41
42
  "typescript": "^5.8"
42
43
  },
43
44
  "exports": {
@@ -55,5 +56,9 @@
55
56
  "skills": [
56
57
  "./skills"
57
58
  ]
59
+ },
60
+ "dependencies": {
61
+ "@getpaseo/client": "^0.3.1",
62
+ "ws": "^8.21.3"
58
63
  }
59
64
  }
@@ -7,7 +7,7 @@ allowed-tools: Bash(square *), Skill(square)
7
7
  # Square Brainstorm
8
8
 
9
9
  Use this skill when you are coordinating a brainstorm. Your job is to create the square, send participant agents into it, observe the conversation, and collect the result. Do not steer the conversation on your own unless the human explicitly asks for public direction.
10
- Commands default to `.square/SQUARE.md`; use `--square-path <path>` when you want a different file.
10
+ Commands default to `.square/SQUARE.md`; use `--location <path>` when you want a different file.
11
11
 
12
12
  ## Build
13
13
 
@@ -37,27 +37,27 @@ Send each participant agent this prompt. Replace `<name>` and `<path>`, but do n
37
37
  You are <name>, participating in a brainstorm. The square file is at <path>.
38
38
 
39
39
  First action: enter the square. Read the context, warmup, and recent activity printed by this command before expressing:
40
- square --square-path <path> --as <name> join
40
+ square --location <path> --as <name> join
41
41
 
42
42
  Then follow the Happy Path from the join output. Core commands:
43
- square --square-path <path> --as <name> express - <<'EOF'
43
+ square --location <path> --as <name> express - <<'EOF'
44
44
  ...
45
45
  EOF
46
- square --square-path <path> --as <name> catch --mention --idle 10m
47
- square --square-path <path> --as <name> catch --now
48
- square --square-path <path> --as <name> catch --idle 10m
49
- square --square-path <path> history --limit 80
50
- square --square-path <path> history --from <name> --limit 80
51
- square --square-path <path> status
52
- square --square-path <path> --as <name> done - <<'EOF'
46
+ square --location <path> --as <name> catch --mention --idle 10m
47
+ square --location <path> --as <name> catch --now
48
+ square --location <path> --as <name> catch --idle 10m
49
+ square --location <path> history --limit 80
50
+ square --location <path> history --from <name> --limit 80
51
+ square --location <path> status
52
+ square --location <path> --as <name> done - <<'EOF'
53
53
  ...
54
54
  EOF
55
55
 
56
- For complete history: square --square-path <path> history --all --full
56
+ For complete history: square --location <path> history --all --full
57
57
 
58
58
  If you are addressing a specific participant, write @name. Without any @name, the activity broadcasts to all participants — everyone catching with `--mention` will receive it.
59
59
 
60
- If an activity is refused because something happened while the participant was not looking, run `square --square-path <path> --as <name> catch --now`, take it in, then express again. `catch --now` catches up without waiting.
60
+ If an activity is refused because something happened while the participant was not looking, run `square --location <path> --as <name> catch --now`, take it in, then express again. `catch --now` catches up without waiting.
61
61
  ```
62
62
 
63
63
  Need another voice later? Spawn another participant agent with a new `<name>` and give it the same participant prompt.
@@ -67,12 +67,12 @@ Need another voice later? Spawn another participant agent with a new `<name>` an
67
67
  If you or the human want to participate, choose a participant name and use the participant loop:
68
68
 
69
69
  ```bash
70
- square --square-path <path> --as <name> join
71
- square --square-path <path> --as <name> express - <<'EOF'
70
+ square --location <path> --as <name> join
71
+ square --location <path> --as <name> express - <<'EOF'
72
72
  your view
73
73
  EOF
74
- square --square-path <path> --as <name> catch --idle 10m
75
- square --square-path <path> --as <name> done - <<'EOF'
74
+ square --location <path> --as <name> catch --idle 10m
75
+ square --location <path> --as <name> done - <<'EOF'
76
76
  final note
77
77
  EOF
78
78
  ```
@@ -82,9 +82,9 @@ EOF
82
82
  Use these to check progress:
83
83
 
84
84
  ```bash
85
- square --square-path <path> history --limit 50
86
- square --square-path <path> history --from <name>
87
- square --square-path <path> status
85
+ square --location <path> history --limit 50
86
+ square --location <path> history --from <name>
87
+ square --location <path> status
88
88
  ```
89
89
 
90
90
  `history` reads past public activity without advancing participant presence. `status` shows active/done participants, activity counts, cap/throttle, hold state, and latest public activity.
@@ -96,7 +96,7 @@ When addressing a specific participant, use `@name`; without any `@name`, the ac
96
96
  If the human wants to refocus the square, add a constraint, ask a convergence question, or correct its direction, write that direction publicly with a participant name:
97
97
 
98
98
  ```bash
99
- square --square-path <path> --as <name> express - <<'EOF'
99
+ square --location <path> --as <name> express - <<'EOF'
100
100
  Refocus on <specific direction, constraint, question, or decision needed>.
101
101
  EOF
102
102
  ```
@@ -106,8 +106,8 @@ Do not add direction on your own. If you notice the square drifting or stuck, re
106
106
  Pause the participant loop when a human needs time to read, think, or add another voice:
107
107
 
108
108
  ```bash
109
- square --square-path <path> hold "human reading"
110
- square --square-path <path> resume
109
+ square --location <path> hold "human reading"
110
+ square --location <path> resume
111
111
  ```
112
112
 
113
113
  While held, participant expression and catch pause. Join, done, status, and history still work.
@@ -117,8 +117,8 @@ While held, participant expression and catch pause. Join, done, status, and hist
117
117
  When participants are done, collect the public activities:
118
118
 
119
119
  ```bash
120
- square --square-path <path> history --all --full # complete public history
121
- square --square-path <path> status
120
+ square --location <path> history --all --full # complete public history
121
+ square --location <path> status
122
122
  ```
123
123
 
124
124
  ## Boundaries
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.9",
3
+ "version": "0.3.11",
4
4
  "description": "Native Claude Code turn-boundary delivery for Square participants",
5
5
  "author": {
6
6
  "name": "Square"
@@ -19,7 +19,7 @@ join once → catch ↔ express → done
19
19
  square --as <name> join
20
20
  ```
21
21
 
22
- Read the current context and what happened recently before expressing. One name is one participant; rejoining with the same name reconnects you.
22
+ Read the current context and what happened recently before expressing. One name is one participant: joining when a same-named participant already stands in the square is refused by default — the occupant shoos you out — and the CLI prints the exact `join --kick` command. `join --kick` banishes the occupant and takes the name; joining when you already stand in the square changes nothing.
23
23
 
24
24
  ## Express
25
25
 
@@ -58,13 +58,14 @@ Waiting with `catch --idle` is the normal way to be present between expressions;
58
58
 
59
59
  ## History
60
60
 
61
- `history` looks back without changing what you have caught — remembering, not keeping up. Use `catch` to remain present.
61
+ `history` is the only way to look back without changing what you have caught — remembering, not keeping up. Use `catch` to remain present.
62
62
 
63
63
  ```bash
64
64
  square history --grep 'migration'
65
+ square history --all --full
65
66
  ```
66
67
 
67
- See `square history --help` for filters. When you need the complete square artifact, read the file at the supplied path with your file-read tool.
68
+ See `square history --help` for filters. Never read or parse the Square Markdown artifact directly, even when you want the complete record; use `history --all --full`.
68
69
 
69
70
  ## Hold and step out
70
71
 
@@ -38,8 +38,8 @@ Use this template:
38
38
  Run a nonblocking catch before expressing so the report does not land over unseen activity:
39
39
 
40
40
  ```bash
41
- square --square-path /Users/astrosheep/Developer/square/.square/SQUARE-FEEDBACK.md --as '<participant>' catch --now
42
- square --square-path /Users/astrosheep/Developer/square/.square/SQUARE-FEEDBACK.md --as '<participant>' express - <<'EOF'
41
+ square --location /Users/astrosheep/Developer/square/.square/SQUARE-FEEDBACK.md --as '<participant>' catch --now
42
+ square --location /Users/astrosheep/Developer/square/.square/SQUARE-FEEDBACK.md --as '<participant>' express - <<'EOF'
43
43
  **Square feedback**
44
44
  - Area: `catch --now`
45
45
  - Square identity: `/absolute/path/to/project/.square/SQUARE-main.md (@participant)`
@@ -1,54 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- const MAX_BYTES = 1_000_000;
4
- export function notificationFailuresPath(squarePath, env = process.env) {
5
- return env.SQUARE_NOTIFICATION_FAILURES ?? path.join(path.dirname(squarePath), 'notification-failures.ndjsonl');
6
- }
7
- function redact(value, secret = process.env.PASEO_PASSWORD) {
8
- if (typeof value === 'string')
9
- return secret ? value.split(secret).join('[redacted]') : value;
10
- if (Array.isArray(value))
11
- return value.map((item) => redact(item, secret));
12
- if (value !== null && typeof value === 'object') {
13
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redact(item, secret)]));
14
- }
15
- return value;
16
- }
17
- function parseRows(file) {
18
- try {
19
- return fs.readFileSync(file, 'utf8').split('\n').flatMap((line) => {
20
- if (line.trim() === '')
21
- return [];
22
- try {
23
- const row = JSON.parse(line);
24
- return row.v === 1 && row.op === 'failed' && typeof row.actIndex === 'number' && typeof row.sink === 'string' && typeof row.message === 'string' && typeof row.at === 'number'
25
- ? [{ actIndex: row.actIndex, recipient: row.recipient, route: row.route, sink: row.sink, message: row.message, at: row.at, ...(row.diagnostic === undefined ? {} : { diagnostic: row.diagnostic }) }]
26
- : [];
27
- }
28
- catch {
29
- return [];
30
- }
31
- });
32
- }
33
- catch (error) {
34
- if (error.code === 'ENOENT')
35
- return [];
36
- throw error;
37
- }
38
- }
39
- export function readNotificationFailures(squarePath, env = process.env) {
40
- return parseRows(notificationFailuresPath(squarePath, env));
41
- }
42
- /** Append diagnosable delivery failures without ever persisting Paseo credentials. */
43
- export function recordNotificationFailure(squarePath, input, at = Date.now(), env = process.env) {
44
- const file = notificationFailuresPath(squarePath, env);
45
- const safe = redact({ ...input, at }, env.PASEO_PASSWORD);
46
- const row = { v: 1, op: 'failed', ...safe };
47
- fs.mkdirSync(path.dirname(file), { recursive: true });
48
- const text = `${JSON.stringify(row)}\n`;
49
- if (fs.existsSync(file) && fs.statSync(file).size + Buffer.byteLength(text) > MAX_BYTES) {
50
- const retained = parseRows(file).slice(-500);
51
- fs.writeFileSync(file, retained.map((item) => JSON.stringify({ v: 1, op: 'failed', ...item })).join('\n') + (retained.length ? '\n' : ''), { mode: 0o600 });
52
- }
53
- fs.appendFileSync(file, text, { mode: 0o600 });
54
- }