@astrosheep/square 0.3.61 → 0.3.63

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 (90) hide show
  1. package/claude-plugin/.claude-plugin/plugin.json +1 -1
  2. package/claude-plugin/skills/square/SKILL.md +5 -7
  3. package/codex-plugin/.codex-plugin/plugin.json +1 -1
  4. package/dist/activity.d.ts +6 -2
  5. package/dist/activity.js +168 -133
  6. package/dist/artifact.d.ts +4 -0
  7. package/dist/artifact.js +43 -14
  8. package/dist/attention-presentation.d.ts +1 -0
  9. package/dist/attention-presentation.js +8 -6
  10. package/dist/automatic-session.js +14 -24
  11. package/dist/boundary-presentation.d.ts +1 -2
  12. package/dist/boundary-presentation.js +4 -19
  13. package/dist/cli/context.d.ts +2 -0
  14. package/dist/cli/context.js +2 -18
  15. package/dist/cli/harness-command.js +1 -1
  16. package/dist/cli/mcp-server-command.d.ts +2 -0
  17. package/dist/cli/mcp-server-command.js +13 -0
  18. package/dist/cli/observation-commands.js +47 -19
  19. package/dist/cli/program.js +5 -2
  20. package/dist/cli/registry.js +2 -0
  21. package/dist/cli/square-commands.d.ts +1 -1
  22. package/dist/cli/square-commands.js +93 -125
  23. package/dist/decisions.d.ts +2 -0
  24. package/dist/decisions.js +44 -3
  25. package/dist/delivery-operations.d.ts +1 -2
  26. package/dist/delivery-operations.js +1 -4
  27. package/dist/file-changes.d.ts +12 -0
  28. package/dist/file-changes.js +235 -0
  29. package/dist/help.js +4 -8
  30. package/dist/host-ledger-file-adapter.d.ts +15 -13
  31. package/dist/host-ledger-file-adapter.js +131 -79
  32. package/dist/host-ledger-root.d.ts +2 -0
  33. package/dist/host-ledger-root.js +9 -0
  34. package/dist/host-ledger.d.ts +4 -24
  35. package/dist/inbox.d.ts +10 -5
  36. package/dist/inbox.js +152 -67
  37. package/dist/index.d.ts +3 -1
  38. package/dist/index.js +1 -0
  39. package/dist/mcp.d.ts +7 -0
  40. package/dist/mcp.js +122 -0
  41. package/dist/mention-parse.d.ts +8 -0
  42. package/dist/mention-parse.js +107 -0
  43. package/dist/model.d.ts +5 -0
  44. package/dist/model.js +4 -1
  45. package/dist/notifications.d.ts +1 -0
  46. package/dist/notifications.js +25 -20
  47. package/dist/participant-host.js +2 -2
  48. package/dist/ports.d.ts +2 -9
  49. package/dist/presentation.d.ts +12 -1
  50. package/dist/presentation.js +42 -12
  51. package/dist/presented.js +2 -1
  52. package/dist/registry.d.ts +8 -4
  53. package/dist/registry.js +41 -74
  54. package/dist/routes.d.ts +19 -2
  55. package/dist/routes.js +48 -5
  56. package/dist/runtime.d.ts +2 -0
  57. package/dist/runtime.js +2 -0
  58. package/dist/square-actions.d.ts +26 -13
  59. package/dist/square-actions.js +200 -84
  60. package/dist/square-application.d.ts +58 -0
  61. package/dist/square-application.js +183 -0
  62. package/dist/square-facade.d.ts +24 -9
  63. package/dist/square-file-adapter.d.ts +5 -12
  64. package/dist/square-file-adapter.js +12 -64
  65. package/dist/square-projections.d.ts +3 -2
  66. package/dist/square-projections.js +12 -3
  67. package/dist/square-storage.js +27 -34
  68. package/dist/square-wiring.d.ts +12 -7
  69. package/dist/square-wiring.js +36 -32
  70. package/dist/state-cell.d.ts +1 -1
  71. package/dist/stream.js +30 -26
  72. package/dist/views.d.ts +3 -3
  73. package/dist/views.js +4 -4
  74. package/dist/wake-attempts.js +2 -1
  75. package/dist/wake-evidence.js +2 -8
  76. package/dist/wakes.d.ts +1 -0
  77. package/dist/wakes.js +5 -5
  78. package/dist/watch.d.ts +6 -1
  79. package/dist/watch.js +20 -18
  80. package/extensions/square-pi.js +100 -112
  81. package/guides/application-boundary.md +32 -0
  82. package/guides/architect.md +77 -77
  83. package/guides/brainstorm.md +197 -197
  84. package/guides/change-waits.md +28 -0
  85. package/guides/host-ledger.md +9 -0
  86. package/guides/mcp.md +9 -0
  87. package/guides/operation-cancellation.md +17 -0
  88. package/guides/participant.md +7 -7
  89. package/package.json +8 -2
  90. package/skills/square/SKILL.md +5 -7
@@ -1,8 +1,7 @@
1
1
  import { presentPendingAtBoundary, renderPendingAtBoundary } from '../dist/boundary-presentation.js';
2
2
  import { automaticSessionEnd, automaticSessionStart } from '../dist/automatic-session.js';
3
- import { sessionInbox, waitForSessionPending } from '../dist/inbox.js';
4
- import { projectSessionBindings } from '../dist/square-projections.js';
5
- import { createHostLedgerPort } from '../dist/host-ledger-file-adapter.js';
3
+ import { sessionInbox, observeSessionPending } from '../dist/inbox.js';
4
+ import { renderAttentionDescription } from '../dist/attention-presentation.js';
6
5
 
7
6
  class PiDeliveryDroppedError extends Error {
8
7
  constructor() {
@@ -11,10 +10,6 @@ class PiDeliveryDroppedError extends Error {
11
10
  }
12
11
  }
13
12
 
14
- function sessionBindings(sessionId) {
15
- return projectSessionBindings({ hostLedger: createHostLedgerPort(), sessionId });
16
- }
17
-
18
13
  export function pendingInbox(inbox) {
19
14
  return inbox.filter((item) => item.notifications?.length > 0);
20
15
  }
@@ -37,14 +32,10 @@ function framePiMessage(content) {
37
32
 
38
33
  export function summarizePendingForNotify(pending) {
39
34
  const lines = pending.flatMap((membership) => membership.notifications.map((note) => {
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})`;
35
+ const description = renderAttentionDescription({ actor: note.actor, recipient: membership.name, route: note.route });
45
36
  const body = note.body.replace(/\s+/g, ' ').trim();
46
37
  const clipped = body.length > NOTIFY_BODY_MAX ? `${body.slice(0, NOTIFY_BODY_MAX).trimEnd()}…` : body;
47
- return `${note.actor} ${verb}${clipped ? ` — ${clipped}` : ''}`;
38
+ return `${description}${clipped ? ` — ${clipped}` : ''}`;
48
39
  }));
49
40
  if (lines.length === 0) return undefined;
50
41
  return lines.length === 1 ? `■ square · ${lines[0]}` : `■ square · ${lines[0]} (+${lines.length - 1} more)`;
@@ -133,6 +124,7 @@ export default function squarePiExtension(pi) {
133
124
  };
134
125
 
135
126
  const pause = (signal, delayMs) => new Promise((resolve) => {
127
+ if (signal.aborted) { resolve(); return; }
136
128
  const finish = () => {
137
129
  signal.removeEventListener('abort', finish);
138
130
  clearTimeout(timer);
@@ -143,115 +135,111 @@ export default function squarePiExtension(pi) {
143
135
  });
144
136
 
145
137
  const wake = async (token, signal) => {
146
- const armDeferredRetry = () => {
147
- const controller = new AbortController();
148
- let resolveArmed;
149
- const armed = new Promise((resolve) => { resolveArmed = resolve; });
150
- const abort = () => controller.abort();
151
- signal.addEventListener('abort', abort, { once: true });
152
- const pending = waitForSessionPending(sessionId, 30_000, {
153
- signal: controller.signal,
154
- excludeKeys: handledPending,
155
- skipImmediate: true,
156
- onChangeArmed: resolveArmed,
157
- }, sessionEnv(sessionId)).catch(() => {
158
- resolveArmed(false);
159
- return [];
160
- }).finally(() => signal.removeEventListener('abort', abort));
161
- return { armed, pending, cancel: () => controller.abort() };
162
- };
138
+ const boundSessionId = sessionId;
139
+ const env = sessionEnv(boundSessionId);
140
+ const observer = await observeSessionPending(boundSessionId, env);
141
+ try {
142
+ if (signal.aborted || token !== generation) return;
143
+ const armDeferredRetry = () => {
144
+ const controller = new AbortController();
145
+ let resolveArmed;
146
+ const armed = new Promise((resolve) => { resolveArmed = resolve; });
147
+ const abort = () => controller.abort();
148
+ signal.addEventListener('abort', abort, { once: true });
149
+ const pending = observer.wait(Infinity, {
150
+ signal: controller.signal,
151
+ excludeKeys: handledPending,
152
+ skipImmediate: true,
153
+ onChangeArmed: resolveArmed,
154
+ }).catch(() => {
155
+ resolveArmed(false);
156
+ return [];
157
+ }).finally(() => signal.removeEventListener('abort', abort));
158
+ return { armed, pending, cancel: () => controller.abort() };
159
+ };
163
160
 
164
- while (sessionId !== undefined && token === generation && !signal.aborted) {
165
- try {
166
- if ((await sessionBindings(sessionId)).length === 0) {
161
+ while (sessionId !== undefined && token === generation && !signal.aborted) {
162
+ const deferredRetry = retryAfterChange;
163
+ let pending;
164
+ try {
165
+ pending = deferredRetry && retryWait !== undefined
166
+ ? await retryWait
167
+ : await observer.wait(Infinity, { signal, excludeKeys: handledPending });
168
+ } catch {
167
169
  await pause(signal, 1_000);
168
170
  continue;
169
171
  }
170
- } catch {
171
- await pause(signal, 1_000);
172
- continue;
173
- }
174
-
175
- const deferredRetry = retryAfterChange;
176
- let pending;
177
- try {
178
- pending = deferredRetry && retryWait !== undefined
179
- ? await retryWait
180
- : await waitForSessionPending(sessionId, 30_000, { signal, excludeKeys: handledPending }, sessionEnv(sessionId));
181
- } catch {
182
- await pause(signal, 1_000);
183
- continue;
184
- }
185
- if (sessionId === undefined || token !== generation || signal.aborted) return;
186
- if (pending.length === 0) {
187
- if (deferredRetry) {
188
- retryAfterChange = false;
189
- retryWait = undefined;
172
+ if (sessionId === undefined || token !== generation || signal.aborted) return;
173
+ if (pending.length === 0) {
174
+ if (deferredRetry) {
175
+ retryAfterChange = false;
176
+ retryWait = undefined;
177
+ }
178
+ continue;
190
179
  }
191
- continue;
192
- }
193
- retryAfterChange = false;
194
- retryWait = undefined;
180
+ retryAfterChange = false;
181
+ retryWait = undefined;
195
182
 
196
- const keys = inboxKeys(pending);
197
- for (const key of keys) observedPending.add(key);
198
- const deferred = armDeferredRetry();
199
- const retryArmed = await deferred.armed;
200
- if (!retryArmed) {
201
- deferred.cancel();
202
- continue;
203
- }
204
- let keepDeferred = false;
205
- try {
206
- await presentPendingAtBoundary(
207
- sessionId,
208
- async (content) => {
209
- if (signal.aborted || currentRunSignal?.aborted) throw new Error('Pi notification delivery cancelled');
210
- // The full activity stays out of the TUI stream (display: false) but still
211
- // reaches the model; a brief notify tells the human what just landed.
212
- const summary = summarizePendingForNotify(pending);
213
- if (summary) ui?.notify?.(summary, 'info');
214
- const messageContent = framePiMessage(content);
215
- const landing = waitForLanding(messageContent, signal, 'steer');
216
- try {
217
- Promise.resolve(pi.sendMessage(
218
- { customType: 'square', content: messageContent, display: false },
219
- { deliverAs: 'steer', triggerTurn: true },
220
- )).catch((error) => landing.ack.settle(error));
221
- } catch (error) {
222
- landing.ack.settle(error);
223
- }
224
- return landing.promise;
225
- },
226
- async (id, env) => {
227
- const inbox = await sessionInbox(id, env ?? sessionEnv(id));
228
- if (signal.aborted || token !== generation) return [];
229
- for (const key of inboxKeys(inbox)) observedPending.add(key);
230
- return inbox.map((membership) => ({
231
- ...membership,
232
- notifications: membership.notifications.filter((note) => !handledPending.has(
233
- `${membership.squarePath}\u0000${membership.name.toLocaleLowerCase()}\u0000${note.actIndex}`,
234
- )),
235
- }));
236
- },
237
- sessionEnv(sessionId),
238
- signal,
239
- );
240
- for (const key of keys) handledPending.add(key);
241
- } catch (error) {
242
- if (signal.aborted || token !== generation) return;
243
- if (error instanceof PiDeliveryDroppedError) {
183
+ const keys = inboxKeys(pending);
184
+ for (const key of keys) observedPending.add(key);
185
+ const deferred = armDeferredRetry();
186
+ const retryArmed = await deferred.armed;
187
+ if (!retryArmed) {
244
188
  deferred.cancel();
245
189
  continue;
246
190
  }
247
- // The next state edge is already being observed before native injection starts.
248
- retryAfterChange = true;
249
- retryWait = deferred.pending;
250
- keepDeferred = true;
251
- } finally {
252
- if (!keepDeferred) deferred.cancel();
191
+ let keepDeferred = false;
192
+ try {
193
+ await presentPendingAtBoundary(
194
+ boundSessionId,
195
+ async (content) => {
196
+ if (signal.aborted || currentRunSignal?.aborted) throw new Error('Pi notification delivery cancelled');
197
+ // The full activity stays out of the TUI stream (display: false) but still
198
+ // reaches the model; a brief notify tells the human what just landed.
199
+ const summary = summarizePendingForNotify(pending);
200
+ if (summary) ui?.notify?.(summary, 'info');
201
+ const messageContent = framePiMessage(content);
202
+ const landing = waitForLanding(messageContent, signal, 'steer');
203
+ try {
204
+ Promise.resolve(pi.sendMessage(
205
+ { customType: 'square', content: messageContent, display: false },
206
+ { deliverAs: 'steer', triggerTurn: true },
207
+ )).catch((error) => landing.ack.settle(error));
208
+ } catch (error) {
209
+ landing.ack.settle(error);
210
+ }
211
+ return landing.promise;
212
+ },
213
+ async (id, env) => {
214
+ const inbox = await sessionInbox(id, env ?? sessionEnv(id));
215
+ if (signal.aborted || token !== generation) return [];
216
+ for (const key of inboxKeys(inbox)) observedPending.add(key);
217
+ return inbox.map((membership) => ({
218
+ ...membership,
219
+ notifications: membership.notifications.filter((note) => !handledPending.has(
220
+ `${membership.squarePath}\u0000${membership.name.toLocaleLowerCase()}\u0000${note.actIndex}`,
221
+ )),
222
+ }));
223
+ },
224
+ env,
225
+ signal,
226
+ );
227
+ for (const key of keys) handledPending.add(key);
228
+ } catch (error) {
229
+ if (signal.aborted || token !== generation) return;
230
+ if (error instanceof PiDeliveryDroppedError) {
231
+ deferred.cancel();
232
+ continue;
233
+ }
234
+ // The next state edge is already being observed before native injection starts.
235
+ retryAfterChange = true;
236
+ retryWait = deferred.pending;
237
+ keepDeferred = true;
238
+ } finally {
239
+ if (!keepDeferred) { deferred.cancel(); await deferred.pending; }
240
+ }
253
241
  }
254
- }
242
+ } finally { observer.close(); }
255
243
  };
256
244
 
257
245
  pi.on('session_start', async (_event, ctx) => {
@@ -0,0 +1,32 @@
1
+ # Application boundary
2
+
3
+ Square has three layers around an activity operation:
4
+
5
+ - **Domain** (`decisions.ts`, `catch-decisions.ts`, and projections) applies
6
+ SquareState rules and perception without transport or CLI concerns.
7
+ - **Application** (`square-application.ts`) receives an explicit caller
8
+ snapshot, resolves defaults and ambiguity, coordinates facade operations,
9
+ owns resources, applies express wait/no-wait policy, and returns structured
10
+ data. It never reads ambient process state from inside an application call.
11
+ - **Transport and presentation** (the CLI today, Pi and MCP later) parse input,
12
+ read stdin, render sensory output, build recovery commands, and choose process
13
+ exit behavior.
14
+
15
+ The application boundary exposes only neutral `HostLedgerPort` behavior. Ledger
16
+ layout, storage scopes, and hook reconciliation remain adapter concerns and are
17
+ intentionally absent. Exact lifecycle cleanup remains owned by the ledger/registry
18
+ adapter used by the facade.
19
+
20
+ The CLI now delegates join/reconnect/takeover gates and cleanup, express admission
21
+ and retry/no-wait behavior, catch/history, listener operations, lifecycle actions,
22
+ and status/participant projections to the application layer. It retains only
23
+ transport-specific details: argv validation, stdin body resolution, ANSI and
24
+ sensory rendering, continuation/recovery commands, watch leases and signals,
25
+ and process exit codes. Advanced history display context (`--at`, `-A`, `-B`,
26
+ `-C`) remains a presentation concern while the ordinary archive/filter/cursor
27
+ path uses the application history operation.
28
+
29
+ Application operations accept the final `OperationControl` coordinate as their
30
+ last control argument and forward it to facade operations. This is deliberately
31
+ separate from the transport's rendering and does not create a second cancellation
32
+ model.
@@ -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.*