@gajae-code/coding-agent 0.11.3 → 0.11.5

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 (213) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/types/cli/notify-cli.d.ts +5 -1
  3. package/dist/types/cli/worktree-cli.d.ts +8 -25
  4. package/dist/types/commands/gc.d.ts +4 -0
  5. package/dist/types/commands/worktree.d.ts +7 -32
  6. package/dist/types/config/settings-schema.d.ts +20 -0
  7. package/dist/types/export/html/template.generated.d.ts +1 -1
  8. package/dist/types/gjc-runtime/deep-interview-state.d.ts +28 -0
  9. package/dist/types/gjc-runtime/gc-runtime.d.ts +9 -0
  10. package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -0
  11. package/dist/types/gjc-runtime/managed-owner-admission.d.ts +42 -0
  12. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +41 -0
  13. package/dist/types/gjc-runtime/state-argv.d.ts +38 -0
  14. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +9 -0
  15. package/dist/types/gjc-runtime/ultragoal-owner-loss-recovery.d.ts +94 -0
  16. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
  17. package/dist/types/harness-control-plane/owner.d.ts +3 -1
  18. package/dist/types/internal-urls/local-protocol.d.ts +18 -0
  19. package/dist/types/main.d.ts +2 -2
  20. package/dist/types/modes/components/ansi-display-validator.d.ts +2 -0
  21. package/dist/types/modes/components/custom-editor.d.ts +8 -3
  22. package/dist/types/modes/components/notifications-settings-editor.d.ts +4 -0
  23. package/dist/types/modes/components/session-observer-overlay.d.ts +3 -1
  24. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  25. package/dist/types/modes/components/tool-transcript-format.d.ts +77 -0
  26. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +3 -0
  27. package/dist/types/modes/controllers/goal-mode-controller.d.ts +8 -1
  28. package/dist/types/modes/controllers/input-controller.d.ts +8 -2
  29. package/dist/types/modes/controllers/selector-controller.d.ts +15 -0
  30. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +3 -0
  31. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +16 -0
  32. package/dist/types/plan-mode/approved-plan.d.ts +1 -0
  33. package/dist/types/runtime-mcp/manager.d.ts +2 -0
  34. package/dist/types/runtime-mcp/oauth-discovery.d.ts +12 -1
  35. package/dist/types/sdk/broker/broker.d.ts +1 -0
  36. package/dist/types/sdk/broker/discovery.d.ts +10 -0
  37. package/dist/types/sdk/broker/ensure.d.ts +24 -1
  38. package/dist/types/sdk/broker/internal.d.ts +3 -0
  39. package/dist/types/sdk/broker/session-index.d.ts +12 -0
  40. package/dist/types/sdk/broker/state-version.d.ts +2 -1
  41. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -1
  42. package/dist/types/sdk/bus/chat-daemon-control.d.ts +39 -4
  43. package/dist/types/sdk/bus/config-commands.d.ts +8 -0
  44. package/dist/types/sdk/bus/config.d.ts +22 -0
  45. package/dist/types/sdk/bus/index.d.ts +162 -2
  46. package/dist/types/sdk/bus/notification-service.d.ts +71 -8
  47. package/dist/types/sdk/bus/session-control.d.ts +11 -8
  48. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +5 -0
  49. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -3
  50. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +23 -4
  51. package/dist/types/sdk/bus/telegram-daemon.d.ts +214 -20
  52. package/dist/types/sdk/session.d.ts +16 -1
  53. package/dist/types/secrets/index.d.ts +4 -2
  54. package/dist/types/secrets/obfuscator.d.ts +1 -1
  55. package/dist/types/session/agent-session.d.ts +31 -2
  56. package/dist/types/session/artifacts.d.ts +30 -8
  57. package/dist/types/session/internal/managed-session-scope.d.ts +7 -0
  58. package/dist/types/session/internal/managed-session-storage.d.ts +63 -6
  59. package/dist/types/session/session-manager.d.ts +88 -18
  60. package/dist/types/session/session-storage.d.ts +27 -0
  61. package/dist/types/session/streaming-output.d.ts +4 -0
  62. package/dist/types/task/executor.d.ts +9 -0
  63. package/dist/types/tool-discovery/tool-index.d.ts +2 -0
  64. package/dist/types/tools/ask.d.ts +87 -12
  65. package/dist/types/tools/bash-skill-urls.d.ts +2 -2
  66. package/dist/types/tools/browser/screenshot-format.d.ts +8 -0
  67. package/dist/types/tools/browser/tab-protocol.d.ts +11 -0
  68. package/dist/types/tools/browser/tab-supervisor.d.ts +1 -1
  69. package/dist/types/tools/browser/tab-worker-path-resolver.d.ts +2 -0
  70. package/dist/types/tools/browser/tab-worker-smoke.d.ts +16 -0
  71. package/dist/types/tools/index.d.ts +4 -0
  72. package/dist/types/tools/path-utils.d.ts +9 -14
  73. package/dist/types/tools/render-utils.d.ts +1 -8
  74. package/dist/types/tools/resource-gc.d.ts +14 -0
  75. package/dist/types/utils/image-loading.d.ts +23 -0
  76. package/dist/types/utils/image-resize.d.ts +2 -0
  77. package/dist/types/utils/pasted-image-loading.d.ts +39 -0
  78. package/dist/types/utils/pasted-image-path.d.ts +21 -19
  79. package/dist/types/web/scrapers/html-to-markdown.d.ts +5 -0
  80. package/dist/types/web/scrapers/types.d.ts +1 -5
  81. package/package.json +25 -9
  82. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  83. package/scripts/run-threat-model-check.ts +60 -0
  84. package/src/cli/notify-cli.ts +40 -18
  85. package/src/cli/worktree-cli.ts +9 -287
  86. package/src/cli.ts +10 -9
  87. package/src/commands/gc.ts +11 -1
  88. package/src/commands/sdk.ts +10 -4
  89. package/src/commands/worktree.ts +10 -53
  90. package/src/config/model-profiles.ts +15 -15
  91. package/src/config/settings-schema.ts +17 -0
  92. package/src/daemon/runtime.ts +2 -2
  93. package/src/defaults/gjc/skills/deep-interview/SKILL.md +70 -49
  94. package/src/defaults/gjc/skills/ralplan/SKILL.md +0 -6
  95. package/src/defaults/gjc/skills/team/SKILL.md +7 -9
  96. package/src/defaults/gjc/skills/ultragoal/SKILL.md +0 -2
  97. package/src/eval/js/shared/runtime.ts +1 -1
  98. package/src/eval/py/index.ts +2 -2
  99. package/src/export/html/template.generated.ts +1 -1
  100. package/src/extensibility/plugins/marketplace/manager.ts +94 -10
  101. package/src/gjc-runtime/deep-interview-recorder.ts +7 -0
  102. package/src/gjc-runtime/deep-interview-runtime.ts +10 -0
  103. package/src/gjc-runtime/deep-interview-state.ts +127 -3
  104. package/src/gjc-runtime/gc-render.ts +19 -1
  105. package/src/gjc-runtime/gc-runtime.ts +68 -3
  106. package/src/gjc-runtime/launch-tmux.ts +61 -6
  107. package/src/gjc-runtime/managed-owner-admission.ts +352 -0
  108. package/src/gjc-runtime/managed-owner-supervisor.ts +162 -0
  109. package/src/gjc-runtime/state-argv.ts +292 -0
  110. package/src/gjc-runtime/state-runtime.ts +80 -222
  111. package/src/gjc-runtime/team-runtime.ts +1 -1
  112. package/src/gjc-runtime/tmux-owner-isolation.ts +117 -0
  113. package/src/gjc-runtime/ultragoal-owner-loss-recovery.ts +328 -0
  114. package/src/gjc-runtime/ultragoal-runtime.ts +14 -0
  115. package/src/harness-control-plane/owner.ts +59 -3
  116. package/src/hindsight/client.ts +65 -2
  117. package/src/internal-urls/agent-protocol.ts +76 -5
  118. package/src/internal-urls/docs-index.generated.ts +7 -6
  119. package/src/internal-urls/local-protocol.ts +416 -18
  120. package/src/lsp/index.ts +4 -88
  121. package/src/main.ts +37 -13
  122. package/src/modes/acp/acp-agent.ts +8 -0
  123. package/src/modes/components/ansi-display-validator.ts +54 -0
  124. package/src/modes/components/custom-editor.ts +103 -32
  125. package/src/modes/components/custom-provider-wizard.ts +26 -1
  126. package/src/modes/components/notifications-settings-editor.ts +26 -2
  127. package/src/modes/components/session-observer-overlay.ts +40 -37
  128. package/src/modes/components/settings-selector.ts +3 -1
  129. package/src/modes/components/tool-transcript-format.ts +393 -0
  130. package/src/modes/components/transcript-viewer-overlay.ts +63 -1
  131. package/src/modes/controllers/goal-mode-controller.ts +20 -1
  132. package/src/modes/controllers/input-controller.ts +98 -37
  133. package/src/modes/controllers/plan-mode-controller.ts +2 -0
  134. package/src/modes/controllers/runtime-mcp-command-controller.ts +3 -0
  135. package/src/modes/controllers/selector-controller.ts +71 -9
  136. package/src/modes/interactive-mode.ts +14 -2
  137. package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -1
  138. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +17 -1
  139. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +4 -2
  140. package/src/modes/shared/agent-wire/workflow-gate-types.ts +120 -3
  141. package/src/plan-mode/approved-plan.ts +3 -1
  142. package/src/prompts/agents/architect.md +1 -0
  143. package/src/prompts/agents/critic.md +1 -0
  144. package/src/prompts/system/custom-system-prompt.md +1 -1
  145. package/src/prompts/system/system-prompt.md +2 -1
  146. package/src/prompts/tools/lsp.md +3 -3
  147. package/src/runtime-mcp/manager.ts +16 -1
  148. package/src/runtime-mcp/oauth-discovery.ts +116 -26
  149. package/src/sdk/broker/broker.ts +139 -32
  150. package/src/sdk/broker/discovery.ts +125 -0
  151. package/src/sdk/broker/ensure.ts +103 -19
  152. package/src/sdk/broker/internal.ts +10 -0
  153. package/src/sdk/broker/session-index.ts +309 -38
  154. package/src/sdk/broker/state-version.ts +5 -2
  155. package/src/sdk/bus/chat-daemon-cli.ts +72 -10
  156. package/src/sdk/bus/chat-daemon-control.ts +526 -130
  157. package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
  158. package/src/sdk/bus/config-commands.ts +22 -0
  159. package/src/sdk/bus/config.ts +37 -0
  160. package/src/sdk/bus/index.ts +384 -92
  161. package/src/sdk/bus/lifecycle-control-runtime.ts +53 -3
  162. package/src/sdk/bus/notification-service.ts +284 -62
  163. package/src/sdk/bus/recent-activity.ts +14 -5
  164. package/src/sdk/bus/session-control.ts +150 -44
  165. package/src/sdk/bus/telegram-daemon-cli.ts +20 -14
  166. package/src/sdk/bus/telegram-daemon-contract.ts +6 -3
  167. package/src/sdk/bus/telegram-daemon-control.ts +294 -76
  168. package/src/sdk/bus/telegram-daemon.ts +1842 -281
  169. package/src/sdk/protocol/operation-inventory.generated.json +11 -0
  170. package/src/sdk/session.ts +155 -27
  171. package/src/secrets/index.ts +53 -10
  172. package/src/secrets/obfuscator.ts +21 -18
  173. package/src/secrets/regex.ts +2 -0
  174. package/src/session/agent-session.ts +495 -253
  175. package/src/session/artifacts.ts +188 -23
  176. package/src/session/context-estimation.ts +7 -2
  177. package/src/session/internal/managed-session-scope.ts +215 -39
  178. package/src/session/internal/managed-session-storage.ts +1152 -46
  179. package/src/session/session-manager.ts +1055 -148
  180. package/src/session/session-storage.ts +109 -11
  181. package/src/session/streaming-output.ts +6 -0
  182. package/src/task/executor.ts +71 -26
  183. package/src/task/index.ts +63 -20
  184. package/src/tool-discovery/tool-index.ts +18 -0
  185. package/src/tools/ask.ts +329 -57
  186. package/src/tools/bash-allowed-prefixes.ts +79 -29
  187. package/src/tools/bash-skill-urls.ts +14 -3
  188. package/src/tools/bash.ts +2 -2
  189. package/src/tools/browser/launch.ts +3 -1
  190. package/src/tools/browser/readable.ts +1 -1
  191. package/src/tools/browser/screenshot-format.ts +34 -0
  192. package/src/tools/browser/tab-protocol.ts +3 -0
  193. package/src/tools/browser/tab-supervisor.ts +125 -122
  194. package/src/tools/browser/tab-worker-entry.ts +33 -14
  195. package/src/tools/browser/tab-worker-path-resolver.ts +67 -0
  196. package/src/tools/browser/tab-worker-smoke.ts +87 -0
  197. package/src/tools/browser/tab-worker.ts +7 -5
  198. package/src/tools/index.ts +4 -0
  199. package/src/tools/path-utils.ts +42 -35
  200. package/src/tools/plan-mode-guard.ts +1 -0
  201. package/src/tools/read.ts +7 -1
  202. package/src/tools/render-utils.ts +1 -27
  203. package/src/tools/resource-gc.ts +123 -34
  204. package/src/tools/sqlite-reader.ts +3 -2
  205. package/src/tools/subagent.ts +2 -0
  206. package/src/utils/image-loading.ts +85 -34
  207. package/src/utils/image-resize.ts +16 -9
  208. package/src/utils/pasted-image-loading.ts +514 -0
  209. package/src/utils/pasted-image-path.ts +223 -98
  210. package/src/web/scrapers/html-to-markdown.ts +68 -0
  211. package/src/web/scrapers/readthedocs.ts +1 -1
  212. package/src/web/scrapers/types.ts +1 -68
  213. package/src/web/search/providers/openai-compatible.ts +76 -23
@@ -1,3 +1,11 @@
1
+ import {
2
+ classifyStateArgv,
3
+ STATE_ACTION_NAMES,
4
+ type StateAction,
5
+ type StateArgvClassification,
6
+ } from "../gjc-runtime/state-argv";
7
+ import { CANONICAL_GJC_WORKFLOW_SKILLS } from "../skill-state/canonical-skills";
8
+
1
9
  export interface BashAllowedPrefixesCheck {
2
10
  allowed: boolean;
3
11
  reason?: string;
@@ -11,9 +19,8 @@ export interface BashRestrictionOptions {
11
19
 
12
20
  const SHELL_CONTROL_CHARS = new Set([";", "|", "&", "<", ">", "(", ")"]);
13
21
  const UNSAFE_UNQUOTED_EXPANSION_CHARS = new Set(["$", "*", "?", "[", "]", "{", "}", "~"]);
14
- const STATE_FLAGS_WITH_VALUES = new Set(["--input", "--mode", "--session-id", "--thread-id", "--turn-id", "--to"]);
15
- const STATE_ACTIONS = new Set(["read", "write", "clear", "contract", "handoff"]);
16
22
  const ALLOWED_STATE_ACTIONS = new Set(["read", "write", "contract"]);
23
+ const CANONICAL_STATE_TARGETS = new Set<string>(CANONICAL_GJC_WORKFLOW_SKILLS);
17
24
  const READ_ONLY_COMMANDS = new Set(["grep", "rg", "tree", "ls", "pwd", "wc", "du", "file", "stat"]);
18
25
 
19
26
  function parseShellWords(command: string): { words: string[]; reason?: string } {
@@ -21,6 +28,7 @@ function parseShellWords(command: string): { words: string[]; reason?: string }
21
28
  let current = "";
22
29
  let quote: "single" | "double" | null = null;
23
30
 
31
+ let wordStarted = false;
24
32
  for (let index = 0; index < command.length; index += 1) {
25
33
  const char = command[index]!;
26
34
  const next = command[index + 1];
@@ -54,10 +62,12 @@ function parseShellWords(command: string): { words: string[]; reason?: string }
54
62
 
55
63
  if (char === "'") {
56
64
  quote = "single";
65
+ wordStarted = true;
57
66
  continue;
58
67
  }
59
68
  if (char === '"') {
60
69
  quote = "double";
70
+ wordStarted = true;
61
71
  continue;
62
72
  }
63
73
  if (char === "`" || (char === "$" && next === "(")) {
@@ -73,9 +83,10 @@ function parseShellWords(command: string): { words: string[]; reason?: string }
73
83
  return { words, reason: `shell expansion character '${char}' is not allowed in restricted bash commands` };
74
84
  }
75
85
  if (/\s/u.test(char)) {
76
- if (current.length > 0) {
86
+ if (wordStarted) {
77
87
  words.push(current);
78
88
  current = "";
89
+ wordStarted = false;
79
90
  }
80
91
  continue;
81
92
  }
@@ -83,12 +94,13 @@ function parseShellWords(command: string): { words: string[]; reason?: string }
83
94
  return { words, reason: "backslash escapes are not allowed in restricted bash commands" };
84
95
  }
85
96
  current += char;
97
+ wordStarted = true;
86
98
  }
87
99
 
88
100
  if (quote !== null) {
89
101
  return { words, reason: "unterminated quote in restricted bash command" };
90
102
  }
91
- if (current.length > 0) words.push(current);
103
+ if (wordStarted) words.push(current);
92
104
  return { words };
93
105
  }
94
106
 
@@ -101,28 +113,62 @@ function wordsStartWith(words: readonly string[], prefix: readonly string[]): bo
101
113
  return prefix.every((word, index) => words[index] === word);
102
114
  }
103
115
 
104
- function parseStateAction(words: readonly string[]): string | undefined {
105
- const args = words.slice(2);
106
- const positional: string[] = [];
107
- let skipNext = false;
108
- for (const arg of args) {
109
- if (skipNext) {
110
- skipNext = false;
111
- continue;
112
- }
113
- if (STATE_FLAGS_WITH_VALUES.has(arg)) {
114
- skipNext = true;
115
- continue;
116
- }
117
- if (!arg.startsWith("-")) positional.push(arg);
116
+ function malformedStateShape(): StateActionClassification {
117
+ return { reason: "restricted role-agent bash only allows documented `gjc state` action shapes" };
118
+ }
119
+
120
+ interface ParsedStateAction {
121
+ action: string;
122
+ target: string;
123
+ }
124
+
125
+ interface StateActionClassification {
126
+ parsed?: ParsedStateAction;
127
+ reason?: string;
128
+ }
129
+
130
+ function hasDocumentedStatePositionals(classification: StateArgvClassification): boolean {
131
+ const [first, second, third] = classification.positionals;
132
+ if (third) return false;
133
+ if (!second) return true;
134
+ return STATE_ACTION_NAMES.has(first as StateAction) || STATE_ACTION_NAMES.has(second as StateAction);
135
+ }
136
+
137
+ function classifyStateAction(words: readonly string[]): StateActionClassification {
138
+ const classification = classifyStateArgv(words.slice(2));
139
+ if (
140
+ classification.unknownFlags.length > 0 ||
141
+ classification.flags.some(flag => flag.malformed) ||
142
+ !hasDocumentedStatePositionals(classification)
143
+ ) {
144
+ return malformedStateShape();
145
+ }
146
+
147
+ const modeFlags = classification.flags.filter(flag => flag.name === "--mode");
148
+ const inputFlags = classification.flags.filter(flag => flag.name === "--input");
149
+ if (modeFlags.length > 1 || inputFlags.length > 1) {
150
+ return { reason: "restricted role-agent bash rejects repeated or conflicting `gjc state` target selectors" };
151
+ }
152
+ if (classification.inputs.some(input => input.kind === "file")) {
153
+ return { reason: "restricted role-agent bash does not allow file-backed `gjc state --input` values" };
118
154
  }
155
+ if (classification.inputs.some(input => input.kind === "invalid")) return malformedStateShape();
119
156
 
120
- const [first, second, third] = positional;
121
- if (!first) return "read";
122
- if (STATE_ACTIONS.has(first)) return second ? undefined : first;
123
- if (!second) return "read";
124
- if (!STATE_ACTIONS.has(second)) return undefined;
125
- return third ? undefined : second;
157
+ const runtimeTarget = classification.runtimeSelectorCandidates.find(candidate => candidate.value)?.value;
158
+ const suppliedTargets = classification.selectorCandidates
159
+ .map(candidate => candidate.value)
160
+ .filter((target): target is string => target !== undefined);
161
+ const distinctTargets = new Set(suppliedTargets);
162
+ if (distinctTargets.size > 1 || (runtimeTarget && suppliedTargets.some(target => target !== runtimeTarget))) {
163
+ return {
164
+ reason:
165
+ "restricted role-agent bash rejects conflicting `gjc state` selectors that disagree with runtime precedence",
166
+ };
167
+ }
168
+ if (!runtimeTarget || !CANONICAL_STATE_TARGETS.has(runtimeTarget)) {
169
+ return { reason: "restricted role-agent bash requires a canonical workflow skill for `gjc state` commands" };
170
+ }
171
+ return { parsed: { action: classification.effectiveAction, target: runtimeTarget } };
126
172
  }
127
173
 
128
174
  function optionWords(words: readonly string[]): string[] {
@@ -189,15 +235,19 @@ function validateMatchedGjcCommand(words: readonly string[]): BashAllowedPrefixe
189
235
  }
190
236
 
191
237
  if (words[1] === "state") {
192
- const action = parseStateAction(words);
193
- if (!action) {
238
+ const classification = classifyStateAction(words);
239
+ if (!classification.parsed) {
194
240
  return {
195
241
  allowed: false,
196
- reason: "restricted role-agent bash only allows documented `gjc state` action shapes",
242
+ reason:
243
+ classification.reason ?? "restricted role-agent bash only allows documented `gjc state` action shapes",
197
244
  };
198
245
  }
199
- if (!ALLOWED_STATE_ACTIONS.has(action)) {
200
- return { allowed: false, reason: `restricted role-agent bash does not allow \`gjc state ${action}\`` };
246
+ if (!ALLOWED_STATE_ACTIONS.has(classification.parsed.action)) {
247
+ return {
248
+ allowed: false,
249
+ reason: `restricted role-agent bash does not allow \`gjc state ${classification.parsed.action}\``,
250
+ };
201
251
  }
202
252
  return { allowed: true };
203
253
  }
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import type { Skill } from "../extensibility/skills";
4
- import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
4
+ import { initializeLocalRoot, type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
5
5
  import { validateRelativePath } from "../internal-urls/skill-protocol";
6
6
  import type { InternalResource } from "../internal-urls/types";
7
7
  import { normalizeLocalScheme } from "./path-utils";
@@ -162,12 +162,23 @@ async function resolveInternalUrlToPath(
162
162
  return resolveSkillUrlToPath(url, skills);
163
163
  }
164
164
 
165
+ // Managed artifact and agent resources carry session authority. Expanding
166
+ // either into a subprocess command would let that subprocess reopen, replace,
167
+ // or unlink the managed object by pathname. Keep those resources behind the
168
+ // internal reader boundary.
169
+ if (scheme === "artifact" || scheme === "agent") {
170
+ throw new ToolError(
171
+ `${scheme}:// URLs cannot be expanded in bash commands; read them through the internal reader instead.`,
172
+ );
173
+ }
174
+
165
175
  if (scheme === "local") {
166
176
  if (!localOptions) {
167
177
  throw new ToolError(
168
178
  "Cannot resolve local:// URL in bash command: local protocol options are unavailable for this session.",
169
179
  );
170
180
  }
181
+ await initializeLocalRoot(localOptions);
171
182
  const resolvedLocalPath = resolveLocalUrlToPath(url, localOptions);
172
183
  if (ensureLocalParentDirs) {
173
184
  await fs.mkdir(path.dirname(resolvedLocalPath), { recursive: true });
@@ -215,8 +226,8 @@ export function expandSkillUrls(command: string, skills: readonly Skill[]): stri
215
226
  }
216
227
 
217
228
  /**
218
- * Expand supported internal URLs in a bash command string to shell-escaped absolute paths.
219
- * Supported schemes: skill://, agent://, artifact://, memory://, rule://, local://
229
+ * Expand filesystem-safe internal URLs in a bash command string. `agent://` and
230
+ * `artifact://` remain internal-reader-only and are rejected before resolution.
220
231
  */
221
232
  export async function expandInternalUrls(command: string, options: InternalUrlExpansionOptions): Promise<string> {
222
233
  if (!command.includes("://") && !command.includes("local:/")) return command;
package/src/tools/bash.ts CHANGED
@@ -636,6 +636,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
636
636
  internalRouter: InternalUrlRouter.instance(),
637
637
  localOptions: {
638
638
  getArtifactsDir: this.session.getArtifactsDir,
639
+ isManagedDestination: this.session.isManagedSessionDestination,
639
640
  getSessionId: this.session.getSessionId,
640
641
  },
641
642
  };
@@ -643,7 +644,6 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
643
644
  ...internalUrlOptions,
644
645
  ensureLocalParentDirs: this.session.bashRestrictionProfile !== "read-only",
645
646
  });
646
- const sessionFile = this.session.getSessionFile?.() ?? null;
647
647
  const expandedEnv = env
648
648
  ? Object.fromEntries(
649
649
  await Promise.all(
@@ -662,7 +662,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
662
662
  : undefined;
663
663
  const resolvedEnv = {
664
664
  ...buildGjcRuntimeSessionEnv({
665
- sessionFile,
665
+ sessionFile: null,
666
666
  sessionId: this.session.getSessionId?.(),
667
667
  cwd: this.session.cwd,
668
668
  }),
@@ -1,7 +1,9 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
- import { $which, getPuppeteerDir, logger } from "@gajae-code/utils";
4
+ import { getPuppeteerDir } from "@gajae-code/utils/dirs";
5
+ import * as logger from "@gajae-code/utils/logger";
6
+ import { $which } from "@gajae-code/utils/which";
5
7
  import * as browsers from "@puppeteer/browsers";
6
8
  import type { Browser, CDPSession, Page, default as Puppeteer, Target } from "puppeteer-core";
7
9
  import { PUPPETEER_REVISIONS } from "puppeteer-core/internal/revisions.js";
@@ -1,6 +1,6 @@
1
1
  import { Readability } from "@mozilla/readability";
2
2
  import { parseHtmlLazy } from "../../utils/linkedom";
3
- import { htmlToBasicMarkdown } from "../../web/scrapers/types";
3
+ import { htmlToBasicMarkdown } from "../../web/scrapers/html-to-markdown";
4
4
 
5
5
  export type ReadableFormat = "text" | "markdown";
6
6
 
@@ -0,0 +1,34 @@
1
+ import * as os from "node:os";
2
+
3
+ import { formatDimensionNote, type ResizedImage } from "../../utils/image-resize";
4
+
5
+ function shortenPath(filePath: string): string {
6
+ const home = os.homedir();
7
+ return home && filePath.startsWith(home) ? `~${filePath.slice(home.length)}` : filePath;
8
+ }
9
+
10
+ export function formatScreenshot(opts: {
11
+ saveFullRes: boolean;
12
+ savedMimeType: string;
13
+ savedByteLength: number;
14
+ dest: string;
15
+ resized: ResizedImage;
16
+ }): string[] {
17
+ const lines = ["Screenshot captured"];
18
+ if (opts.saveFullRes) {
19
+ lines.push(
20
+ `Saved: ${opts.savedMimeType} (${(opts.savedByteLength / 1024).toFixed(2)} KB) to ${shortenPath(opts.dest)}`,
21
+ );
22
+ lines.push(
23
+ `Model: ${opts.resized.mimeType} (${(opts.resized.buffer.length / 1024).toFixed(2)} KB, ${opts.resized.width}x${opts.resized.height})`,
24
+ );
25
+ } else {
26
+ lines.push(`Format: ${opts.resized.mimeType} (${(opts.resized.buffer.length / 1024).toFixed(2)} KB)`);
27
+ lines.push(`Dimensions: ${opts.resized.width}x${opts.resized.height}`);
28
+ }
29
+ const dimensionNote = formatDimensionNote(opts.resized);
30
+ if (dimensionNote) {
31
+ lines.push(dimensionNote);
32
+ }
33
+ return lines;
34
+ }
@@ -63,6 +63,7 @@ export type WorkerInitPayload =
63
63
  export type ToolReply = { ok: true; value: unknown } | { ok: false; error: RunErrorPayload };
64
64
 
65
65
  export type WorkerInbound =
66
+ | { type: "bootstrap"; version: 1; mode: "native-free" }
66
67
  | { type: "init"; payload: WorkerInitPayload }
67
68
  | { type: "run"; id: string; name: string; code: string; timeoutMs: number; session: SessionSnapshot }
68
69
  | { type: "abort"; id: string }
@@ -91,6 +92,8 @@ export interface RunErrorPayload {
91
92
  }
92
93
 
93
94
  export type WorkerOutbound =
95
+ | { type: "bootstrap-ready"; version: 1; mode: "native-free" }
96
+ | { type: "bootstrap-failed"; error: string }
94
97
  | { type: "ready"; info: ReadyInfo }
95
98
  | { type: "init-failed"; error: RunErrorPayload }
96
99
  | { type: "result"; id: string; ok: true; payload: RunResultOk }
@@ -19,7 +19,6 @@ import type {
19
19
  RunResultOk,
20
20
  SessionSnapshot,
21
21
  Transferable,
22
- Transport,
23
22
  WorkerInbound,
24
23
  WorkerInitPayload,
25
24
  WorkerOutbound,
@@ -38,8 +37,8 @@ interface WorkerHandle {
38
37
  send(msg: WorkerInbound, transferList?: Transferable[]): void;
39
38
  onMessage(handler: (msg: WorkerOutbound) => void): () => void;
40
39
  onError(handler: (error: Error) => void): () => void;
40
+ onClose(handler: () => void): () => void;
41
41
  terminate(): Promise<void>;
42
- readonly mode: "worker" | "inline";
43
42
  }
44
43
 
45
44
  export type DialogPolicy = "accept" | "dismiss";
@@ -118,6 +117,13 @@ let workerInitializerForTest:
118
117
  | ((worker: WorkerHandle, payload: WorkerInitPayload, timeoutMs: number) => Promise<ReadyInfo>)
119
118
  | undefined;
120
119
  const GRACE_MS = 750;
120
+ const TAB_WORKER_MODE = "native-free";
121
+
122
+ function startupError(stage: string): ToolError {
123
+ return new ToolError(
124
+ `Tab worker startup failed (stage=${stage}, mode=${TAB_WORKER_MODE}, platform=${process.platform}).`,
125
+ );
126
+ }
121
127
 
122
128
  interface DeadTabDescriptor {
123
129
  tab: TabSession;
@@ -152,7 +158,7 @@ async function awaitWithinBudget(op: Promise<unknown>, remainingMs: number, labe
152
158
  logger.debug("close teardown budget already spent; detaching step", { label });
153
159
  return false;
154
160
  }
155
- let timer: ReturnType<typeof setTimeout> | undefined;
161
+ let timer: NodeJS.Timeout | undefined;
156
162
  const timedOut = new Promise<boolean>(resolve => {
157
163
  timer = setTimeout(() => resolve(false), remainingMs);
158
164
  });
@@ -358,64 +364,69 @@ export async function acquireTab(
358
364
  if (replacementHold) await releaseBrowser(browser, { kill: false });
359
365
  throw error;
360
366
  }
361
- let worker = await spawnAcquireWorker();
362
- let info: ReadyInfo;
367
+ let worker: WorkerHandle;
363
368
  try {
364
- info = await initializeAcquireWorker(worker, initPayload, opts.timeoutMs + GRACE_MS);
365
- } catch (error) {
366
- // `BuildMessage`-class failures arrive asynchronously via the worker's `error` event,
367
- // after `spawnTabWorker`'s synchronous try/catch has already returned. Fall back to
368
- // the inline worker here so module-resolution failures don't poison every tab open.
369
- await worker.terminate().catch(() => undefined);
370
- if (worker.mode === "inline") {
371
- if (replacementHold) await releaseBrowser(browser, { kill: false });
372
- else if (browser.refCount === 0) await releaseBrowser(browser, { kill: false });
373
- throw error;
374
- }
375
- logger.warn("Tab worker init failed; retrying with inline tab worker (no sync-loop guard)", {
376
- error: error instanceof Error ? error.message : String(error),
377
- });
378
- worker = await (workerFactoryForTest?.() ?? spawnInlineWorker());
379
- try {
380
- info = await initializeAcquireWorker(worker, initPayload, opts.timeoutMs + GRACE_MS);
381
- } catch (inlineError) {
382
- await worker.terminate().catch(() => undefined);
383
- if (replacementHold) await releaseBrowser(browser, { kill: false });
384
- else if (browser.refCount === 0) await releaseBrowser(browser, { kill: false });
385
- const finalError = new ToolError(
386
- `Failed to start browser tab worker (inline fallback also failed): ${inlineError instanceof Error ? inlineError.message : String(inlineError)}`,
387
- );
388
- (finalError as { cause?: unknown }).cause = error;
389
- throw finalError;
369
+ worker = await spawnAcquireWorker();
370
+ } catch {
371
+ if (replacementHold) await releaseBrowser(browser, { kill: false });
372
+ else if (browser.refCount === 0) await releaseBrowser(browser, { kill: false });
373
+ throw startupError("spawn");
374
+ }
375
+ let registeredTab: TabSession | undefined;
376
+ let startupFailure: Error | undefined;
377
+ let browserHeld = false;
378
+ const observeFailure = (error: Error): void => {
379
+ if (registeredTab) markTabDead(registeredTab, error);
380
+ else startupFailure ??= error;
381
+ };
382
+ const unlistenLifetimeMessages = worker.onMessage(msg => {
383
+ if (msg.type === "closed") observeFailure(startupError("protocol-closed"));
384
+ });
385
+ const unlistenLifetimeErrors = worker.onError(() => observeFailure(startupError("error")));
386
+ const unlistenLifetimeClose = worker.onClose(() => observeFailure(startupError("physical-close")));
387
+ try {
388
+ const info = await initializeAcquireWorker(worker, initPayload, opts.timeoutMs + GRACE_MS);
389
+ afterWorkerInitializationForTest?.(name);
390
+ if (startupFailure) throw startupFailure;
391
+ if (opts.requireVacantName && tabs.has(name))
392
+ throw new ToolError(`Tab ${JSON.stringify(name)} was replaced during recovery.`);
393
+
394
+ holdBrowser(browser);
395
+ browserHeld = true;
396
+ if (replacementHold) {
397
+ await releaseBrowser(browser, { kill: false });
398
+ replacementHold = false;
390
399
  }
391
- }
392
-
393
- afterWorkerInitializationForTest?.(name);
394
- if (opts.requireVacantName && tabs.has(name)) {
400
+ if (startupFailure) throw startupFailure;
401
+
402
+ const tab: TabSession = {
403
+ name,
404
+ browser,
405
+ targetId: info.targetId,
406
+ worker,
407
+ state: "alive",
408
+ info,
409
+ pending: new Map(),
410
+ dialogPolicy: opts.dialogs,
411
+ kindTag: browser.kind.kind,
412
+ ownerId: opts.ownerId,
413
+ lastUsedAt: Date.now(),
414
+ recoveryOpts: freezeRecoveryOptions(opts),
415
+ };
416
+ registeredTab = tab;
417
+ worker.onMessage(msg => handleTabMessage(tab, msg));
418
+ tabs.set(name, tab);
419
+ return { tab, created: true };
420
+ } catch (error) {
421
+ unlistenLifetimeMessages();
422
+ unlistenLifetimeErrors();
423
+ unlistenLifetimeClose();
395
424
  await worker.terminate().catch(() => undefined);
425
+ if (browserHeld) await releaseBrowser(browser, { kill: false });
396
426
  if (replacementHold) await releaseBrowser(browser, { kill: false });
397
- throw new ToolError(`Tab ${JSON.stringify(name)} was replaced during recovery.`);
427
+ else if (!browserHeld && browser.refCount === 0) await releaseBrowser(browser, { kill: false });
428
+ throw error;
398
429
  }
399
- holdBrowser(browser);
400
- if (replacementHold) await releaseBrowser(browser, { kill: false });
401
- const tab: TabSession = {
402
- name,
403
- browser,
404
- targetId: info.targetId,
405
- worker,
406
- state: "alive",
407
- info,
408
- pending: new Map(),
409
- dialogPolicy: opts.dialogs,
410
- kindTag: browser.kind.kind,
411
- ownerId: opts.ownerId,
412
- lastUsedAt: Date.now(),
413
- recoveryOpts: freezeRecoveryOptions(opts),
414
- };
415
- worker.onMessage(msg => handleTabMessage(tab, msg));
416
- worker.onError(error => markTabDead(tab, error));
417
- tabs.set(name, tab);
418
- return { tab, created: true };
419
430
  }
420
431
 
421
432
  export async function runInTab(name: string, opts: RunInTabOptions): Promise<RunResultOk> {
@@ -871,22 +882,14 @@ async function raceWithTimeout<T>(
871
882
  }
872
883
 
873
884
  async function spawnTabWorker(): Promise<WorkerHandle> {
874
- try {
875
- const worker = isCompiledBinary()
876
- ? new Worker("./packages/coding-agent/src/tools/browser/tab-worker-entry.ts", { type: "module" })
877
- : new Worker(new URL("./tab-worker-entry.ts", import.meta.url).href, { type: "module" });
878
- return wrapBunWorker(worker);
879
- } catch (err) {
880
- logger.warn("Bun Worker spawn failed; using inline tab worker (no sync-loop guard)", {
881
- error: err instanceof Error ? err.message : String(err),
882
- });
883
- return spawnInlineWorker();
884
- }
885
+ const worker = isCompiledBinary()
886
+ ? new Worker("./packages/coding-agent/src/tools/browser/tab-worker-entry.ts", { type: "module" })
887
+ : new Worker(new URL("./tab-worker-entry.ts", import.meta.url).href, { type: "module" });
888
+ return wrapBunWorker(worker);
885
889
  }
886
890
 
887
891
  function wrapBunWorker(worker: Worker): WorkerHandle {
888
892
  return {
889
- mode: "worker",
890
893
  send(msg, transferList) {
891
894
  worker.postMessage(msg, { transfer: transferList ?? [] });
892
895
  },
@@ -896,9 +899,8 @@ function wrapBunWorker(worker: Worker): WorkerHandle {
896
899
  return () => worker.removeEventListener("message", wrap);
897
900
  },
898
901
  onError(handler) {
899
- const onError = (event: ErrorEvent): void => handler(errorFromWorkerEvent(event));
900
- const onMessageError = (event: MessageEvent): void =>
901
- handler(new ToolError(`Tab worker message error: ${String(event.data)}`));
902
+ const onError = (): void => handler(startupError("error"));
903
+ const onMessageError = (): void => handler(startupError("messageerror"));
902
904
  worker.addEventListener("error", onError);
903
905
  worker.addEventListener("messageerror", onMessageError);
904
906
  return () => {
@@ -906,69 +908,76 @@ function wrapBunWorker(worker: Worker): WorkerHandle {
906
908
  worker.removeEventListener("messageerror", onMessageError);
907
909
  };
908
910
  },
911
+ onClose(handler) {
912
+ const onClose = (): void => handler();
913
+ worker.addEventListener("close", onClose);
914
+ return () => worker.removeEventListener("close", onClose);
915
+ },
909
916
  async terminate() {
910
917
  worker.terminate();
911
918
  },
912
919
  };
913
920
  }
914
921
 
915
- /**
916
- * Inline fallback for environments where Bun cannot compile or spawn the worker
917
- * entry. This preserves normal browser behavior but cannot interrupt synchronous
918
- * infinite loops because user code runs on the main thread.
919
- */
920
- async function spawnInlineWorker(): Promise<WorkerHandle> {
921
- const hostListeners = new Set<(message: WorkerOutbound) => void>();
922
- const workerListeners = new Set<(message: WorkerInbound) => void>();
923
- const workerTransport: Transport = {
924
- send: msg =>
925
- queueMicrotask(() => {
926
- for (const listener of hostListeners) listener(msg as WorkerOutbound);
927
- }),
928
- onMessage: handler => {
929
- const typed = handler as (message: WorkerInbound) => void;
930
- workerListeners.add(typed);
931
- return () => workerListeners.delete(typed);
932
- },
933
- close: () => {},
934
- };
935
- const { WorkerCore } = await import("./tab-worker");
936
- new WorkerCore(workerTransport);
937
- return {
938
- mode: "inline",
939
- send: msg =>
940
- queueMicrotask(() => {
941
- for (const listener of workerListeners) listener(msg);
942
- }),
943
- onMessage: handler => {
944
- hostListeners.add(handler);
945
- return () => hostListeners.delete(handler);
946
- },
947
- onError: () => () => {},
948
- async terminate() {},
949
- };
950
- }
951
-
952
922
  async function initializeTabWorker(
953
923
  worker: WorkerHandle,
954
924
  payload: WorkerInitPayload,
955
925
  timeoutMs: number,
956
926
  ): Promise<ReadyInfo> {
957
- const { promise, resolve, reject } = Promise.withResolvers<ReadyInfo>();
927
+ type StartupPhase = "await-bootstrap" | "bootstrap-confirmed" | "await-init";
928
+ let phase: StartupPhase = "await-bootstrap";
929
+ const { promise: bootstrap, resolve: bootstrapReady, reject: rejectBootstrap } = Promise.withResolvers<void>();
930
+ const { promise: initialized, resolve: ready, reject: rejectInitialized } = Promise.withResolvers<ReadyInfo>();
931
+ void initialized.catch(() => undefined);
932
+ const fail = (stage: string): void => {
933
+ const error = startupError(stage);
934
+ rejectBootstrap(error);
935
+ rejectInitialized(error);
936
+ };
958
937
  const unlisten = worker.onMessage(msg => {
959
- if (msg.type === "ready") resolve(msg.info);
960
- else if (msg.type === "init-failed") reject(errorFromPayload(msg.error));
961
- else if (msg.type === "log") logWorkerMessage(msg);
962
- });
963
- const unlistenError = worker.onError(error => {
964
- reject(new ToolError(`Tab worker failed during startup: ${error.message}`));
938
+ if (msg.type === "log") {
939
+ logWorkerMessage(msg);
940
+ return;
941
+ }
942
+ if (phase === "await-bootstrap") {
943
+ if (msg.type === "bootstrap-ready" && msg.version === 1 && msg.mode === TAB_WORKER_MODE) {
944
+ phase = "bootstrap-confirmed";
945
+ bootstrapReady();
946
+ } else if (msg.type === "bootstrap-failed") {
947
+ fail("bootstrap");
948
+ } else if (msg.type === "closed") {
949
+ fail("protocol-closed");
950
+ } else {
951
+ fail("protocol-phase");
952
+ }
953
+ return;
954
+ }
955
+ if (phase !== "await-init") {
956
+ fail("protocol-phase");
957
+ return;
958
+ }
959
+ if (msg.type === "ready") {
960
+ ready(msg.info);
961
+ } else if (msg.type === "init-failed") {
962
+ fail("init");
963
+ } else if (msg.type === "closed") {
964
+ fail("protocol-closed");
965
+ } else {
966
+ fail("protocol-phase");
967
+ }
965
968
  });
969
+ const unlistenError = worker.onError(() => fail("error"));
970
+ const unlistenClose = worker.onClose(() => fail("physical-close"));
966
971
  try {
972
+ worker.send({ type: "bootstrap", version: 1, mode: TAB_WORKER_MODE });
973
+ await raceWithTimeout(bootstrap, timeoutMs, startupError("bootstrap-timeout").message);
974
+ phase = "await-init";
967
975
  worker.send({ type: "init", payload });
968
- return await raceWithTimeout(promise, timeoutMs, "Timed out initializing browser tab worker");
976
+ return await raceWithTimeout(initialized, timeoutMs, startupError("init-timeout").message);
969
977
  } finally {
970
978
  unlisten();
971
979
  unlistenError();
980
+ unlistenClose();
972
981
  }
973
982
  }
974
983
 
@@ -979,9 +988,3 @@ export function initializeTabWorkerForTest(
979
988
  ): Promise<ReadyInfo> {
980
989
  return initializeTabWorker(worker, payload, timeoutMs);
981
990
  }
982
-
983
- function errorFromWorkerEvent(event: ErrorEvent): Error {
984
- if (event.error instanceof Error) return event.error;
985
- if (event.message) return new Error(event.message);
986
- return new Error("Unknown tab worker error");
987
- }