@dynamicagents/plugins 0.12.0 → 0.14.0

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 (132) hide show
  1. package/README.md +58 -77
  2. package/dist/browser/index.d.ts +0 -2
  3. package/dist/browser/index.js +21 -27
  4. package/dist/claude-code/cards.d.ts +55 -0
  5. package/dist/claude-code/cards.js +283 -0
  6. package/dist/claude-code/config.d.ts +28 -63
  7. package/dist/claude-code/config.js +0 -11
  8. package/dist/claude-code/egress.d.ts +8 -1
  9. package/dist/claude-code/egress.js +42 -4
  10. package/dist/claude-code/events.d.ts +106 -9
  11. package/dist/claude-code/events.js +201 -56
  12. package/dist/claude-code/github.d.ts +39 -0
  13. package/dist/claude-code/github.js +82 -0
  14. package/dist/claude-code/index.d.ts +16 -90
  15. package/dist/claude-code/index.js +14 -276
  16. package/dist/claude-code/model.d.ts +152 -0
  17. package/dist/claude-code/model.js +366 -0
  18. package/dist/claude-code/run.d.ts +230 -116
  19. package/dist/claude-code/run.js +380 -167
  20. package/dist/claude-code/session.d.ts +131 -0
  21. package/dist/claude-code/session.js +288 -0
  22. package/dist/claude-code/spec.d.ts +46 -0
  23. package/dist/claude-code/spec.js +83 -0
  24. package/dist/claude-code/verified.d.ts +15 -0
  25. package/dist/claude-code/verified.js +15 -0
  26. package/dist/computer/context.d.ts +1 -5
  27. package/dist/computer/context.js +7 -10
  28. package/dist/computer/file-lock.js +5 -4
  29. package/dist/computer/gate.d.ts +4 -4
  30. package/dist/computer/gate.js +5 -5
  31. package/dist/computer/index.d.ts +40 -258
  32. package/dist/computer/index.js +71 -324
  33. package/dist/computer/paths.d.ts +36 -16
  34. package/dist/computer/paths.js +42 -29
  35. package/dist/computer/proxy.d.ts +45 -0
  36. package/dist/computer/proxy.js +234 -0
  37. package/dist/computer/render.d.ts +1 -40
  38. package/dist/computer/render.js +2 -68
  39. package/dist/computer/tools-exec.js +12 -13
  40. package/dist/computer/tools-file.d.ts +9 -1
  41. package/dist/computer/tools-file.js +22 -82
  42. package/dist/computer/{tools-find.d.ts → tools-grep.d.ts} +1 -1
  43. package/dist/computer/tools-grep.js +129 -0
  44. package/dist/repo/checkout.d.ts +4 -5
  45. package/dist/repo/checkout.js +31 -8
  46. package/dist/repo/context.d.ts +35 -9
  47. package/dist/repo/context.js +101 -49
  48. package/dist/repo/index.d.ts +73 -28
  49. package/dist/repo/index.js +112 -96
  50. package/dist/repo/tools-checks.d.ts +3 -0
  51. package/dist/repo/tools-checks.js +112 -0
  52. package/dist/repo/tools-clone.js +1 -1
  53. package/dist/repo/tools-pr.d.ts +14 -0
  54. package/dist/repo/{tools-forge.js → tools-pr.js} +53 -36
  55. package/dist/repo/tools-review.d.ts +9 -0
  56. package/dist/repo/tools-review.js +67 -47
  57. package/dist/repo/tools-worktree.js +144 -60
  58. package/dist/repo/tools-worktrees.d.ts +12 -0
  59. package/dist/repo/tools-worktrees.js +48 -0
  60. package/dist/repo/url.d.ts +2 -2
  61. package/dist/repo/url.js +7 -7
  62. package/dist/scratch/index.d.ts +4 -4
  63. package/dist/scratch/index.js +12 -11
  64. package/dist/{computer → workspace}/advisory.d.ts +4 -4
  65. package/dist/{computer → workspace}/advisory.js +2 -2
  66. package/dist/{computer/host → workspace}/ca-trust.d.ts +8 -1
  67. package/dist/{computer/host → workspace}/ca-trust.js +12 -3
  68. package/dist/{computer/host → workspace}/container-deps.d.ts +4 -0
  69. package/dist/{computer/host → workspace}/container-deps.js +26 -1
  70. package/dist/{computer/host → workspace}/container-fault.d.ts +3 -3
  71. package/dist/{computer/host → workspace}/container-fault.js +23 -8
  72. package/dist/workspace/deps-snapshot.d.ts +110 -0
  73. package/dist/workspace/deps-snapshot.js +424 -0
  74. package/dist/workspace/exec.d.ts +94 -0
  75. package/dist/workspace/exec.js +153 -0
  76. package/dist/workspace/format.d.ts +36 -0
  77. package/dist/workspace/format.js +57 -0
  78. package/dist/{computer/host → workspace}/git-host.d.ts +21 -4
  79. package/dist/{computer/host → workspace}/git-host.js +72 -2
  80. package/dist/{computer/host → workspace}/git-identity.d.ts +1 -1
  81. package/dist/{computer/host → workspace}/git-identity.js +1 -1
  82. package/dist/workspace/index.d.ts +44 -47
  83. package/dist/workspace/index.js +52 -112
  84. package/dist/{computer/host → workspace}/install-job.d.ts +53 -4
  85. package/dist/{computer/host → workspace}/install-job.js +346 -178
  86. package/dist/{computer → workspace}/install.d.ts +17 -8
  87. package/dist/{computer → workspace}/install.js +24 -6
  88. package/dist/{computer/host/workspace.d.ts → workspace/object.d.ts} +125 -27
  89. package/dist/{computer/host/workspace.js → workspace/object.js} +341 -58
  90. package/dist/workspace/open.d.ts +95 -0
  91. package/dist/workspace/open.js +63 -0
  92. package/dist/workspace/read.d.ts +16 -0
  93. package/dist/workspace/read.js +22 -0
  94. package/dist/{computer → workspace}/shell.d.ts +5 -5
  95. package/dist/{computer → workspace}/shell.js +6 -6
  96. package/dist/{computer/host → workspace}/wake.d.ts +3 -1
  97. package/package.json +17 -37
  98. package/dist/arc-agi/analysis.d.ts +0 -322
  99. package/dist/arc-agi/analysis.js +0 -802
  100. package/dist/arc-agi/client.d.ts +0 -82
  101. package/dist/arc-agi/client.js +0 -113
  102. package/dist/arc-agi/game-tools.d.ts +0 -24
  103. package/dist/arc-agi/game-tools.js +0 -26
  104. package/dist/arc-agi/index.d.ts +0 -40
  105. package/dist/arc-agi/index.js +0 -175
  106. package/dist/arc-agi/main-agent.d.ts +0 -24
  107. package/dist/arc-agi/main-agent.js +0 -62
  108. package/dist/arc-agi/recipe.d.ts +0 -48
  109. package/dist/arc-agi/recipe.js +0 -97
  110. package/dist/arc-agi/scorecard.d.ts +0 -118
  111. package/dist/arc-agi/scorecard.js +0 -163
  112. package/dist/arc-agi/soul.d.ts +0 -29
  113. package/dist/arc-agi/soul.js +0 -79
  114. package/dist/arc-agi/store.d.ts +0 -153
  115. package/dist/arc-agi/store.js +0 -215
  116. package/dist/arc-agi/tools.d.ts +0 -36
  117. package/dist/arc-agi/tools.js +0 -574
  118. package/dist/arc-agi/types.d.ts +0 -211
  119. package/dist/arc-agi/types.js +0 -19
  120. package/dist/claude-code/recipe.d.ts +0 -53
  121. package/dist/claude-code/recipe.js +0 -100
  122. package/dist/computer/read.d.ts +0 -133
  123. package/dist/computer/read.js +0 -200
  124. package/dist/computer/tools-find.js +0 -216
  125. package/dist/recall/index.d.ts +0 -105
  126. package/dist/recall/index.js +0 -181
  127. package/dist/repo/tools-forge.d.ts +0 -4
  128. package/dist/triage/index.d.ts +0 -360
  129. package/dist/triage/index.js +0 -311
  130. /package/dist/{computer/host → workspace}/sync.d.ts +0 -0
  131. /package/dist/{computer/host → workspace}/sync.js +0 -0
  132. /package/dist/{computer/host → workspace}/wake.js +0 -0
@@ -1,3 +1,18 @@
1
+ import { initCard, resultCard, thinkingCard, toolCallCard, toolResultCard } from "./cards.js";
2
+ /**
3
+ * How far a thinking line's arrival may be from its own `timestamp` for the
4
+ * block to be timed.
5
+ *
6
+ * Both ends of a block are timed by when they arrive, which is only the truth
7
+ * while the drain reads live. A drain that re-attached after a cut is handed
8
+ * what was printed meanwhile in one burst, and its arrival times are the
9
+ * replay's — so a block whose line arrived far from when the CLI stamped it
10
+ * reads "Thought", with no time. Locally a line arrives 5–31 ms after its
11
+ * stamp; a re-attach has been measured at about a second. A container clock
12
+ * this far from the Worker's degrades every block to no time, rather than to a
13
+ * wrong one.
14
+ */
15
+ export const LIVE_SLACK_MS = 5_000;
1
16
  const EMPTY_USAGE = {
2
17
  input: 0,
3
18
  output: 0,
@@ -27,29 +42,86 @@ function readUsage(raw) {
27
42
  };
28
43
  }
29
44
  /**
30
- * Flatten an `assistant` message into a line of prose and the tools it called.
45
+ * An `assistant` message as events, one per block, in the order it said them.
31
46
  *
32
- * `thinking` blocks are dropped. They are the model's private reasoning, they
33
- * are the largest thing in the stream by a wide margin, and the parent agent is
34
- * being shown progress rather than asked to review the subagent's deliberation.
47
+ * Per block rather than flattened, because the blocks are different things to
48
+ * a reader: prose, the model's reasoning, and each tool it called. A message
49
+ * that only calls tools is as much progress as one that narrates.
35
50
  */
36
- function readAssistant(message) {
51
+ function readAssistant(message, thought) {
52
+ const messageId = str(message.id);
37
53
  const content = Array.isArray(message.content)
38
54
  ? message.content
39
55
  : [];
40
- const text = [];
41
- const tools = [];
56
+ const events = [];
42
57
  for (const block of content) {
43
58
  if (!asRecord(block))
44
59
  continue;
45
- const blockText = str(block.text);
46
- if (block.type === "text" && blockText)
47
- text.push(blockText.trim());
48
- const toolName = str(block.name);
49
- if (block.type === "tool_use" && toolName)
50
- tools.push(toolName);
60
+ if (block.type === "text") {
61
+ // Trimmed only to ask whether it is empty: indentation is a code block's
62
+ // and trailing spaces a hard break's, so the event keeps it verbatim.
63
+ const text = str(block.text);
64
+ if (text?.trim())
65
+ events.push({ kind: "text", text });
66
+ }
67
+ else if (block.type === "thinking") {
68
+ events.push({
69
+ kind: "thinking",
70
+ ...(messageId ? { messageId } : {}),
71
+ ...(thought === undefined ? {} : { durationMs: thought })
72
+ });
73
+ }
74
+ else if (block.type === "tool_use") {
75
+ const id = str(block.id);
76
+ const name = str(block.name);
77
+ if (id && name)
78
+ events.push({ kind: "toolUse", id, name, input: block.input });
79
+ }
80
+ }
81
+ return events;
82
+ }
83
+ /**
84
+ * A `user` line's tool results. Anything else a user line carries — the
85
+ * prompt echoed back, a hook's note — is not a tool's answer and says nothing.
86
+ */
87
+ function readToolResults(event, message) {
88
+ const content = Array.isArray(message.content) ? message.content : [];
89
+ const results = content.filter((block) => asRecord(block)?.type === "tool_result");
90
+ const structured = results.length === 1 ? event.tool_use_result : undefined;
91
+ const events = [];
92
+ for (const block of results) {
93
+ const id = str(block.tool_use_id);
94
+ if (!id)
95
+ continue;
96
+ events.push({
97
+ kind: "toolResult",
98
+ id,
99
+ isError: block.is_error === true,
100
+ content: resultText(block.content),
101
+ ...(structured === undefined || structured === null ? {} : { structured })
102
+ });
51
103
  }
52
- return { text: text.join(" ").replace(/\s+/g, " ").trim(), tools };
104
+ return events;
105
+ }
106
+ /**
107
+ * A `tool_result` block's content as text. It is a string, or a list of
108
+ * blocks of which only the text ones are readable here — an image a tool
109
+ * returned is named, not carried.
110
+ */
111
+ function resultText(content) {
112
+ if (typeof content === "string")
113
+ return content;
114
+ if (!Array.isArray(content))
115
+ return "";
116
+ return content
117
+ .map((block) => {
118
+ const record = asRecord(block);
119
+ if (record?.type === "text")
120
+ return str(record.text) ?? "";
121
+ return record?.type ? `[${String(record.type)}]` : "";
122
+ })
123
+ .filter(Boolean)
124
+ .join("\n");
53
125
  }
54
126
  /**
55
127
  * Read a `system`/`permission_denied` line into something worth reporting.
@@ -87,8 +159,13 @@ function readDenial(event) {
87
159
  * the filter.
88
160
  *
89
161
  * `result` and `init` are never nested and are read unconditionally.
162
+ *
163
+ * `clock` is when this read arrived, and the pending thinking block's start
164
+ * from the previous read — see {@link ParsedStream.thinkingFrom}. Without it no
165
+ * block is timed.
90
166
  */
91
- export function parseStream(buffer) {
167
+ export function parseStream(buffer, clock) {
168
+ let thinkingFrom = clock?.thinkingFrom;
92
169
  const events = [];
93
170
  let skipped = 0;
94
171
  let nested = 0;
@@ -131,6 +208,7 @@ export function parseStream(buffer) {
131
208
  nested++;
132
209
  continue;
133
210
  }
211
+ const at = readTime(event.timestamp);
134
212
  switch (event.type) {
135
213
  case "system": {
136
214
  if (event.subtype === "init") {
@@ -140,10 +218,14 @@ export function parseStream(buffer) {
140
218
  break;
141
219
  }
142
220
  const model = str(event.model);
221
+ const cwd = str(event.cwd);
222
+ const permissionMode = str(event.permissionMode);
143
223
  events.push({
144
224
  kind: "init",
145
225
  sessionId,
146
- ...(model ? { model } : {})
226
+ ...(model ? { model } : {}),
227
+ ...(cwd ? { cwd } : {}),
228
+ ...(permissionMode ? { permissionMode } : {})
147
229
  });
148
230
  break;
149
231
  }
@@ -151,6 +233,11 @@ export function parseStream(buffer) {
151
233
  events.push(readDenial(event));
152
234
  break;
153
235
  }
236
+ if (event.subtype === "thinking_tokens") {
237
+ if (clock)
238
+ thinkingFrom ??= clock.now;
239
+ break;
240
+ }
154
241
  if (event.subtype === "api_retry") {
155
242
  events.push({
156
243
  kind: "retry",
@@ -176,19 +263,25 @@ export function parseStream(buffer) {
176
263
  skip(trimmed);
177
264
  break;
178
265
  }
179
- const { text, tools } = readAssistant(message);
180
- if (!text && tools.length === 0)
181
- break;
182
- events.push({ kind: "assistant", text, tools });
266
+ // Ended by its own line's arrival, and trusted only on a live read.
267
+ const thought = clock !== undefined &&
268
+ thinkingFrom !== undefined &&
269
+ at !== undefined &&
270
+ Math.abs(clock.now - at) <= LIVE_SLACK_MS
271
+ ? clock.now - thinkingFrom
272
+ : undefined;
273
+ events.push(...readAssistant(message, thought));
183
274
  break;
184
275
  }
185
276
  // `user` messages on this stream are tool *results* being fed back to the
186
- // model, not anything a human said. They are large, they are already
187
- // summarised by the assistant turn that follows, and the un-nested ones
188
- // belong to the outer run's own tools — so they are deliberately not
189
- // surfaced as progress.
190
- case "user":
277
+ // model, not anything a human said — each one completes the card its call
278
+ // opened.
279
+ case "user": {
280
+ const message = asRecord(event.message);
281
+ if (message)
282
+ events.push(...readToolResults(event, message));
191
283
  break;
284
+ }
192
285
  case "result": {
193
286
  events.push({ kind: "result", result: readResult(event) });
194
287
  break;
@@ -216,8 +309,17 @@ export function parseStream(buffer) {
216
309
  default:
217
310
  skip(trimmed);
218
311
  }
312
+ if (at !== undefined)
313
+ thinkingFrom = undefined;
219
314
  }
220
- return { events, carry, skipped, nested, ...(sample ? { sample } : {}) };
315
+ return {
316
+ events,
317
+ carry,
318
+ skipped,
319
+ nested,
320
+ ...(sample ? { sample } : {}),
321
+ ...(thinkingFrom === undefined ? {} : { thinkingFrom })
322
+ };
221
323
  }
222
324
  /**
223
325
  * Read a `rate_limit_event` line, or nothing when it carries no status.
@@ -255,6 +357,11 @@ function readRateLimit(event) {
255
357
  : {})
256
358
  };
257
359
  }
360
+ /** A line's `timestamp`, in epoch milliseconds, or `undefined`. */
361
+ function readTime(value) {
362
+ const ms = typeof value === "string" ? Date.parse(value) : NaN;
363
+ return Number.isFinite(ms) ? ms : undefined;
364
+ }
258
365
  /** Whether a millisecond value is one `Date` can actually represent. */
259
366
  function isRealDate(ms) {
260
367
  return Number.isFinite(ms) && !Number.isNaN(new Date(ms).getTime());
@@ -279,6 +386,7 @@ function readResult(event) {
279
386
  : 0;
280
387
  const sessionId = str(event.session_id);
281
388
  const spawned = stats ? num(stats.spawned) : undefined;
389
+ const errors = readErrors(event.errors);
282
390
  return {
283
391
  subtype: str(event.subtype) ?? "unknown",
284
392
  isError: event.is_error === true,
@@ -290,9 +398,40 @@ function readResult(event) {
290
398
  costUsd: num(event.total_cost_usd),
291
399
  usage: readUsage(event.usage),
292
400
  ...(spawned === undefined ? {} : { subagentsSpawned: spawned }),
293
- permissionDenials: denials
401
+ permissionDenials: denials,
402
+ ...(errors.length > 0 ? { errors } : {}),
403
+ ...(event.structured_output === undefined ||
404
+ event.structured_output === null
405
+ ? {}
406
+ : { structured: event.structured_output })
294
407
  };
295
408
  }
409
+ /**
410
+ * A result line's `errors`, as sentences.
411
+ *
412
+ * **Anything that is not a string is stringified rather than dropped**, which is
413
+ * the whole of the care this needs: the field is a vendor's, an entry carrying a
414
+ * code and a message is a perfectly likely shape for it, and on the run this
415
+ * exists to explain — a resume with nowhere to resume from — it is the only
416
+ * account of what happened. Dropping an unexpected entry would report that run
417
+ * as having failed for no stated reason.
418
+ */
419
+ function readErrors(raw) {
420
+ if (!Array.isArray(raw))
421
+ return [];
422
+ return raw
423
+ .map((entry) => (typeof entry === "string" ? entry : safeJson(entry)))
424
+ .filter((entry) => entry.length > 0);
425
+ }
426
+ /** `value` as JSON, or as whatever `String` makes of it when it has no JSON. */
427
+ function safeJson(value) {
428
+ try {
429
+ return JSON.stringify(value) ?? String(value);
430
+ }
431
+ catch {
432
+ return String(value);
433
+ }
434
+ }
296
435
  /**
297
436
  * How much of an unrecognised line is kept for the logs.
298
437
  *
@@ -301,14 +440,15 @@ function readResult(event) {
301
440
  */
302
441
  const SAMPLE_MAX_CHARS = 200;
303
442
  /**
304
- * Turn events into progress notes the parent can post.
443
+ * Turn events into progress notes for the parent's transcript.
305
444
  *
306
445
  * **`from` is the number of notes already emitted for this run, and the key is
307
446
  * derived from it.** Not from the note's content, not from a clock, and not from
308
447
  * the Claude Code `uuid` — from position alone.
309
448
  *
310
- * The reason is replay. A chunk that dies mid-drain is retried by the Workflow,
311
- * the re-attach replays the tail, and the same assistant turn is parsed twice. A
449
+ * The reason is replay. A drain that dies mid-stream is resumed from the last
450
+ * stored sequence, whatever arrived after it is read again, and the same
451
+ * assistant turn is parsed twice. A
312
452
  * content-derived key would make two identical notes collide *by luck* (and two
313
453
  * genuinely repeated notes — "Running tests" twice — collide wrongly). A
314
454
  * clock-derived key would never collide, so every replay would re-post
@@ -319,31 +459,30 @@ export function toProgress(events, from) {
319
459
  const out = [];
320
460
  let n = from;
321
461
  for (const event of events) {
322
- const text = describe(event);
323
- if (!text)
462
+ const note = describe(event);
463
+ if (!note)
324
464
  continue;
325
- out.push({ key: `claude:${n}`, text });
465
+ out.push({ key: `claude:${n}`, ...note });
326
466
  n++;
327
467
  }
328
468
  return out;
329
469
  }
330
- /** One line of progress, or nothing when the event is not worth a note. */
470
+ /** One note, or nothing when the event is not worth one. */
331
471
  function describe(event) {
332
472
  switch (event.kind) {
333
- // Tool calls are not narration. A turn that only called tools said nothing
334
- // the reader can act on, and it cost one Slack message per Bash/Read/Edit —
335
- // nine of them for a one-line README edit. The tool names added nothing to
336
- // the turns that *did* have text either, so the prefix goes with them.
337
- //
338
- // Same policy the in-process agents already apply in the gatekeeper
339
- // (src/agents/shared/loop.ts: "Tool-only steps stay silent in Slack").
340
- // Deliberately narrower than silencing the whole channel: `denied` and
341
- // `retry` below stay, because they are the only evidence of a session that
342
- // is being refused or throttled.
343
- //
344
473
  // Never clipped. A plan or a report cut mid-sentence reads as complete.
345
- case "assistant":
346
- return event.text || undefined;
474
+ case "text":
475
+ return { text: event.text };
476
+ // Tool activity is a card, and a card never reaches the thread — core's
477
+ // `transcribeNote` keeps it on the transcript — so a session's every Bash,
478
+ // Read and Edit is shown without one message each in front of the person.
479
+ // `./cards.ts` holds how each tool reads.
480
+ case "toolUse":
481
+ return toolCallCard(event.id, event.name, event.input);
482
+ case "toolResult":
483
+ return toolResultCard(event);
484
+ case "thinking":
485
+ return thinkingCard(event.durationMs, event.messageId);
347
486
  case "denied":
348
487
  // The one progress note that is more useful than the session's own
349
488
  // account of itself. A refused tool call is invisible in the transcript —
@@ -351,12 +490,14 @@ function describe(event) {
351
490
  // parent sees a subagent being resourceful and never learns it was fenced
352
491
  // in. Named tool first, because that is what distinguishes a broken
353
492
  // configuration from a deny rule doing its job.
354
- return `permission denied${event.tool ? ` for ${event.tool}` : ""}: ${event.reason}`;
493
+ return {
494
+ text: `permission denied${event.tool ? ` for ${event.tool}` : ""}: ${event.reason}`
495
+ };
355
496
  case "retry":
356
497
  // Surfaced deliberately. This is what a budget refusal from the egress
357
498
  // gateway looks like from inside the container, and a run that ends
358
499
  // shortly afterwards is explained by it.
359
- return `retrying the model call: ${event.detail}`;
500
+ return { text: `retrying the model call: ${event.detail}` };
360
501
  case "rateLimit":
361
502
  /**
362
503
  * Silent while the bucket is fine, which is every line of a healthy
@@ -370,15 +511,19 @@ function describe(event) {
370
511
  */
371
512
  return event.info.status === RATE_LIMIT_OK
372
513
  ? undefined
373
- : `the ${event.info.rateLimitType ?? "subscription"} limit reports "${event.info.status}"` +
374
- (event.info.resetsAt === undefined
375
- ? ""
376
- : ` until ${new Date(event.info.resetsAt * 1000).toISOString()}`);
377
- // `init` names a session id nobody outside this package can use, and
378
- // `result` is the terminal outcome the caller reports itself.
514
+ : {
515
+ text: `the ${event.info.rateLimitType ?? "subscription"} limit reports "${event.info.status}"` +
516
+ (event.info.resetsAt === undefined
517
+ ? ""
518
+ : ` until ${new Date(event.info.resetsAt * 1000).toISOString()}`)
519
+ };
520
+ // `init`'s session id is a handle as well as news: it reaches the host on
521
+ // the drain's cursor — see `DrainCursor.sessionId` in ./run.ts — where
522
+ // something can record it and resume with it.
379
523
  case "init":
524
+ return initCard(event);
380
525
  case "result":
381
- return undefined;
526
+ return resultCard(event.result);
382
527
  }
383
528
  }
384
529
  function clip(text, max) {
@@ -0,0 +1,39 @@
1
+ /**
2
+ * GitHub access for a session, with the token on the Worker side.
3
+ *
4
+ * The session is launched with {@link GITHUB_TOKEN_PLACEHOLDER} as `GH_TOKEN`,
5
+ * and git is given a credential helper that answers with it. The egress gateway swaps
6
+ * the placeholder for the real token on {@link GITHUB_HOSTS} only, so a process
7
+ * in the container can *use* GitHub as the deployment's account and can never
8
+ * *read* the token — the same containment as the Anthropic credential.
9
+ *
10
+ * Keyed on the placeholder, unlike the Anthropic swap: a GitHub request that
11
+ * did not ask for a credential stays anonymous, as it would be anywhere else.
12
+ */
13
+ /** The hosts the token is ever sent to: git over HTTPS, and the API. */
14
+ export declare const GITHUB_HOSTS: ReadonlySet<string>;
15
+ /**
16
+ * What `GH_TOKEN` and git's credential helper hold. Shaped like a classic
17
+ * token, so nothing that checks the shape refuses it before sending it.
18
+ */
19
+ export declare const GITHUB_TOKEN_PLACEHOLDER: string;
20
+ /**
21
+ * The session's GitHub environment. A credential helper for `https://github.com`
22
+ * rather than a token in the remote URL, so the checkout's `.git/config` holds
23
+ * nothing and every git the session starts presents the placeholder.
24
+ */
25
+ export declare function githubEnv(): Record<string, string>;
26
+ /** The keys {@link githubEnv} owns, which a host's `env` may not set beside it. */
27
+ export declare const GITHUB_ENV_KEYS: readonly string[];
28
+ /** How an authorization header carried the placeholder, if it did. */
29
+ type Presented = "bearer" | "basic";
30
+ /**
31
+ * Whether `value` presents the placeholder: `Bearer`/`token` as `gh` sends it,
32
+ * or Basic `user:placeholder` as git does after a `401`.
33
+ */
34
+ export declare function presentsPlaceholder(value: string | null): Presented | undefined;
35
+ /** The real credential, in the scheme the placeholder came in. */
36
+ export declare function githubAuthorization(presented: Presented, token: string): string;
37
+ /** A refusal in GitHub's error shape, which `gh` and git both print. */
38
+ export declare function githubError(message: string, status: number): Response;
39
+ export {};
@@ -0,0 +1,82 @@
1
+ /**
2
+ * GitHub access for a session, with the token on the Worker side.
3
+ *
4
+ * The session is launched with {@link GITHUB_TOKEN_PLACEHOLDER} as `GH_TOKEN`,
5
+ * and git is given a credential helper that answers with it. The egress gateway swaps
6
+ * the placeholder for the real token on {@link GITHUB_HOSTS} only, so a process
7
+ * in the container can *use* GitHub as the deployment's account and can never
8
+ * *read* the token — the same containment as the Anthropic credential.
9
+ *
10
+ * Keyed on the placeholder, unlike the Anthropic swap: a GitHub request that
11
+ * did not ask for a credential stays anonymous, as it would be anywhere else.
12
+ */
13
+ /** The hosts the token is ever sent to: git over HTTPS, and the API. */
14
+ export const GITHUB_HOSTS = new Set([
15
+ "github.com",
16
+ "api.github.com"
17
+ ]);
18
+ /**
19
+ * What `GH_TOKEN` and git's credential helper hold. Shaped like a classic
20
+ * token, so nothing that checks the shape refuses it before sending it.
21
+ */
22
+ export const GITHUB_TOKEN_PLACEHOLDER = "ghp_" + "0".repeat(36);
23
+ /** The username GitHub expects beside a token in git's Basic auth. */
24
+ const GIT_USER = "x-access-token";
25
+ /**
26
+ * The session's GitHub environment. A credential helper for `https://github.com`
27
+ * rather than a token in the remote URL, so the checkout's `.git/config` holds
28
+ * nothing and every git the session starts presents the placeholder.
29
+ */
30
+ export function githubEnv() {
31
+ return {
32
+ GH_TOKEN: GITHUB_TOKEN_PLACEHOLDER,
33
+ GIT_CONFIG_COUNT: "1",
34
+ GIT_CONFIG_KEY_0: "credential.https://github.com.helper",
35
+ // Answers `get` only; `store` and `erase` are no-ops that still exit 0.
36
+ GIT_CONFIG_VALUE_0: `!f() { if test "$1" = get; then printf 'username=${GIT_USER}\\npassword=%s\\n' "$GH_TOKEN"; fi; }; f`
37
+ };
38
+ }
39
+ /** The keys {@link githubEnv} owns, which a host's `env` may not set beside it. */
40
+ export const GITHUB_ENV_KEYS = Object.keys(githubEnv());
41
+ /**
42
+ * Whether `value` presents the placeholder: `Bearer`/`token` as `gh` sends it,
43
+ * or Basic `user:placeholder` as git does after a `401`.
44
+ */
45
+ export function presentsPlaceholder(value) {
46
+ if (!value)
47
+ return undefined;
48
+ const [scheme = "", credential = ""] = value.trim().split(/\s+/, 2);
49
+ switch (scheme.toLowerCase()) {
50
+ case "bearer":
51
+ case "token":
52
+ return credential === GITHUB_TOKEN_PLACEHOLDER ? "bearer" : undefined;
53
+ case "basic": {
54
+ let decoded;
55
+ try {
56
+ decoded = atob(credential);
57
+ }
58
+ catch {
59
+ return undefined;
60
+ }
61
+ const password = decoded.slice(decoded.indexOf(":") + 1);
62
+ return decoded.includes(":") && password === GITHUB_TOKEN_PLACEHOLDER
63
+ ? "basic"
64
+ : undefined;
65
+ }
66
+ default:
67
+ return undefined;
68
+ }
69
+ }
70
+ /** The real credential, in the scheme the placeholder came in. */
71
+ export function githubAuthorization(presented, token) {
72
+ return presented === "basic"
73
+ ? `Basic ${btoa(`${GIT_USER}:${token}`)}`
74
+ : `Bearer ${token}`;
75
+ }
76
+ /** A refusal in GitHub's error shape, which `gh` and git both print. */
77
+ export function githubError(message, status) {
78
+ return new Response(JSON.stringify({ message }), {
79
+ status,
80
+ headers: { "content-type": "application/json" }
81
+ });
82
+ }
@@ -1,9 +1,5 @@
1
- import type { AgentPlugin } from "@dynamicagents/core";
2
- import type { CredentialStore, Lead } from "./credentials.js";
3
- import { type DrainCursor, type DrainOutcome, type DrainOptions, type SessionRuntime } from "./run.js";
4
- import { type ClaudeCodeConfig } from "./config.js";
5
1
  /**
6
- * `@dynamicagents/plugins/claude-code` — subtasks that run the Claude Code CLI.
2
+ * `@dynamicagents/plugins/claude-code` — sub-agents that run the Claude Code CLI.
7
3
  *
8
4
  * ## Why this exists
9
5
  *
@@ -16,11 +12,12 @@ import { type ClaudeCodeConfig } from "./config.js";
16
12
  *
17
13
  * ## What it contributes, and what it does not
18
14
  *
19
- * It declares **one subtask type and no tool families**. That is unusual here
20
- * and it is the whole shape of the thing: Claude Code brings its own tools, its
21
- * own loop and its own context management, so there is nothing for core's
22
- * resumable runner to drive. The host's subagent overrides `executeChunk` and
23
- * calls {@link claudeCodeSession} instead.
15
+ * **A writer's sub-agent spec, a model, and no plugin.** Claude Code brings its own
16
+ * tools, its own loop and its own context management, so there is nothing for
17
+ * an agent loop to drive: the session *is* the sub-agent's model —
18
+ * {@link claudeCodeModel} — and Think's recovery drives that. A host binds
19
+ * {@link CLAUDE_CODE_AGENT} to a `SubAgent` class whose `getModel()` returns
20
+ * one, and adds the `prepare` and `settle` that claim and release a workspace.
24
21
  *
25
22
  * ## The credential never enters the container
26
23
  *
@@ -55,91 +52,20 @@ import { type ClaudeCodeConfig } from "./config.js";
55
52
  * policy is `{ mode: "http-gateway" }` and which supplies a
56
53
  * {@link CredentialStore}. See the README.
57
54
  */
58
- /**
59
- * Bind a config to a workspace runtime: start, drain, stop.
60
- *
61
- * The three calls a host's `executeChunk` needs and nothing else. State lives in
62
- * the {@link DrainCursor} the caller persists between chunks, so this holds none
63
- * and a fresh isolate picks up exactly where the last one stopped.
64
- */
65
- export declare function claudeCodeSession(config: ClaudeCodeConfig): {
66
- /**
67
- * Spawn a session for one subtask and drain its first window.
68
- *
69
- * `subtaskId` namespaces the exec id. Subtasks are a concurrent fan-out, so
70
- * two sessions in one workspace under one id would spawn over each other —
71
- * see {@link execIdFor}.
72
- */
73
- start(runtime: SessionRuntime, subtaskId: string | number, prompt: string, dir: string, sinks?: Pick<DrainOptions, "onProgress" | "onCheckpoint">): Promise<DrainOutcome>;
74
- /**
75
- * Re-attach to a running session and drain one more window.
76
- *
77
- * **A session whose container was replaced is reported, not thrown.** The
78
- * attachment is the first thing to notice a replacement, and the caller is a
79
- * chunk loop with a cursor it will happily keep presenting: a throw here
80
- * retries the same dead id until the chunk allowance runs out, and the run
81
- * ends on a stack trace that names neither the session nor the cause. A
82
- * terminal outcome ends it at the first attempt instead, with the exit code
83
- * the runtime uses for a killed process and an explanation on `stderr`,
84
- * which is exactly the channel a session that died without a result line
85
- * already reports through.
86
- *
87
- * **What it must not say is that starting over is safe.** A session is an
88
- * agent: by the time its container went, it may have written files that
89
- * reached the workspace, committed, pushed, or called something outside
90
- * altogether. None of that is visible from here — only that the attachment
91
- * is gone — so the honest report is what is known plus where to look, and
92
- * the decision belongs to whoever can read the durable state.
93
- */
94
- resume(runtime: SessionRuntime, cursor: DrainCursor, sinks?: Pick<DrainOptions, "onProgress" | "onCheckpoint">): Promise<DrainOutcome>;
95
- /** Stop a session — `SIGTERM`, so its own process tree goes with it. */
96
- stop(runtime: SessionRuntime, subtaskId: string | number): Promise<void>;
97
- /**
98
- * Is any credential usable right now, and if not, when?
99
- *
100
- * The same question {@link egress} answers per request, asked ahead of time
101
- * so a host can decline to start a session it cannot pay for. That is not a
102
- * micro-optimisation: an invocation carries an 18.7-27k-token cached prefix
103
- * before it does anything, so starting one and letting the gateway refuse
104
- * its first model call costs a container start and that prefix to learn what
105
- * this returns for free — and reports it as a failed run rather than as a
106
- * rate limit with a time on it.
107
- *
108
- * Reads only. Nothing here marks anything spent.
109
- */
110
- credentials: (store: CredentialStore) => Promise<Lead>;
111
- /**
112
- * The `Fetcher` the workspace object installs as its egress policy.
113
- *
114
- * `store` is an **argument rather than a config field**, and that is the
115
- * point: the credential pool's state belongs to whichever object has
116
- * storage, and this same config object is also held by the parent's plugin
117
- * list and by the subagent facet, neither of which has any. Making it a
118
- * field would force both of them to invent one.
119
- */
120
- egress: (store: CredentialStore) => {
121
- fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
122
- connect(address: SocketAddress | string, options?: SocketOptions): Socket;
123
- };
124
- };
125
- export type ClaudeCodeSession = ReturnType<typeof claudeCodeSession>;
126
- /**
127
- * The plugin.
128
- *
129
- * `subtaskType` and nothing else: no `toolFamilies`, because Claude Code's tools
130
- * are its own, and no `capability` on the plugin, because a plugin declaring a
131
- * subtask type puts its capability block on the **type** — declaring both makes
132
- * the main agent read the same advice twice per round.
133
- */
134
- export declare function claudeCode(config: ClaudeCodeConfig): AgentPlugin;
55
+ export { claudeCodeModel } from "./model.js";
56
+ export type { ClaudeCodeModelOptions, SessionEnd, SessionOutcome, SessionRecord, SessionWorkspace } from "./model.js";
57
+ export { claudeCodeSession } from "./session.js";
58
+ export type { ClaudeCodeSession, ResumeSession, StartSession } from "./session.js";
59
+ export { CLAUDE_CODE_AGENT, CLAUDE_CODE_WRITE_INPUT, type ClaudeCodeInput } from "./spec.js";
135
60
  export { ANTHROPIC_HOST, claudeCodeEgress } from "./egress.js";
61
+ export { VERIFIED_CLAUDE_CODE_VERSION } from "./verified.js";
136
62
  export type { EgressConfig } from "./egress.js";
63
+ export { GITHUB_HOSTS, GITHUB_TOKEN_PLACEHOLDER } from "./github.js";
137
64
  export { credentialPool, readRefusal, readRateLimitEvent } from "./credentials.js";
138
65
  export type { CredentialPool, CredentialPoolConfig, CredentialState, CredentialStore, Lead, Refusal } from "./credentials.js";
139
- export { buildLaunch, CLAUDE_EXEC_PREFIX, CREDENTIAL_PLACEHOLDER, execIdFor, freshCursor } from "./run.js";
66
+ export { buildLaunch, CLAUDE_EXEC_PREFIX, CREDENTIAL_PLACEHOLDER, execIdFor, followUpExecIdFor, freshCursor } from "./run.js";
140
67
  export type { DrainCursor, DrainOptions, DrainOutcome, Launch, LaunchOptions, SessionRuntime } from "./run.js";
141
68
  export { parseStream, toProgress, RATE_LIMIT_OK } from "./events.js";
142
69
  export type { ClaudeCodeEvent, ClaudeCodeResult, ClaudeCodeUsage, RateLimitInfo } from "./events.js";
143
- export { CLAUDE_CODE_CAPABILITY, CLAUDE_CODE_RECIPE, CLAUDE_CODE_SPEC, CLAUDE_CODE_TYPE, WORKSPACE_RUNTIME_KEY } from "./recipe.js";
144
- export { DEFAULT_PERMISSION_MODE, DEFAULT_TIMEOUT_MS, DEFAULT_WINDOW_MS } from "./config.js";
70
+ export { DEFAULT_PERMISSION_MODE, DEFAULT_TIMEOUT_MS } from "./config.js";
145
71
  export type { ClaudeCodeConfig, EffortLevel, PermissionMode } from "./config.js";