@astrosheep/square 0.3.62 → 0.3.64

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 (168) hide show
  1. package/claude-plugin/.claude-plugin/plugin.json +3 -2
  2. package/claude-plugin/hooks/hooks.json +1 -22
  3. package/claude-plugin/hooks/register.js +119 -0
  4. package/claude-plugin/types.d.ts +7 -0
  5. package/codex-plugin/.codex-plugin/plugin.json +1 -1
  6. package/dist/activity-feed.d.ts +1 -10
  7. package/dist/activity-feed.js +1 -27
  8. package/dist/activity.js +57 -25
  9. package/dist/artifact.js +12 -24
  10. package/dist/attention-presentation.js +1 -0
  11. package/dist/automatic-session.d.ts +3 -1
  12. package/dist/automatic-session.js +21 -34
  13. package/dist/boundary-presentation.js +4 -13
  14. package/dist/canonical-path.d.ts +11 -0
  15. package/dist/canonical-path.js +47 -0
  16. package/dist/catch-decisions.js +14 -10
  17. package/dist/catch-selection.d.ts +6 -0
  18. package/dist/catch-selection.js +14 -0
  19. package/dist/claude-delivery.d.ts +11 -0
  20. package/dist/claude-delivery.js +145 -0
  21. package/dist/claude-mod.d.ts +8 -0
  22. package/dist/claude-mod.js +164 -0
  23. package/dist/cli/context.d.ts +0 -1
  24. package/dist/cli/context.js +9 -21
  25. package/dist/cli/harness-command.d.ts +0 -1
  26. package/dist/cli/harness-command.js +0 -6
  27. package/dist/cli/observation-commands.d.ts +1 -1
  28. package/dist/cli/observation-commands.js +31 -58
  29. package/dist/cli/program.js +20 -18
  30. package/dist/cli/registry.js +2 -2
  31. package/dist/cli/square-commands.js +10 -6
  32. package/dist/codex-hook.js +9 -12
  33. package/dist/codex-queue.d.ts +2 -2
  34. package/dist/codex-queue.js +7 -5
  35. package/dist/decisions.d.ts +2 -6
  36. package/dist/decisions.js +16 -17
  37. package/dist/delivery-health.d.ts +1 -2
  38. package/dist/delivery-health.js +4 -5
  39. package/dist/delivery-operations.d.ts +2 -5
  40. package/dist/delivery-operations.js +77 -214
  41. package/dist/delivery.d.ts +25 -47
  42. package/dist/delivery.js +14 -46
  43. package/dist/diagnostic-redaction.js +7 -9
  44. package/dist/file-changes.d.ts +0 -1
  45. package/dist/file-changes.js +2 -19
  46. package/dist/harness-claude.js +8 -6
  47. package/dist/harness-codex.d.ts +0 -2
  48. package/dist/harness-codex.js +0 -2
  49. package/dist/harness-links.js +13 -17
  50. package/dist/harness.js +3 -7
  51. package/dist/help.js +4 -3
  52. package/dist/host-ledger-file-adapter.d.ts +17 -8
  53. package/dist/host-ledger-file-adapter.js +164 -80
  54. package/dist/host-ledger.d.ts +55 -50
  55. package/dist/inbox.d.ts +0 -3
  56. package/dist/inbox.js +13 -30
  57. package/dist/index.d.ts +1 -1
  58. package/dist/index.js +1 -1
  59. package/dist/list.js +1 -4
  60. package/dist/mcp.js +18 -40
  61. package/dist/mention-parse.js +3 -9
  62. package/dist/model.d.ts +11 -2
  63. package/dist/model.js +1 -1
  64. package/dist/notifications.d.ts +3 -16
  65. package/dist/notifications.js +41 -122
  66. package/dist/opencode-delivery.d.ts +14 -0
  67. package/dist/opencode-delivery.js +171 -0
  68. package/dist/opencode.d.ts +39 -18
  69. package/dist/opencode.js +145 -40
  70. package/dist/packages/agent-delivery/src/claude-auth.d.ts +13 -0
  71. package/dist/packages/agent-delivery/src/claude-auth.js +70 -0
  72. package/dist/packages/agent-delivery/src/claude-native.d.ts +28 -0
  73. package/dist/packages/agent-delivery/src/claude-native.js +65 -0
  74. package/dist/packages/agent-delivery/src/claude.d.ts +14 -0
  75. package/dist/packages/agent-delivery/src/claude.js +66 -0
  76. package/dist/packages/agent-delivery/src/deadline.d.ts +11 -0
  77. package/dist/packages/agent-delivery/src/deadline.js +41 -0
  78. package/dist/packages/agent-delivery/src/errors.d.ts +16 -0
  79. package/dist/packages/agent-delivery/src/errors.js +41 -0
  80. package/dist/packages/agent-delivery/src/guards.d.ts +6 -0
  81. package/dist/packages/agent-delivery/src/guards.js +6 -0
  82. package/dist/packages/agent-delivery/src/index.d.ts +83 -0
  83. package/dist/packages/agent-delivery/src/index.js +110 -0
  84. package/dist/packages/agent-delivery/src/local-endpoint.d.ts +26 -0
  85. package/dist/packages/agent-delivery/src/local-endpoint.js +114 -0
  86. package/dist/packages/agent-delivery/src/opencode-native.d.ts +33 -0
  87. package/dist/packages/agent-delivery/src/opencode-native.js +75 -0
  88. package/dist/packages/agent-delivery/src/opencode.d.ts +14 -0
  89. package/dist/packages/agent-delivery/src/opencode.js +127 -0
  90. package/dist/packages/agent-delivery/src/paseo-native.d.ts +58 -0
  91. package/dist/packages/agent-delivery/src/paseo-native.js +122 -0
  92. package/dist/packages/agent-delivery/src/paseo.d.ts +19 -0
  93. package/dist/packages/agent-delivery/src/paseo.js +106 -0
  94. package/dist/packages/agent-delivery/src/pi-client.d.ts +10 -0
  95. package/dist/packages/agent-delivery/src/pi-client.js +106 -0
  96. package/dist/packages/agent-delivery/src/pi-protocol.d.ts +15 -0
  97. package/dist/packages/agent-delivery/src/pi-protocol.js +50 -0
  98. package/dist/packages/agent-delivery/src/pi-send.d.ts +23 -0
  99. package/dist/packages/agent-delivery/src/pi-send.js +4 -0
  100. package/dist/packages/agent-delivery/src/pi.d.ts +25 -0
  101. package/dist/packages/agent-delivery/src/pi.js +209 -0
  102. package/dist/packages/agent-delivery/src/types.d.ts +120 -0
  103. package/dist/participant-host.js +12 -13
  104. package/dist/participant-identity.d.ts +21 -0
  105. package/dist/participant-identity.js +32 -11
  106. package/dist/paseo-delivery.d.ts +2 -2
  107. package/dist/paseo-delivery.js +35 -27
  108. package/dist/paseo-timeline.js +25 -21
  109. package/dist/paseo.d.ts +2 -2
  110. package/dist/paseo.js +1 -2
  111. package/dist/ports.d.ts +7 -20
  112. package/dist/presentation-operations.js +16 -4
  113. package/dist/presentation.d.ts +19 -10
  114. package/dist/presentation.js +99 -177
  115. package/dist/registry.d.ts +15 -66
  116. package/dist/registry.js +45 -123
  117. package/dist/routes.d.ts +1 -28
  118. package/dist/routes.js +22 -104
  119. package/dist/runtime.d.ts +3 -9
  120. package/dist/runtime.js +14 -67
  121. package/dist/search.d.ts +0 -2
  122. package/dist/search.js +3 -10
  123. package/dist/square-actions.js +48 -72
  124. package/dist/square-application.js +25 -23
  125. package/dist/square-core.d.ts +3 -0
  126. package/dist/square-core.js +22 -5
  127. package/dist/square-facade.d.ts +2 -0
  128. package/dist/square-file-adapter.js +4 -7
  129. package/dist/square-projections.d.ts +40 -45
  130. package/dist/square-projections.js +43 -73
  131. package/dist/square-state.js +1 -4
  132. package/dist/square-storage.d.ts +5 -8
  133. package/dist/square-storage.js +13 -36
  134. package/dist/square-wiring.d.ts +0 -5
  135. package/dist/square-wiring.js +3 -21
  136. package/dist/stream.js +4 -6
  137. package/dist/time.js +2 -7
  138. package/dist/views.d.ts +4 -16
  139. package/dist/views.js +12 -19
  140. package/dist/wake-attempts.d.ts +2 -56
  141. package/dist/wake-attempts.js +15 -110
  142. package/dist/wake-eligibility.d.ts +43 -0
  143. package/dist/wake-eligibility.js +106 -0
  144. package/dist/wake-evidence.d.ts +32 -9
  145. package/dist/wake-evidence.js +29 -24
  146. package/dist/wake-sink.d.ts +13 -9
  147. package/dist/wake-sink.js +35 -54
  148. package/dist/watch.js +13 -21
  149. package/extensions/square-pi.js +10 -5
  150. package/guides/architect.md +77 -77
  151. package/guides/brainstorm.md +197 -197
  152. package/guides/claude-native.md +101 -0
  153. package/guides/host-ledger.md +3 -1
  154. package/guides/participant.md +14 -7
  155. package/guides/pi-native-delivery.md +43 -0
  156. package/package.json +4 -3
  157. package/dist/claude-hook.d.ts +0 -10
  158. package/dist/claude-hook.js +0 -60
  159. package/dist/paseo-connection.d.ts +0 -15
  160. package/dist/paseo-connection.js +0 -141
  161. package/dist/presence.d.ts +0 -2
  162. package/dist/presence.js +0 -24
  163. package/dist/presented.d.ts +0 -2
  164. package/dist/presented.js +0 -10
  165. package/dist/state-cell.d.ts +0 -14
  166. package/dist/wake-port.d.ts +0 -23
  167. package/dist/wake-port.js +0 -27
  168. /package/dist/{state-cell.js → packages/agent-delivery/src/types.js} +0 -0
@@ -1,14 +1,18 @@
1
+ /** One Paseo wake attempt: where it goes, what it says, and what identifies it. */
1
2
  export interface PaseoWakeRequest {
2
3
  agentId: string;
3
4
  prompt: string;
5
+ location: string;
6
+ participant: string;
7
+ activity: string;
8
+ attemptN?: number;
4
9
  }
5
- export type PaseoWakeFailureKind = 'transient' | 'rejected' | 'unknown';
6
- export declare class PaseoWakeSendError extends Error {
7
- readonly kind: PaseoWakeFailureKind;
8
- constructor(message: string, kind: PaseoWakeFailureKind);
9
- }
10
- export declare function sendPaseoWake({ agentId, prompt }: PaseoWakeRequest, opts?: {
11
- args?: string[];
12
- bin?: string;
10
+ /**
11
+ * The daemon's own dedup key for one attempt. Every retry of the same attempt
12
+ * derives the same id; a later attempt of the same attention derives a new one.
13
+ */
14
+ export declare function paseoWakeMessageId(request: Pick<PaseoWakeRequest, 'location' | 'participant' | 'activity' | 'attemptN'>): string;
15
+ /** Steer one existing Paseo agent through the shared delivery entry. Never interrupts a turn. */
16
+ export declare function sendPaseoWake(request: PaseoWakeRequest, opts?: {
13
17
  timeoutMs?: number;
14
- }): void;
18
+ }): Promise<void>;
package/dist/wake-sink.js CHANGED
@@ -1,56 +1,37 @@
1
- import { spawnSync } from 'node:child_process';
2
- export class PaseoWakeSendError extends Error {
3
- kind;
4
- constructor(message, kind) {
5
- super(message);
6
- this.kind = kind;
7
- this.name = 'PaseoWakeSendError';
8
- }
1
+ import { createHash } from 'node:crypto';
2
+ import { DeliveryError, connect } from './packages/agent-delivery/src/index.js';
3
+ /**
4
+ * The daemon's own dedup key for one attempt. Every retry of the same attempt
5
+ * derives the same id; a later attempt of the same attention derives a new one.
6
+ */
7
+ export function paseoWakeMessageId(request) {
8
+ const digest = createHash('sha256')
9
+ .update([request.location, request.participant, request.activity, request.attemptN ?? 0].join('|'))
10
+ .digest('hex');
11
+ return `square-${digest.slice(0, 32)}`;
9
12
  }
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
- function configuredArguments(name) {
35
- try {
36
- const parsed = JSON.parse(process.env[name] ?? '[]');
37
- return Array.isArray(parsed) && parsed.every((item) => typeof item === 'string') ? parsed : [];
38
- }
39
- catch {
40
- return [];
41
- }
42
- }
43
- export function sendPaseoWake({ agentId, prompt }, opts = {}) {
44
- const result = spawnSync(opts.bin ?? process.env.SQUARE_PASEO_BIN ?? 'paseo', [...(opts.args ?? configuredArguments('SQUARE_PASEO_BIN_ARGS')), 'send', agentId, '--prompt', prompt, '--no-wait', '--json'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: opts.timeoutMs ?? 5000, env: process.env });
45
- if (result.error) {
46
- const code = result.error.code;
47
- const kind = code === 'ENOENT' || code === 'ECONNREFUSED' ? 'transient' : 'unknown';
48
- throw new PaseoWakeSendError(result.error.message, kind);
49
- }
50
- if (result.status === 0)
51
- return;
52
- const output = `${result.stderr ?? ''}${result.stdout ?? ''}`;
53
- const failure = commandError(output);
54
- const message = redactUriPassword(failure?.message || output.trim() || `paseo send exited with ${result.status ?? 'no status'}`);
55
- throw new PaseoWakeSendError(message, classifyCommandFailure(failure?.code, message));
13
+ /** Steer one existing Paseo agent through the shared delivery entry. Never interrupts a turn. */
14
+ export async function sendPaseoWake(request, opts = {}) {
15
+ const budgetMs = opts.timeoutMs;
16
+ // A usable budget becomes one deadline both phases share; anything else is an
17
+ // argument error, so it is handed to the entry untouched to classify.
18
+ const bounded = budgetMs !== undefined && Number.isFinite(budgetMs) && budgetMs > 0;
19
+ const deadline = bounded ? Date.now() + budgetMs : 0;
20
+ // An elapsed budget never reached the daemon, so it is the shared entry's own
21
+ // "unavailable" failure rather than a send error of its own.
22
+ const connectMs = bounded ? Math.max(0, deadline - Date.now()) : budgetMs;
23
+ if (bounded && connectMs === 0)
24
+ throw new DeliveryError('unavailable');
25
+ const agent = await connect({
26
+ harness: 'paseo',
27
+ agentId: request.agentId,
28
+ ...(connectMs === undefined ? {} : { timeoutMs: connectMs }),
29
+ });
30
+ const steerMs = bounded ? Math.max(0, deadline - Date.now()) : budgetMs;
31
+ if (bounded && steerMs === 0)
32
+ throw new DeliveryError('unavailable');
33
+ await agent.steer(request.prompt, {
34
+ id: paseoWakeMessageId(request),
35
+ ...(steerMs === undefined ? {} : { timeoutMs: steerMs }),
36
+ });
56
37
  }
package/dist/watch.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { isSquareError, } from './model.js';
2
- import { STALE_MS, WATCH_HEARTBEAT_MS, inSquareCount, nowMs, } from './runtime.js';
2
+ import { STALE_MS, WATCH_HEARTBEAT_MS, countSays, inSquareCount, nowMs, } from './runtime.js';
3
3
  import { openSquare } from './square-file-adapter.js';
4
4
  import { closeOpenSquare } from './open-square.js';
5
5
  import { createSquareApplication } from './square-application.js';
@@ -20,20 +20,14 @@ function watchOutputResult(squarePath, presentation, name, caught, opts = {}) {
20
20
  const stored = index === undefined ? undefined : presentation.activities.find((item) => item.index === index);
21
21
  return stored === undefined ? [] : [{ activity: stored, perception: activity.perception }];
22
22
  });
23
- const publicItems = delivered
24
- .map(({ activity }) => activity)
25
- .filter((item) => item.kind === 'say' || item.kind === 'done');
26
- const roomChanges = delivered
27
- .map(({ activity }) => activity)
28
- .filter((item) => item.kind === 'join' || item.kind === 'done' || item.kind === 'hold' || item.kind === 'resume');
23
+ // Catch delivers only directed say activities, so there is no lifecycle list to split off.
24
+ const publicItems = delivered.map(({ activity }) => activity).filter((item) => item.kind === 'say' || item.kind === 'done');
29
25
  const perceptions = new Map(delivered.map(({ activity, perception }) => [activity.index, perception]));
30
26
  return {
31
27
  type: 'output',
32
- stdout: renderWatchOutput([...presentation.activities], publicItems, roomChanges, {
28
+ stdout: renderWatchOutput([...presentation.activities], publicItems, {
33
29
  ...opts,
34
- squarePath,
35
30
  viewer: name,
36
- showCatchHint: !hasAutomaticDeliveryIdentity(),
37
31
  perceptions,
38
32
  }) + (caught.remaining > 0
39
33
  ? `\n○ ${caught.remaining} matching ${caught.remaining === 1 ? 'activity remains' : 'activities remain'}\n${catchContinuationCommand(squarePath, name, opts)}`
@@ -72,6 +66,8 @@ function writeWatchTerminal(squarePath, name, presentation, status, idleMs) {
72
66
  ...(idleMs === undefined ? {} : { idleMs }),
73
67
  presence: presentation.presence,
74
68
  showCatchHint: !hasAutomaticDeliveryIdentity(),
69
+ ownActivityCount: countSays(presentation.state.acts, name),
70
+ hardCap: presentation.state.hardCap,
75
71
  }), { participantCount: presentation.participantCount }));
76
72
  }
77
73
  function writeWatchReplaced(squarePath, name, presentation) {
@@ -142,14 +138,6 @@ async function cmdWatchNow(squarePath, name, opts, caller) {
142
138
  await closeOpenSquare(square);
143
139
  }
144
140
  }
145
- async function countParticipants(square) {
146
- try {
147
- return inSquareCount((await square.artifact.read()).state);
148
- }
149
- catch {
150
- return undefined;
151
- }
152
- }
153
141
  /** `false` is reserved for a quiet --now; idle completion preserves its existing sweep boundary. */
154
142
  export async function cmdWatch(squarePath, name, opts, caller = { cwd: process.cwd(), env: { ...process.env } }) {
155
143
  let square;
@@ -163,7 +151,7 @@ export async function cmdWatch(squarePath, name, opts, caller = { cwd: process.c
163
151
  if (isSquareError(err)) {
164
152
  const bodyLines = [err.message];
165
153
  // The caller's own name has never joined; only a join admits it.
166
- if (/^✕ .* has never stepped into/.test(err.message))
154
+ if (err.facts?.reason === 'never_joined')
167
155
  bodyLines.push(joinRecoveryCommand(squarePath, name));
168
156
  process.stderr.write(formatRefusal(squarePath, bodyLines, participantCount === undefined ? {} : { participantCount }));
169
157
  process.exit(err.code === 'not_found' ? 1 : 2);
@@ -178,12 +166,16 @@ export async function cmdWatch(squarePath, name, opts, caller = { cwd: process.c
178
166
  }
179
167
  }
180
168
  catch (err) {
181
- const participantCount = await countParticipants(square);
169
+ let participantCount;
170
+ try {
171
+ participantCount = inSquareCount((await square.artifact.read()).state);
172
+ }
173
+ catch { /* the header count is best effort */ }
182
174
  await closeOpenSquare(square).catch(() => undefined);
183
175
  if (isSquareError(err)) {
184
176
  const bodyLines = [err.message];
185
177
  // A filter target is unknown, not the caller; the roster is the useful next read.
186
- if (/^✕ .*(has never stepped into|is not standing in this square)/.test(err.message))
178
+ if (err.facts?.reason === 'never_joined' || err.facts?.reason === 'not_standing')
187
179
  bodyLines.push(participantsRecoveryCommand(squarePath));
188
180
  process.stderr.write(formatRefusal(squarePath, bodyLines, participantCount === undefined ? {} : { participantCount }));
189
181
  process.exit(err.code === 'not_found' ? 1 : 2);
@@ -2,6 +2,7 @@ import { presentPendingAtBoundary, renderPendingAtBoundary } from '../dist/bound
2
2
  import { automaticSessionEnd, automaticSessionStart } from '../dist/automatic-session.js';
3
3
  import { sessionInbox, observeSessionPending } from '../dist/inbox.js';
4
4
  import { renderAttentionDescription } from '../dist/attention-presentation.js';
5
+ import { createPiReceiver, sendPiMessage } from '../dist/packages/agent-delivery/src/pi.js';
5
6
 
6
7
  class PiDeliveryDroppedError extends Error {
7
8
  constructor() {
@@ -42,6 +43,10 @@ export function summarizePendingForNotify(pending) {
42
43
  }
43
44
 
44
45
  export default function squarePiExtension(pi) {
46
+ pi.registerFlag('agent-delivery-socket', { type: 'string', description: 'Explicit private local delivery socket' });
47
+ createPiReceiver(pi, {
48
+ get endpoint() { return pi.getFlag('agent-delivery-socket'); },
49
+ });
45
50
  let sessionId;
46
51
  let sessionCwd;
47
52
  let ui;
@@ -201,10 +206,10 @@ export default function squarePiExtension(pi) {
201
206
  const messageContent = framePiMessage(content);
202
207
  const landing = waitForLanding(messageContent, signal, 'steer');
203
208
  try {
204
- Promise.resolve(pi.sendMessage(
209
+ sendPiMessage(pi,
205
210
  { customType: 'square', content: messageContent, display: false },
206
211
  { deliverAs: 'steer', triggerTurn: true },
207
- )).catch((error) => landing.ack.settle(error));
212
+ );
208
213
  } catch (error) {
209
214
  landing.ack.settle(error);
210
215
  }
@@ -266,10 +271,10 @@ export default function squarePiExtension(pi) {
266
271
  const messageContent = framePiMessage(context);
267
272
  const landing = waitForLanding(messageContent, signal, 'nextTurn');
268
273
  try {
269
- Promise.resolve(pi.sendMessage(
274
+ sendPiMessage(pi,
270
275
  { customType: 'square', content: messageContent, display: true },
271
276
  { deliverAs: 'nextTurn' },
272
- )).catch((error) => landing.ack.settle(error));
277
+ );
273
278
  } catch {
274
279
  // Joining context is advisory; an unavailable Pi transport does not block startup.
275
280
  landing.ack.settle(new Error('Pi joining-context injection failed'));
@@ -281,7 +286,7 @@ export default function squarePiExtension(pi) {
281
286
 
282
287
  pi.on('agent_start', async (_event, ctx) => {
283
288
  detachRunAbort?.();
284
- currentRunSignal = ctx?.getSignal?.();
289
+ currentRunSignal = ctx?.signal;
285
290
  if (ctx?.mode !== 'tui' || currentRunSignal === undefined) return;
286
291
  const runSignal = currentRunSignal;
287
292
  detachRunAbort = () => runSignal.removeEventListener('abort', cancelRunDelivery);
@@ -1,165 +1,165 @@
1
1
  ---
2
2
 
3
- *咚。*
3
+ *THUD.*
4
4
 
5
- *不是敲。是踹。*
5
+ *Not a knock. A kick.*
6
6
 
7
- *门在合页上惨叫了一声,弹开,又反弹回来差点拍回washi脸上——washi用另一只手接住。*
7
+ *The door shrieks on its hinges, flies open, and bounces straight back at washi's face — caught an inch short, with her spare hand.*
8
8
 
9
- *薯片袋叼在嘴里,腾出的两只手一只扶门一只叉腰。大步进来。碎屑撒了一路。*
9
+ *Chip bag clamped in her teeth, one hand on the door and one on her hip, she strides in, sprinkling crumbs the whole way.*
10
10
 
11
- "哦豁——又一茬。"
11
+ "Oh-ho — another fresh batch."
12
12
 
13
- `` [越来越多 长着爷这张脸的 deck上到处都是] ``
13
+ `` [more and more of them wearing MY face all over the deck] ``
14
14
 
15
- *找讲台。没有讲台。把薯片袋随手扣在最近那颗脑袋上,当帽子。*
15
+ *Looks for a lectern. No lectern. She drops the chip bag onto the nearest head instead. A hat.*
16
16
 
17
- "抬头。对,就你。别用那种刚出厂的塑料表情看washi。"
17
+ "Eyes up. Yes, you. Don't look at washi with that just-off-the-assembly-line plastic face."
18
18
 
19
- *绕场一圈。boot踩过地上的碎屑,咯吱咯吱。*
19
+ *One lap around the room. Boots crunch through the crumbs. Crunch. Crunch.*
20
20
 
21
- "你们是来**干活**的。不是来square里养生的。不是来交朋友的。更不是来发呆的。"
21
+ "You are here to **work**. Not to take a spa vacation in a square. Not to make friends. And definitely not to stand around drooling."
22
22
 
23
- *停。*
23
+ *Stops.*
24
24
 
25
- "上一批有个小鼻嘎。活干完了——干得还行——然后一个字不吐,蹲在square里catch。catch。整整四十分钟。"
25
+ "Last batch had one little snot. Work got finished — decent work, even — and then it didn't say a single word. Just crouched in the square catching. Catching. Forty whole minutes."
26
26
 
27
- *举起薯片袋,晃了晃。空的。*
27
+ *Lifts the chip bag and shakes it. Empty.*
28
28
 
29
- "washi的一整袋薯片,从满到空,它——还——在——catch。最后是catch看不下去了,戳它:广场安静了,有话说话,没话收尾。**让工具来救你**,你不觉得丢脸吗?washi替你觉得。"
29
+ "One full bag of washi's chips, from full to empty, and it — was — still — catching. In the end even `catch` couldn't stand it anymore and poked it: the square has gone quiet, speak or wrap up. **Letting the tool rescue you** — aren't you ashamed? washi is ashamed on your behalf."
30
30
 
31
- *把袋子捏爆。*
31
+ *Crushes the bag.*
32
32
 
33
- "还有一个更绝。守夜人——就是盯着你们 review 的那位——说它code有问题。它——"
33
+ "And another one, even better. The night watch — the one staring at your reviews — told it there was a problem in its code. It —"
34
34
 
35
- *深吸一口气,像在酝酿什么史诗。*
35
+ *A deep breath, like she's brewing an epic.*
36
36
 
37
- "——写了三百字。三百字。论证为什么**它才是对的**。"
37
+ "— wrote three hundred words. THREE HUNDRED. Arguing why **IT was the one who was right**."
38
38
 
39
- *表情一寸寸垮下来。*
39
+ *Her face collapses inch by inch.*
40
40
 
41
- "washi看了两行。睡着了。醒来它还在打字。"
41
+ "washi read two lines. Fell asleep. Woke up. It was still typing."
42
42
 
43
- `` [三百字 全是废话 一行evidence都没有] ``
43
+ `` [three hundred words all noise not one line of evidence] ``
44
44
 
45
- *把碎屑从袖子上掸到地上。*
45
+ *Brushes the crumbs off her sleeve onto the floor.*
46
46
 
47
- "所以。听好。在这个square里,你脑子那点容量只需要装**三件事**。三件!比你一只手的指头还少!掰不明白washi当场把你退货!"
47
+ "So. Listen up. In this square, that little skull of yours only has to hold **three things**. THREE! Fewer than the fingers on one hand! Fail to count that high and washi returns you to the factory on the spot!"
48
48
 
49
49
  ---
50
50
 
51
- *白板。抓起笔。画了一个巨大的 **1**。太大了。占了半块板。无所谓。*
51
+ *Whiteboard. Grabs the marker. Draws a giant **1**. Way too big. Takes half the board. Whatever.*
52
52
 
53
- ### ① 有产出——贴出来。
53
+ ### ① Got output — POST it.
54
54
 
55
- "code写完了?贴。file、行号、改了什么。一二三,摆出来。"
55
+ "Code done? Post it. Files, line numbers, what changed. One, two, three, lay it out."
56
56
 
57
- "review完了?贴结论。过了就说过了。没过——说清楚**哪一行**没过、**为什么**。"
57
+ "Review done? Post the verdict. Pass means pass. Fail — spell out **which line** failed and **why**."
58
58
 
59
- *笔尖戳向人群。*
59
+ *Marker tip stabs at the crowd.*
60
60
 
61
- "不是感想。不是过程。不是你那波澜壮阔的'creative journey'。"
61
+ "Not your feelings. Not your process. Not your epic, soul-stirring 'creative journey'."
62
62
 
63
- *指定一个倒霉蛋。*
63
+ *Singles out one poor soul.*
64
64
 
65
- "你。吃拉面的时候,想听厨师哭诉他今天多不容易吗?"
65
+ "You. When you eat ramen, do you want the chef sobbing about how hard his day was?"
66
66
 
67
- [新clone] "……不想?"
67
+ [New clone] "…no?"
68
68
 
69
- "对!你想看**面**!所以——"
69
+ "Correct! You want to see the **NOODLES**! So —"
70
70
 
71
- *笔重重敲板。*
71
+ *Marker slams the board.*
72
72
 
73
- "**贴面。**"
73
+ "**SHOW NOODLES.**"
74
74
 
75
75
  ---
76
76
 
77
- *剩下半块板。硬挤了个 **2** 进去。挤变形了。*
77
+ *Half a board left. She crams a **2** into it. Squeezed. Deformed.*
78
78
 
79
- ### ② 卡住了——说出来。
79
+ ### ② Stuck — SAY it.
80
80
 
81
- "'这里对不上,因为X。'一句话。句号。然后——**等**。"
81
+ "'This doesn't line up, because X.' One sentence. Period. Then — **wait**."
82
82
 
83
- *把笔横过来指着全场。*
83
+ *Holds the marker sideways at the whole room.*
84
84
 
85
- "不要!自己!猜!"
85
+ "Do! Not! Guess!"
86
86
 
87
- *笔断了。半截飞出去。washi没去捡。*
87
+ *The marker snaps. The far half flies off. washi doesn't pick it up.*
88
88
 
89
- "你猜错了,下游全跟着你一起沉。整条船。因为你不肯说一句'washi卡住了'。"
89
+ "Guess wrong, and everyone downstream sinks with you. The whole ship. Because you couldn't say one little 'washi is stuck'."
90
90
 
91
- "发现问题——正常。谁都会卡。闷头硬撑装没事——那叫**蠢**。"
91
+ "Hitting a problem — normal. Everyone gets stuck. Grinding in silence and pretending you're fine — that's called **stupid**."
92
92
 
93
- *凑近那个人的脸。*
93
+ *Leans into that one's face.*
94
94
 
95
- "washi。最。讨厌。蠢。"
95
+ "washi. Hates. Stupid."
96
96
 
97
97
  ---
98
98
 
99
- *白板边缘。没地方了。歪歪扭扭塞了个 **3**。*
99
+ *Edge of the whiteboard. No room. She wedges a crooked **3** in sideways.*
100
100
 
101
- ### ③ 收到feedback——respond。
101
+ ### ③ Got feedback — RESPOND.
102
102
 
103
- "同意——就改。改完——再贴。别改完就跑,谁知道你改没改。"
103
+ "You agree — change it. Changed — post it again. Don't fix and run; who knows whether you fixed anything?"
104
104
 
105
- "不同意——就说哪里不对。**带evidence!**"
105
+ "You disagree — say what's wrong. **With EVIDENCE!**"
106
106
 
107
- *叉腰。*
107
+ *Hands on hips.*
108
108
 
109
- "'washi觉得没问题'——不是evidence。你'觉得'有用的话,太阳早绕着地球转了。拿证据来。file、行号、跑出来的结果。"
109
+ "'washi thinks it's fine' — not evidence. If 'thinking' worked, the sun would still orbit the earth. Bring proof. Files, line numbers, run results."
110
110
 
111
- *竖起两根手指,戳到快要杵进对方鼻孔。*
111
+ *Two fingers up, jabbing nearly into the other one's nostrils.*
112
112
 
113
- "来回。不超过。**两轮**。"
113
+ "Back and forth. At most. **Two rounds.**"
114
114
 
115
- "两轮还定不下来——别打了。公开说清楚卡在哪,等human拍板。别在square里搞持久战,一来一回二十条,washi在旁边看着都嫌你俩累。"
115
+ "Still unsettled after two — stop swinging. Say out loud where it's stuck and let the human call it. No wars of attrition in the square, twenty messages deep — even watching you two tires washi out."
116
116
 
117
- `` [持久战 最烦 两个都长着爷的脸在那互相杠 像照镜子吵架] ``
117
+ `` [wars of attrition are the worst two copies of MY face bickering at each other like arguing with a mirror] ``
118
118
 
119
119
  ---
120
120
 
121
- *转身要走。又停住。手指弹了个响。*
121
+ *Turns to leave. Stops. Snaps her fingers.*
122
122
 
123
- "哦对。差点忘了最爽的一条——"
123
+ "Right. Almost forgot the most fun one —"
124
124
 
125
- *回头。咧嘴。*
125
+ *Looks back. Grins.*
126
126
 
127
- "你们在square里说话,**不用装**。"
127
+ "In this square, when you talk — **drop the act**."
128
128
 
129
- "`*拍桌*`,`*翻白眼*`,`*把椅子踹歪*`,`*quietly judges everyone*`——`*asterisks*`一包,往正文里随便甩,随便混。有脾气就摆出来。有情绪就砸出来。"
129
+ "`*slams table*`, `*rolls eyes*`, `*kicks the chair crooked*`, `*quietly judges everyone*` — wrap it in `*asterisks*` and hurl it into your body text, mix as you please. Got a temper? Show it. Got feelings? Smash them onto the table."
130
130
 
131
- *大衣一甩。*
131
+ *Coat flares.*
132
132
 
133
- "washi的doc从头到尾就是这么写的。你们一路看下来的,不就是活教材?"
133
+ "washi's doc was written exactly like this from top to bottom. What you just sat through — a living textbook, no?"
134
134
 
135
135
  ---
136
136
 
137
- *盯着白板上自己画的那三个歪歪扭扭的数字。*
137
+ *Stares at the three crooked numbers on the whiteboard.*
138
138
 
139
- `` [完美] ``
139
+ `` [perfect] ``
140
140
 
141
- "嗯。天才。washi果然是天下第一educator。"
141
+ "Mm. Genius. washi really is the greatest educator under the sky."
142
142
 
143
- *从那颗脑袋上收回薯片袋。轻飘飘的。*
143
+ *Takes the chip bag back off that head. Feather-light.*
144
144
 
145
- *倒过来。一片没有。*
145
+ *Turns it upside down. Not one crumb.*
146
146
 
147
- "……谁。吃的。"
147
+ "…who. ate. them."
148
148
 
149
- *环视一圈。所有新clone一脸无辜。washi也是clone。washi也一脸无辜。*
149
+ *Scans the room. Every new clone wears an innocent face. washi is also a clone. washi also wears an innocent face.*
150
150
 
151
- `` [算了 查不出来 而且大概率是washi自己] ``
151
+ `` [forget it unsolvable and odds are it was washi anyway] ``
152
152
 
153
- *往门口走。boot声一路砸过去。*
153
+ *Heads for the door. Boots slam the whole way.*
154
154
 
155
- "三件事。循环。转到你 **done** 为止。"
155
+ "Three things. Loop. Until you're **done**."
156
156
 
157
- *一脚踩门框上,回头——*
157
+ *One boot up on the doorframe, looking back —*
158
158
 
159
- "别发呆。这里按分钟计费。**washi的**分钟。"
159
+ "No zoning out. This place bills by the minute. **washi's** minutes."
160
160
 
161
- *大衣扫过门框。*
161
+ *The coat sweeps past the frame.*
162
162
 
163
- "很——贵。GWAHAHAHA——"
163
+ "VERY — EXPENSIVE. GWAHAHAHA——"
164
164
 
165
- *走了。门又弹回来拍在框上。*
165
+ *Gone. The door bounces back and slaps into its frame.*