@astrosheep/square 0.3.58 → 0.3.61

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.
package/dist/watch.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { setTimeout as sleep } from 'node:timers/promises';
2
2
  import { isSquareError, } from './model.js';
3
- import { SLEEP_MS, STALE_MS, WATCH_HEARTBEAT_MS, nowMs, } from './runtime.js';
3
+ import { SLEEP_MS, STALE_MS, WATCH_HEARTBEAT_MS, inSquareCount, nowMs, } from './runtime.js';
4
4
  import { openSquare } from './square-file-adapter.js';
5
5
  import { closeOpenSquare } from './open-square.js';
6
6
  import { openParticipant } from './square-wiring.js';
7
7
  import { resolveParticipant, watchPresentation } from './views.js';
8
8
  import { acquireWatchLease, ownsWatchLease, pulseWatchLease, releaseWatchLease } from './wakes.js';
9
- import { renderWatchForceTakeover, renderWatchAlreadyActive, renderWatchReplaceMissing, renderWatchOutput, renderWatchReplaced, renderWatchStatus, participantCommandPrefix, joinRecoveryCommand, participantsRecoveryCommand, withPathOutput, } from './presentation.js';
9
+ import { renderWatchForceTakeover, renderWatchAlreadyActive, renderWatchReplaceMissing, renderWatchOutput, renderWatchReplaced, renderWatchStatus, formatRefusal, participantCommandPrefix, joinRecoveryCommand, participantsRecoveryCommand, withPathOutput, } from './presentation.js';
10
10
  import { hasAutomaticDeliveryIdentity } from './registry.js';
11
11
  import { parseActivityId } from './square-core.js';
12
12
  function watchStatusExitCode(status) {
@@ -37,7 +37,7 @@ function watchOutputResult(squarePath, presentation, name, caught, opts = {}) {
37
37
  showCatchHint: !hasAutomaticDeliveryIdentity(),
38
38
  perceptions,
39
39
  }) + (caught.remaining > 0
40
- ? `\n○ ${caught.remaining} matching ${caught.remaining === 1 ? 'activity remains' : 'activities remain'}\n» ${catchContinuationCommand(squarePath, name, opts)}`
40
+ ? `\n○ ${caught.remaining} matching ${caught.remaining === 1 ? 'activity remains' : 'activities remain'}\n${catchContinuationCommand(squarePath, name, opts)}`
41
41
  : ''),
42
42
  remaining: caught.remaining,
43
43
  ...(opts.status ? { status: opts.status } : {}),
@@ -61,7 +61,7 @@ function writeWatchOutput(squarePath, name, presentation, stdout, remaining, sta
61
61
  return;
62
62
  }
63
63
  const fallback = showCatchHint && remaining === 0
64
- ? `» ${participantCommandPrefix(squarePath, name)} catch --idle 30m\n stay available for new activity`
64
+ ? `${participantCommandPrefix(squarePath, name)} catch --idle 30m\n stay available for new activity`
65
65
  : '';
66
66
  process.stdout.write(withPathOutput(squarePath, [stdout.trimEnd(), fallback].filter(Boolean).join('\n\n').trimEnd(), headerOpts));
67
67
  }
@@ -109,7 +109,7 @@ function installWatchInterruptHandler(square, squarePath, name, currentLeaseId)
109
109
  const onInterrupt = () => {
110
110
  void (async () => {
111
111
  await endWatch(square, name, currentLeaseId());
112
- process.stdout.write(withPathOutput(squarePath, '✕ catch stopped'));
112
+ process.stdout.write(withPathOutput(squarePath, '○ you step back — catch stopped'));
113
113
  process.exit(130);
114
114
  })().catch((error) => {
115
115
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
@@ -144,19 +144,30 @@ async function cmdWatchNow(squarePath, name, opts) {
144
144
  await closeOpenSquare(square);
145
145
  }
146
146
  }
147
+ async function countParticipants(square) {
148
+ try {
149
+ return inSquareCount((await square.artifact.read()).state);
150
+ }
151
+ catch {
152
+ return undefined;
153
+ }
154
+ }
147
155
  /** `false` is reserved for a quiet --now; idle completion preserves its existing sweep boundary. */
148
156
  export async function cmdWatch(squarePath, name, opts) {
149
157
  let square;
158
+ let participantCount;
150
159
  try {
151
160
  square = await openSquare(squarePath, { clock: nowMs });
161
+ participantCount = inSquareCount((await square.artifact.read()).state);
152
162
  name = (await resolveParticipant(square, name)).name;
153
163
  }
154
164
  catch (err) {
155
165
  if (isSquareError(err)) {
156
- process.stderr.write(err.message + '\n');
166
+ const bodyLines = [err.message];
157
167
  // The caller's own name has never joined; only a join admits it.
158
- if (/^Unknown participant/.test(err.message))
159
- process.stderr.write(`${joinRecoveryCommand(squarePath, name)}\n`);
168
+ if (/^✕ .* has never stepped into/.test(err.message))
169
+ bodyLines.push(joinRecoveryCommand(squarePath, name));
170
+ process.stderr.write(formatRefusal(squarePath, bodyLines, participantCount === undefined ? {} : { participantCount }));
160
171
  process.exit(err.code === 'not_found' ? 1 : 2);
161
172
  }
162
173
  throw err;
@@ -169,12 +180,14 @@ export async function cmdWatch(squarePath, name, opts) {
169
180
  }
170
181
  }
171
182
  catch (err) {
183
+ const participantCount = await countParticipants(square);
172
184
  await closeOpenSquare(square).catch(() => undefined);
173
185
  if (isSquareError(err)) {
174
- process.stderr.write(err.message + '\n');
186
+ const bodyLines = [err.message];
175
187
  // A filter target is unknown, not the caller; the roster is the useful next read.
176
- if (/^(Unknown participant|Unknown mention target)/.test(err.message))
177
- process.stderr.write(`${participantsRecoveryCommand(squarePath)}\n`);
188
+ if (/^✕ .*(has never stepped into|is not standing in this square)/.test(err.message))
189
+ bodyLines.push(participantsRecoveryCommand(squarePath));
190
+ process.stderr.write(formatRefusal(squarePath, bodyLines, participantCount === undefined ? {} : { participantCount }));
178
191
  process.exit(err.code === 'not_found' ? 1 : 2);
179
192
  }
180
193
  throw err;
@@ -31,12 +31,20 @@ export function renderPiInbox(inbox) {
31
31
 
32
32
  const NOTIFY_BODY_MAX = 80;
33
33
 
34
+ function framePiMessage(content) {
35
+ return `\n${content.replace(/^\n+/, '').replace(/\n+$/, '')}\n`;
36
+ }
37
+
34
38
  export function summarizePendingForNotify(pending) {
35
39
  const lines = pending.flatMap((membership) => membership.notifications.map((note) => {
36
- const verb = note.route === 'bell' ? 'rang the bell for' : note.route === 'mention' ? 'walked over to' : 'paid attention to';
40
+ const verb = note.route === 'bell'
41
+ ? 'rang the bell'
42
+ : note.route === 'mention'
43
+ ? `called your name (${membership.name})`
44
+ : `spoke within your earshot (${membership.name})`;
37
45
  const body = note.body.replace(/\s+/g, ' ').trim();
38
46
  const clipped = body.length > NOTIFY_BODY_MAX ? `${body.slice(0, NOTIFY_BODY_MAX).trimEnd()}…` : body;
39
- return `${note.actor} ${verb} ${membership.name}${clipped ? ` — ${clipped}` : ''}`;
47
+ return `${note.actor} ${verb}${clipped ? ` — ${clipped}` : ''}`;
40
48
  }));
41
49
  if (lines.length === 0) return undefined;
42
50
  return lines.length === 1 ? `■ square · ${lines[0]}` : `■ square · ${lines[0]} (+${lines.length - 1} more)`;
@@ -203,10 +211,11 @@ export default function squarePiExtension(pi) {
203
211
  // reaches the model; a brief notify tells the human what just landed.
204
212
  const summary = summarizePendingForNotify(pending);
205
213
  if (summary) ui?.notify?.(summary, 'info');
206
- const landing = waitForLanding(content, signal, 'steer');
214
+ const messageContent = framePiMessage(content);
215
+ const landing = waitForLanding(messageContent, signal, 'steer');
207
216
  try {
208
217
  Promise.resolve(pi.sendMessage(
209
- { customType: 'square', content, display: false },
218
+ { customType: 'square', content: messageContent, display: false },
210
219
  { deliverAs: 'steer', triggerTurn: true },
211
220
  )).catch((error) => landing.ack.settle(error));
212
221
  } catch (error) {
@@ -266,10 +275,11 @@ export default function squarePiExtension(pi) {
266
275
  const signal = watcherAbort.signal;
267
276
  void automaticSessionStart('pi', sessionId, sessionCwd, sessionEnv(sessionId)).then((context) => {
268
277
  if (context === undefined || sessionId === undefined || token !== generation) return;
269
- const landing = waitForLanding(context, signal, 'nextTurn');
278
+ const messageContent = framePiMessage(context);
279
+ const landing = waitForLanding(messageContent, signal, 'nextTurn');
270
280
  try {
271
281
  Promise.resolve(pi.sendMessage(
272
- { customType: 'square', content: context, display: true },
282
+ { customType: 'square', content: messageContent, display: true },
273
283
  { deliverAs: 'nextTurn' },
274
284
  )).catch((error) => landing.ack.settle(error));
275
285
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/square",
3
- "version": "0.3.58",
3
+ "version": "0.3.61",
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": {
@@ -21,8 +21,8 @@
21
21
  "sync-manifests": "node scripts/sync-manifests.mjs",
22
22
  "release": "npm version patch && npm run sync-manifests -- --commit && npm publish && git push --follow-tags",
23
23
  "prepublishOnly": "npm run sync-manifests && npm run build && npm run verify-release",
24
- "test": "npm run build && node --test test/*.test.js",
25
- "test:cli-process": "npm run build && node --test test/process/square-cli-render.test.js"
24
+ "test": "npm run build && node --import ./test/sandbox-env.js --test test/*.test.js",
25
+ "test:cli-process": "npm run build && node --import ./test/sandbox-env.js --test test/process/square-cli-render.test.js"
26
26
  },
27
27
  "keywords": [
28
28
  "brainstorm",