@dynamicagents/plugins 0.12.0 → 0.13.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 (130) 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 +53 -0
  5. package/dist/claude-code/cards.js +272 -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 +342 -0
  18. package/dist/claude-code/run.d.ts +209 -112
  19. package/dist/claude-code/run.js +339 -138
  20. package/dist/claude-code/session.d.ts +131 -0
  21. package/dist/claude-code/session.js +285 -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-fault.d.ts +3 -3
  69. package/dist/{computer/host → workspace}/container-fault.js +23 -8
  70. package/dist/workspace/exec.d.ts +94 -0
  71. package/dist/workspace/exec.js +153 -0
  72. package/dist/workspace/format.d.ts +36 -0
  73. package/dist/workspace/format.js +57 -0
  74. package/dist/{computer/host → workspace}/git-host.d.ts +21 -4
  75. package/dist/{computer/host → workspace}/git-host.js +72 -2
  76. package/dist/{computer/host → workspace}/git-identity.d.ts +1 -1
  77. package/dist/{computer/host → workspace}/git-identity.js +1 -1
  78. package/dist/workspace/index.d.ts +43 -47
  79. package/dist/workspace/index.js +52 -112
  80. package/dist/{computer/host → workspace}/install-job.d.ts +4 -4
  81. package/dist/{computer/host → workspace}/install-job.js +276 -176
  82. package/dist/{computer → workspace}/install.d.ts +8 -8
  83. package/dist/{computer → workspace}/install.js +3 -3
  84. package/dist/{computer/host/workspace.d.ts → workspace/object.d.ts} +105 -27
  85. package/dist/{computer/host/workspace.js → workspace/object.js} +217 -48
  86. package/dist/workspace/open.d.ts +95 -0
  87. package/dist/workspace/open.js +63 -0
  88. package/dist/workspace/read.d.ts +16 -0
  89. package/dist/workspace/read.js +22 -0
  90. package/dist/{computer → workspace}/shell.d.ts +5 -5
  91. package/dist/{computer → workspace}/shell.js +6 -6
  92. package/dist/{computer/host → workspace}/wake.d.ts +1 -1
  93. package/package.json +16 -36
  94. package/dist/arc-agi/analysis.d.ts +0 -322
  95. package/dist/arc-agi/analysis.js +0 -802
  96. package/dist/arc-agi/client.d.ts +0 -82
  97. package/dist/arc-agi/client.js +0 -113
  98. package/dist/arc-agi/game-tools.d.ts +0 -24
  99. package/dist/arc-agi/game-tools.js +0 -26
  100. package/dist/arc-agi/index.d.ts +0 -40
  101. package/dist/arc-agi/index.js +0 -175
  102. package/dist/arc-agi/main-agent.d.ts +0 -24
  103. package/dist/arc-agi/main-agent.js +0 -62
  104. package/dist/arc-agi/recipe.d.ts +0 -48
  105. package/dist/arc-agi/recipe.js +0 -97
  106. package/dist/arc-agi/scorecard.d.ts +0 -118
  107. package/dist/arc-agi/scorecard.js +0 -163
  108. package/dist/arc-agi/soul.d.ts +0 -29
  109. package/dist/arc-agi/soul.js +0 -79
  110. package/dist/arc-agi/store.d.ts +0 -153
  111. package/dist/arc-agi/store.js +0 -215
  112. package/dist/arc-agi/tools.d.ts +0 -36
  113. package/dist/arc-agi/tools.js +0 -574
  114. package/dist/arc-agi/types.d.ts +0 -211
  115. package/dist/arc-agi/types.js +0 -19
  116. package/dist/claude-code/recipe.d.ts +0 -53
  117. package/dist/claude-code/recipe.js +0 -100
  118. package/dist/computer/read.d.ts +0 -133
  119. package/dist/computer/read.js +0 -200
  120. package/dist/computer/tools-find.js +0 -216
  121. package/dist/recall/index.d.ts +0 -105
  122. package/dist/recall/index.js +0 -181
  123. package/dist/repo/tools-forge.d.ts +0 -4
  124. package/dist/triage/index.d.ts +0 -360
  125. package/dist/triage/index.js +0 -311
  126. /package/dist/{computer/host → workspace}/container-deps.d.ts +0 -0
  127. /package/dist/{computer/host → workspace}/container-deps.js +0 -0
  128. /package/dist/{computer/host → workspace}/sync.d.ts +0 -0
  129. /package/dist/{computer/host → workspace}/sync.js +0 -0
  130. /package/dist/{computer/host → workspace}/wake.js +0 -0
@@ -2,97 +2,37 @@ import { tool } from "ai";
2
2
  import { z } from "zod";
3
3
  import { guardPath } from "./paths.js";
4
4
  import { withFileLock } from "./file-lock.js";
5
- import { readBounded, readWindow } from "./read.js";
6
- /** One file at a time: reading it, replacing it, editing it in place. */
5
+ /**
6
+ * Editing a file in place — the one file tool this plugin keeps. Think's
7
+ * `read`, `write` and `delete` reach the workspace through the agent's own
8
+ * `this.workspace`, which is `computerWorkspace` (`./proxy.ts`).
9
+ *
10
+ * Not Think's `edit`, because that one is two calls through the workspace — a
11
+ * `readFile`, then a `writeFile` — and a lock inside the workspace cannot span
12
+ * the pair. See `./file-lock.ts`.
13
+ */
7
14
  export function fileTools(ctx) {
8
- const { maxChars, inWorkspace, refuseWrite, scope } = ctx;
15
+ const { inWorkspace, refuseWrite, scope } = ctx;
9
16
  return {
10
- sb_read: tool({
11
- description: "Read a file from the workspace. Returns the file's text, or a note if it does not exist. " +
12
- "A large file comes back with its middle removed and a marker giving the `offset` that reaches the missing part. " +
13
- "Pass `offset` (and optionally `length`) to read a specific byte window instead — the result states the window it returned and how many bytes follow, so you can page through a file. Byte offsets, not lines: to see the lines around a match, use sb_grep with `context`. " +
14
- "Files under node_modules are not in the workspace — read them with sb_exec.",
15
- inputSchema: z.object({
16
- path: z
17
- .string()
18
- .describe("Absolute path, e.g. '/workspace/repo/src/a.ts'"),
19
- offset: z
20
- .number()
21
- .int()
22
- .min(0)
23
- .optional()
24
- .describe("Byte offset to start reading from"),
25
- length: z
26
- .number()
27
- .int()
28
- .min(1)
29
- .optional()
30
- .describe("Maximum bytes to return from `offset`")
31
- }),
32
- execute: async ({ path, offset, length }) => {
33
- const refusal = guardPath(path, "sb_read");
34
- if (refusal)
35
- return refusal;
36
- return inWorkspace("reading", path, async (fs) => {
37
- // Either knob means the model chose a region; only an unqualified read
38
- // gets the middle-out guess.
39
- return offset === undefined && length === undefined
40
- ? await readBounded(fs, path, maxChars)
41
- : await readWindow(fs, path, offset ?? 0, length ?? maxChars, maxChars);
42
- });
43
- }
44
- }),
45
- sb_write: tool({
46
- description: "Create or overwrite a file in the workspace. Parent directories are created for you. For a small change to a large file prefer sb_edit, which does not require sending the whole file back.",
47
- inputSchema: z.object({
48
- path: z.string().describe("Absolute path"),
49
- content: z
50
- .string()
51
- .describe("Full file content (overwrites any existing file)")
52
- }),
53
- execute: async ({ path, content }) => {
54
- const refusal = guardPath(path, "sb_write");
55
- if (refusal)
56
- return refusal;
57
- // Before the write, not after: the point is to not report a success that
58
- // did not happen.
59
- const lost = await refuseWrite();
60
- if (lost)
61
- return lost;
62
- return inWorkspace("writing", path, (fs) => withFileLock(scope(), path, async () => {
63
- const dir = path.slice(0, path.lastIndexOf("/"));
64
- if (dir)
65
- await fs.mkdir(dir, { recursive: true });
66
- await fs.writeFile(path, content);
67
- // Characters, not bytes. `String.length` counts UTF-16 code units — the
68
- // distinction `readBounded` documents at length — so calling them bytes
69
- // was simply wrong for anything outside ASCII. The exact byte count
70
- // would cost a `TextEncoder` pass over the whole content for a number
71
- // nobody does arithmetic with, and "characters" is already this module's
72
- // word for the same count in `truncateOutput`'s omission marker.
73
- const written = content.length;
74
- return `wrote ${path} (${written} character${written === 1 ? "" : "s"})`;
75
- }));
76
- }
77
- }),
78
- sb_edit: tool({
79
- description: "Replace an exact string in a file. The string must be non-empty and must appear exactly once — if it appears zero times or more than once the edit is refused, so include enough surrounding context to make it unique.",
17
+ edit: tool({
18
+ description: "Replace an exact string in a file. old_string must be non-empty and must appear exactly once — if it appears zero times or more than once the edit is refused, so include enough surrounding context to make it unique. To create a file, use write.",
80
19
  inputSchema: z.object({
81
20
  path: z.string().describe("Absolute path"),
82
- find: z
21
+ old_string: z
83
22
  .string()
84
23
  // Empty is refused at the schema rather than in the body, because
85
24
  // `split("")` does not count empty-string occurrences: on a file of
86
25
  // one character it reports none, on a longer one it reports one per
87
26
  // character, and on an *empty* file it reports -1 — which slips past
88
- // both guards below and writes `replace` into the file as if an edit
89
- // had been found. A write tool must not write what nobody asked for.
27
+ // both guards below and writes `new_string` into the file as if an
28
+ // edit had been found. A write tool must not write what nobody asked
29
+ // for.
90
30
  .min(1)
91
31
  .describe("Exact text to replace, unique within the file"),
92
- replace: z.string().describe("Replacement text")
32
+ new_string: z.string().describe("Replacement text")
93
33
  }),
94
- execute: async ({ path, find, replace }) => {
95
- const refusal = guardPath(path, "sb_edit");
34
+ execute: async ({ path, old_string, new_string }) => {
35
+ const refusal = guardPath(path, "edit");
96
36
  if (refusal)
97
37
  return refusal;
98
38
  const lost = await refuseWrite();
@@ -100,9 +40,9 @@ export function fileTools(ctx) {
100
40
  return lost;
101
41
  return inWorkspace("editing", path, (fs) => withFileLock(scope(), path, async () => {
102
42
  const content = await fs.readFile(path, "utf8");
103
- const occurrences = content.split(find).length - 1;
43
+ const occurrences = content.split(old_string).length - 1;
104
44
  // Refusing an ambiguous edit is the whole value of this tool over
105
- // sb_write: a silent first-match replace corrupts the file in a way
45
+ // `write`: a silent first-match replace corrupts the file in a way
106
46
  // that surfaces much later, usually as a confusing test failure.
107
47
  if (occurrences === 0)
108
48
  return `no match for that text in ${path}`;
@@ -115,7 +55,7 @@ export function fileTools(ctx) {
115
55
  // in everything before the match. Those two are not exotic — `$$`
116
56
  // escapes a dollar in a Makefile and reads a PID in shell, and `$'…'`
117
57
  // is bash ANSI-C quoting. A function's return value is used verbatim.
118
- await fs.writeFile(path, content.replace(find, () => replace));
58
+ await fs.writeFile(path, content.replace(old_string, () => new_string));
119
59
  return `edited ${path}`;
120
60
  }));
121
61
  }
@@ -1,3 +1,3 @@
1
1
  import type { ToolSet } from "ai";
2
2
  import type { ComputerContext } from "./context.js";
3
- export declare function findTools(ctx: ComputerContext): ToolSet;
3
+ export declare function grepTools(ctx: ComputerContext): ToolSet;
@@ -0,0 +1,129 @@
1
+ import { tool } from "ai";
2
+ import { z } from "zod";
3
+ import { guardPath, WALK_SKIPS } from "./paths.js";
4
+ import { renderGrepMatches } from "./render.js";
5
+ /**
6
+ * Searching file contents, under Think's name so it replaces Think's `grep` —
7
+ * which lists every file under the root and reads each one through the
8
+ * workspace, one round trip per file. Think's `find` and `list` stay Think's:
9
+ * over `computerWorkspace` they walk with `.git` and `node_modules` pruned.
10
+ */
11
+ /**
12
+ * How many matches one `grep` returns.
13
+ *
14
+ * Bounded at the source: without a `limit`, `fs.grep` reads *every* file under
15
+ * the path looking for more, so an unbounded search of `/workspace` answers only
16
+ * after streaming the whole tree through the isolate. The limit stops that walk
17
+ * early; `exclude` keeps it out of `.git` and the dependency tree entirely.
18
+ */
19
+ const DEFAULT_MAX_MATCHES = 200;
20
+ /**
21
+ * The most `context` lines one `grep` shows around each match.
22
+ *
23
+ * Clamped rather than refused, since a refusal costs the model a step to retry.
24
+ * The bound is {@link file://./render.ts packBlocks}: it ships the first match's
25
+ * block whole, so a block has to stay well inside the output budget.
26
+ */
27
+ const MAX_CONTEXT_LINES = 10;
28
+ export function grepTools(ctx) {
29
+ const { cwd, maxChars, inWorkspace } = ctx;
30
+ return {
31
+ /**
32
+ * Search, without the container.
33
+ *
34
+ * The alternative is `bash("grep -rn …")`, and the case for a native one is
35
+ * not that shelling out fails — it is where the search runs. `fs.grep` reads
36
+ * the Durable Object's SQLite, so it answers while the container is being
37
+ * replaced or an install is still running, which is exactly the window the
38
+ * install gate leaves an agent with nothing to do. It is also why this tool
39
+ * is not gated: see {@link file://./context.ts awaitAdvisories}.
40
+ *
41
+ * And not Think's `grep`, which globs every file under the root and reads
42
+ * each one through the workspace.
43
+ *
44
+ * Two lesser reasons that still matter. The query arrives as a value rather
45
+ * than through `shellQuote` and a shell that would re-parse it. And the result
46
+ * is bounded by a `limit` at the source instead of being middle-truncated
47
+ * afterwards, which for a match list means losing whole files silently.
48
+ */
49
+ grep: tool({
50
+ description: "Search file contents across the workspace. Returns matching lines grouped by file, each with its line number. " +
51
+ "The query is matched literally — set `regex` to interpret it as a regular expression. " +
52
+ "Pass `include` to limit which files are searched, e.g. '**/*.ts' — without it every file under `path` is read, which is slower and rarely what you meant. " +
53
+ "A cut result reports the `offset` that continues it. Use `context` to see the lines around a match. " +
54
+ "Results from `.git` and `node_modules` are left out — search node_modules with bash.",
55
+ inputSchema: z.object({
56
+ query: z.string().describe("Text to find, e.g. 'buildComputerTools'"),
57
+ path: z
58
+ .string()
59
+ .optional()
60
+ .describe(`Absolute file or directory to search (default: ${cwd})`),
61
+ include: z
62
+ .string()
63
+ .optional()
64
+ .describe("Glob relative to path limiting which files are searched, e.g. '**/*.ts'"),
65
+ regex: z
66
+ .boolean()
67
+ .optional()
68
+ .describe("Interpret query as a regular expression"),
69
+ ignoreCase: z.boolean().optional().describe("Ignore letter case"),
70
+ context: z
71
+ .number()
72
+ .int()
73
+ .min(0)
74
+ .optional()
75
+ .describe(`Lines of surrounding context to include with each match, at most ${MAX_CONTEXT_LINES}`),
76
+ offset: z
77
+ .number()
78
+ .int()
79
+ .min(0)
80
+ .optional()
81
+ .describe("Matches to skip — use the offset a cut result reports")
82
+ }),
83
+ execute: async ({ query, path, include, regex, ignoreCase, context, offset }) => {
84
+ const target = path ?? cwd;
85
+ const refusal = guardPath(target, "grep");
86
+ if (refusal)
87
+ return refusal;
88
+ const from = offset ?? 0;
89
+ const around = context === undefined
90
+ ? undefined
91
+ : Math.min(context, MAX_CONTEXT_LINES);
92
+ return inWorkspace("searching", target, async (fs) => {
93
+ // Pruned in the store, not filtered here: an excluded directory is
94
+ // never descended into, so `node_modules` costs nothing to skip and
95
+ // `offset` counts only matches the model can actually be shown. A
96
+ // filter on this side could not do either — it pages blind, so a
97
+ // crowded page could hide every real match behind skipped ones.
98
+ const matches = await fs.grep(query, target, {
99
+ include,
100
+ regex,
101
+ ignoreCase,
102
+ context: around,
103
+ limit: DEFAULT_MAX_MATCHES + 1,
104
+ offset: from,
105
+ exclude: WALK_SKIPS.map((segment) => `**/${segment}`)
106
+ });
107
+ if (matches.length === 0)
108
+ return `no matches for ${JSON.stringify(query)} in ${target}${include ? ` (${include})` : ""}${from > 0 ? ` past offset ${from}` : ""}`;
109
+ const { body, shown, capped } = renderGrepMatches(matches.slice(0, DEFAULT_MAX_MATCHES), maxChars);
110
+ return (body +
111
+ // `shown` falls short of what came back either because the page was
112
+ // capped or because the render budget ran out; both continue here.
113
+ (shown < matches.length
114
+ ? `\n\n… showed ${shown} matches; there are more. Continue with ` +
115
+ `\`offset: ${from + shown}\`, or narrow with \`include\` — ` +
116
+ `narrowing is cheaper, since an offset still walks everything ` +
117
+ `it skips.`
118
+ : "") +
119
+ (capped
120
+ ? `\n(Some lines were shortened. Read one in full with \`read\`, passing its line number as \`offset\`.)`
121
+ : "") +
122
+ (around !== context
123
+ ? `\n(\`context\` is at most ${MAX_CONTEXT_LINES}, so ${context} was clamped to ${MAX_CONTEXT_LINES}. Read a wider region with \`read\`.)`
124
+ : ""));
125
+ });
126
+ }
127
+ })
128
+ };
129
+ }
@@ -77,9 +77,8 @@ export declare function resolveDefaultBranch(plain: GitRunners["plain"], dir: st
77
77
  * on the commit itself, which is the one layer a stale config cannot outrank.
78
78
  * Underneath, a repository this plugin never cloned falls back to whatever the
79
79
  * shell's own git is configured with — which is an identity rather than nothing
80
- * only where `exec` is backed by the computer plugin, whose workspace writes one
81
- * per container; see
82
- * {@link file://../computer/host/git-identity.ts}. `exec` is an arbitrary
80
+ * only where `exec` is backed by `/workspace`, whose object writes one per
81
+ * container; see {@link file://../workspace/git-identity.ts}. `exec` is an arbitrary
83
82
  * runner, so that is a property of a deployment, not of this plugin.
84
83
  *
85
84
  * Unchecked, like the config pins at the clone: git's identity is needed by the
@@ -95,13 +94,13 @@ export declare function writeGitIdentity(plain: GitRunners["plain"], dir: string
95
94
  *
96
95
  * The workspace outlives the task, so a clone target may already hold one —
97
96
  * where `git clone` fails with "destination path already exists and is not an
98
- * empty directory" and the round has to improvise from an error that reads like
97
+ * empty directory" and the model has to improvise from an error that reads like
99
98
  * a bug.
100
99
  *
101
100
  * The refusal on a dirty tree is the important half. Uncommitted changes there
102
101
  * are a *previous task's work* — possibly the thing a human is waiting on — and
103
102
  * silently `reset --hard`ing them away to make a fresh clone look clean is the
104
- * one outcome nobody could recover from. Refusing costs a round; discarding
103
+ * one outcome nobody could recover from. Refusing costs a tool call; discarding
105
104
  * costs the work.
106
105
  */
107
106
  export declare function refreshCheckout({ dir, url, branch, author, plain, fetchOrigin }: GitRunners & {
@@ -34,9 +34,8 @@ export async function resolveDefaultBranch(plain, dir) {
34
34
  * on the commit itself, which is the one layer a stale config cannot outrank.
35
35
  * Underneath, a repository this plugin never cloned falls back to whatever the
36
36
  * shell's own git is configured with — which is an identity rather than nothing
37
- * only where `exec` is backed by the computer plugin, whose workspace writes one
38
- * per container; see
39
- * {@link file://../computer/host/git-identity.ts}. `exec` is an arbitrary
37
+ * only where `exec` is backed by `/workspace`, whose object writes one per
38
+ * container; see {@link file://../workspace/git-identity.ts}. `exec` is an arbitrary
40
39
  * runner, so that is a property of a deployment, not of this plugin.
41
40
  *
42
41
  * Unchecked, like the config pins at the clone: git's identity is needed by the
@@ -56,13 +55,13 @@ export async function writeGitIdentity(plain, dir, author) {
56
55
  *
57
56
  * The workspace outlives the task, so a clone target may already hold one —
58
57
  * where `git clone` fails with "destination path already exists and is not an
59
- * empty directory" and the round has to improvise from an error that reads like
58
+ * empty directory" and the model has to improvise from an error that reads like
60
59
  * a bug.
61
60
  *
62
61
  * The refusal on a dirty tree is the important half. Uncommitted changes there
63
62
  * are a *previous task's work* — possibly the thing a human is waiting on — and
64
63
  * silently `reset --hard`ing them away to make a fresh clone look clean is the
65
- * one outcome nobody could recover from. Refusing costs a round; discarding
64
+ * one outcome nobody could recover from. Refusing costs a tool call; discarding
66
65
  * costs the work.
67
66
  */
68
67
  export async function refreshCheckout({ dir, url, branch, author, plain, fetchOrigin }) {
@@ -99,7 +98,14 @@ export async function refreshCheckout({ dir, url, branch, author, plain, fetchOr
99
98
  // and `reset --hard`. Conflating the two lets a permissions error or a
100
99
  // half-written index discard a tree nobody established was clean, which is the
101
100
  // one loss in this file that cannot be undone.
102
- const dirty = await plain("status --porcelain", dir);
101
+ //
102
+ // Submodules are left out of the question. Neither `checkout` nor
103
+ // `reset --hard` below touches a submodule's tree without
104
+ // `--recurse-submodules`, so a submodule that has moved past its pin — which
105
+ // is what a superproject looks like after its own sync — is not work this
106
+ // refresh could lose. Counted as dirty, it would refuse every refresh of such
107
+ // a checkout, and a host's `afterCheckout` would never learn it is there.
108
+ const dirty = await plain("status --porcelain --ignore-submodules=all", dir);
103
109
  if (!dirty.success) {
104
110
  return {
105
111
  message: `could not read the state of the checkout at ${dir}: ` +
@@ -121,9 +127,26 @@ export async function refreshCheckout({ dir, url, branch, author, plain, fetchOr
121
127
  `Nothing was fetched or reset.`
122
128
  };
123
129
  }
130
+ /**
131
+ * A failed fetch leaves a checkout that is still there, still clean and still
132
+ * on its branch — only not updated. So it is still reported, on the branch it
133
+ * is on: a host that records checkouts would otherwise lose sight of one that
134
+ * exists because its remote could not be reached, and a clone taken from the
135
+ * recorded url goes to the remote itself. Nothing below — no checkout, no
136
+ * reset — runs against a tree whose remote state is unknown.
137
+ */
124
138
  const fetched = await fetchOrigin(dir, url);
125
- if (!fetched.success)
126
- return { message: `fetch failed: ${fetched.stderr || fetched.stdout}` };
139
+ if (!fetched.success) {
140
+ const failure = `fetch failed: ${fetched.stderr || fetched.stdout}`;
141
+ const current = await plain("symbolic-ref --quiet --short HEAD", dir);
142
+ const on = current.success ? current.stdout.trim() : "";
143
+ if (!on)
144
+ return { message: failure };
145
+ return {
146
+ message: `${failure}\nThe checkout was left on ${on}, clean but not updated from the remote.`,
147
+ branch: on
148
+ };
149
+ }
127
150
  // The branch to land on: the one asked for, else the remote's own default.
128
151
  let target = branch;
129
152
  if (!target) {
@@ -1,6 +1,7 @@
1
+ import type { ActionContext } from "@cloudflare/think";
1
2
  import type { RepoCheckout, RepoConfig, RepoExec, RepoGitResult } from "./index.js";
2
3
  /**
3
- * How many items one forge list call asks for. GitHub's maximum, and its default
4
+ * How many items one GitHub list call asks for. GitHub's maximum, and its default
4
5
  * without this is 30.
5
6
  *
6
7
  * One page, never a Link-header walk: an unbounded fetch loop does not belong
@@ -9,7 +10,7 @@ import type { RepoCheckout, RepoConfig, RepoExec, RepoGitResult } from "./index.
9
10
  * presented as the whole answer — a pull request's newest review comments are on
10
11
  * the last page, and those are exactly the ones an agent was sent to act on.
11
12
  */
12
- export declare const FORGE_PAGE_SIZE = 100;
13
+ export declare const GITHUB_PAGE_SIZE = 100;
13
14
  /**
14
15
  * Where GraphQL lives, given where REST does.
15
16
  *
@@ -24,10 +25,10 @@ export declare function graphqlEndpoint(apiBase: string): string;
24
25
  /**
25
26
  * Middle-out truncation, so both the head of a diff and its tail survive.
26
27
  *
27
- * Deliberately a **copy** of the computer plugin's function of the same name,
28
- * not an import. `npm run verify:exports` fails any subpath whose module graph
28
+ * Deliberately a **copy** of `/workspace`'s function of the same name, not an
29
+ * import. `npm run verify:exports` fails any subpath whose module graph
29
30
  * reaches a sibling's directory, and its own advice for a shared helper is to
30
- * duplicate it — installing `repo` must not drag `computer` (and therefore
31
+ * duplicate it — installing `repo` must not drag `/workspace` (and therefore
31
32
  * `@cloudflare/computer`) into a consumer's bundle. Fifteen lines is a cheaper
32
33
  * price than that coupling, and it is the same reason `exec` is injected here
33
34
  * rather than imported.
@@ -82,7 +83,7 @@ export interface RepoContext {
82
83
  };
83
84
  unreachable?: string;
84
85
  }>;
85
- forge: (tool: string, path: string, init?: {
86
+ github: (tool: string, path: string, init?: {
86
87
  method: string;
87
88
  body: unknown;
88
89
  }) => Promise<{
@@ -92,18 +93,43 @@ export interface RepoContext {
92
93
  ok: false;
93
94
  message: string;
94
95
  }>;
95
- forgeGraphql: (tool: string, query: string, variables: Record<string, unknown>) => Promise<{
96
+ githubGraphql: (tool: string, query: string, variables: Record<string, unknown>) => Promise<{
96
97
  ok: true;
97
98
  data: unknown;
98
99
  } | {
99
100
  ok: false;
100
101
  message: string;
101
102
  }>;
102
- forgeRepo: (dir: string) => Promise<{
103
+ githubRepo: (dir: string) => Promise<{
103
104
  owner: string;
104
105
  repo: string;
105
106
  } | {
106
107
  refusal: string;
107
108
  }>;
108
109
  }
109
- export declare function repoContext(config: RepoConfig, runtime?: unknown): RepoContext;
110
+ /**
111
+ * How long an action that calls GitHub may run before Think gives up on it.
112
+ *
113
+ * An action's default is thirty seconds, which is {@link GITHUB_TIMEOUT_MS}
114
+ * alone — and a thread reply is three GitHub calls, after an `origin` read that
115
+ * waits its turn in the git queue.
116
+ */
117
+ export declare const GITHUB_ACTION_TIMEOUT_MS = 120000;
118
+ /**
119
+ * The task an action runs for, which leads its idempotency key.
120
+ *
121
+ * Think replays a settled key for as long as it keeps the row
122
+ * (`actionLedgerRetention`), so a key without the task would replay, not post,
123
+ * the same comment in a later task. Every turn core runs carries a task id, so
124
+ * a missing one is a wiring fault — and keying without it would be the silent
125
+ * version of that fault.
126
+ */
127
+ export declare function actionTask(ctx: ActionContext): string;
128
+ /** A hex SHA-256, so a key names a body without carrying it. */
129
+ export declare function sha256(text: string): Promise<string>;
130
+ export declare function repoContext(config: RepoConfig,
131
+ /**
132
+ * The running sub-agent's `runtime()`, read per command. Forwarded to every
133
+ * `exec`; see {@link RepoExec}'s `runtime` option.
134
+ */
135
+ runtime?: () => unknown): RepoContext;