@gajae-code/coding-agent 0.12.0 → 0.12.1

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 (223) hide show
  1. package/CHANGELOG.md +82 -1
  2. package/dist/types/async/job-manager.d.ts +17 -3
  3. package/dist/types/config/settings-schema.d.ts +54 -0
  4. package/dist/types/exa/mcp-client.d.ts +10 -1
  5. package/dist/types/exec/bash-executor.d.ts +45 -12
  6. package/dist/types/extensibility/extensions/types.d.ts +16 -2
  7. package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
  8. package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
  9. package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
  10. package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
  11. package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
  12. package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
  13. package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
  14. package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
  15. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +18 -0
  16. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  17. package/dist/types/gjc-runtime/team-runtime.d.ts +25 -0
  18. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
  19. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +10 -0
  20. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +31 -80
  21. package/dist/types/modes/acp/acp-agent.d.ts +2 -19
  22. package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
  23. package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
  24. package/dist/types/modes/components/session-observer-overlay.d.ts +18 -0
  25. package/dist/types/modes/components/settings-selector.d.ts +9 -0
  26. package/dist/types/modes/components/status-line/gh.d.ts +5 -0
  27. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
  28. package/dist/types/reminders/star-reminder.d.ts +3 -13
  29. package/dist/types/runtime-mcp/manager.d.ts +1 -0
  30. package/dist/types/sdk/acp/adapter.d.ts +3 -1
  31. package/dist/types/sdk/broker/transport.d.ts +2 -0
  32. package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
  33. package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -1
  34. package/dist/types/sdk/bus/config.d.ts +6 -2
  35. package/dist/types/sdk/bus/index.d.ts +29 -0
  36. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +8 -1
  37. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  38. package/dist/types/sdk/bus/reconciliation-store.d.ts +7 -3
  39. package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
  40. package/dist/types/sdk/bus/rich-render.d.ts +2 -0
  41. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
  42. package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
  43. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -4
  44. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
  45. package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
  46. package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
  47. package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
  48. package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
  49. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  50. package/dist/types/sdk/prompt-status.d.ts +23 -3
  51. package/dist/types/sdk/session.d.ts +6 -0
  52. package/dist/types/session/agent-session.d.ts +38 -2
  53. package/dist/types/session/artifacts.d.ts +8 -0
  54. package/dist/types/session/blob-store.d.ts +57 -4
  55. package/dist/types/session/internal/managed-session-storage.d.ts +8 -1
  56. package/dist/types/session/session-manager.d.ts +23 -6
  57. package/dist/types/session/session-storage.d.ts +4 -0
  58. package/dist/types/session/streaming-output.d.ts +36 -7
  59. package/dist/types/setup/credential-import.d.ts +12 -0
  60. package/dist/types/skill-state/active-state.d.ts +12 -0
  61. package/dist/types/skill-state/workflow-hud.d.ts +6 -0
  62. package/dist/types/task/executor.d.ts +3 -2
  63. package/dist/types/task/index.d.ts +3 -1
  64. package/dist/types/task/provider-retry-status.d.ts +8 -0
  65. package/dist/types/task/receipt.d.ts +7 -1
  66. package/dist/types/task/types.d.ts +23 -0
  67. package/dist/types/tools/ask-answer-registry.d.ts +2 -2
  68. package/dist/types/tools/bash-interactive.d.ts +4 -1
  69. package/dist/types/tools/bash.d.ts +3 -1
  70. package/dist/types/tools/image-gen.d.ts +4 -0
  71. package/dist/types/tools/index.d.ts +4 -0
  72. package/dist/types/tools/output-meta.d.ts +23 -2
  73. package/dist/types/tools/path-utils.d.ts +1 -0
  74. package/dist/types/tools/read.d.ts +0 -6
  75. package/dist/types/tools/review.d.ts +5 -7
  76. package/dist/types/tools/subagent.d.ts +2 -0
  77. package/dist/types/utils/gh.d.ts +13 -0
  78. package/dist/types/utils/image-resize.d.ts +1 -0
  79. package/dist/types/web/search/providers/kimi.d.ts +2 -0
  80. package/dist/types/web/search/providers/searxng.d.ts +7 -0
  81. package/dist/types/web/search/providers/xai.d.ts +2 -0
  82. package/package.json +10 -8
  83. package/scripts/acp-conformance-agent.ts +218 -0
  84. package/scripts/capture-gjc-bundle-settings.ts +306 -0
  85. package/scripts/generate-sdk-operation-inventory.ts +3 -0
  86. package/scripts/resident-memory-bench.ts +1341 -0
  87. package/scripts/run-acp-conformance.ts +285 -0
  88. package/src/async/job-manager.ts +78 -20
  89. package/src/cli/fast-help.ts +1 -1
  90. package/src/cli/plugin-cli.ts +176 -18
  91. package/src/commands/team.ts +19 -1
  92. package/src/config/file-lock.ts +11 -2
  93. package/src/config/settings-schema.ts +51 -0
  94. package/src/coordinator-mcp/question-gate-codec.ts +1 -1
  95. package/src/coordinator-mcp/server.ts +5 -12
  96. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -0
  97. package/src/defaults/gjc/skills/ralplan/SKILL.md +19 -16
  98. package/src/defaults/gjc/skills/team/SKILL.md +2 -0
  99. package/src/defaults/gjc/skills/ultragoal/SKILL.md +37 -24
  100. package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
  101. package/src/defaults/gjc-defaults.ts +4 -4
  102. package/src/exa/mcp-client.ts +12 -3
  103. package/src/exec/bash-executor.ts +144 -27
  104. package/src/extensibility/extensions/runner.ts +15 -0
  105. package/src/extensibility/extensions/types.ts +18 -1
  106. package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
  107. package/src/extensibility/gjc-plugins/index.ts +19 -2
  108. package/src/extensibility/gjc-plugins/installer.ts +189 -50
  109. package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
  110. package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
  111. package/src/extensibility/gjc-plugins/observability.ts +3 -3
  112. package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
  113. package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
  114. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
  115. package/src/extensibility/gjc-plugins/schema.ts +84 -12
  116. package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
  117. package/src/extensibility/gjc-plugins/types.ts +141 -0
  118. package/src/extensibility/gjc-plugins/validation.ts +9 -0
  119. package/src/gjc-runtime/launch-tmux.ts +25 -6
  120. package/src/gjc-runtime/managed-owner-admission.ts +1 -1
  121. package/src/gjc-runtime/ralplan-runtime.ts +411 -17
  122. package/src/gjc-runtime/team-launch.ts +8 -0
  123. package/src/gjc-runtime/team-runtime.ts +140 -32
  124. package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
  125. package/src/gjc-runtime/tmux-owner-isolation.ts +10 -0
  126. package/src/gjc-runtime/tmux-sessions.ts +58 -13
  127. package/src/gjc-runtime/ultragoal-runtime.ts +401 -611
  128. package/src/gjc-runtime/workflow-manifest.generated.json +8 -99
  129. package/src/gjc-runtime/workflow-manifest.ts +4 -68
  130. package/src/internal-urls/docs-index.generated.ts +15 -15
  131. package/src/modes/DESIGN.md +51 -0
  132. package/src/modes/acp/acp-agent.ts +298 -186
  133. package/src/modes/components/gjc-bundle-settings.ts +508 -0
  134. package/src/modes/components/notifications-settings-editor.ts +12 -1
  135. package/src/modes/components/oauth-selector.ts +68 -30
  136. package/src/modes/components/session-observer-overlay.ts +305 -86
  137. package/src/modes/components/settings-selector.ts +56 -5
  138. package/src/modes/components/status-line/gh.ts +16 -0
  139. package/src/modes/components/tool-status-header.ts +3 -14
  140. package/src/modes/components/transcript-viewer-overlay.ts +99 -24
  141. package/src/modes/controllers/command-controller.ts +4 -0
  142. package/src/modes/controllers/extension-ui-controller.ts +7 -0
  143. package/src/modes/controllers/selector-controller.ts +45 -0
  144. package/src/modes/runtime-init.ts +3 -0
  145. package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
  146. package/src/prompts/system/eager-todo.md +8 -4
  147. package/src/prompts/system/system-prompt.md +11 -4
  148. package/src/prompts/tools/bash.md +5 -5
  149. package/src/prompts/tools/search-tool-bm25.md +5 -0
  150. package/src/prompts/tools/task.md +1 -1
  151. package/src/reminders/star-reminder.ts +4 -42
  152. package/src/runtime-mcp/manager.ts +19 -4
  153. package/src/sdk/acp/adapter.ts +22 -6
  154. package/src/sdk/broker/transport.ts +35 -3
  155. package/src/sdk/bus/attachment-registry.ts +5 -1
  156. package/src/sdk/bus/chat-daemon-cli.ts +4 -39
  157. package/src/sdk/bus/chat-daemon-control.ts +6 -3
  158. package/src/sdk/bus/config.ts +18 -2
  159. package/src/sdk/bus/index.ts +661 -126
  160. package/src/sdk/bus/kind-aware-reconciliation.ts +148 -73
  161. package/src/sdk/bus/lifecycle-commands.ts +4 -1
  162. package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
  163. package/src/sdk/bus/reconciliation-store.ts +95 -16
  164. package/src/sdk/bus/rich-draft.ts +1 -1
  165. package/src/sdk/bus/rich-render.ts +2 -2
  166. package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
  167. package/src/sdk/bus/telegram-cli.ts +33 -8
  168. package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
  169. package/src/sdk/bus/telegram-daemon-contract.ts +11 -4
  170. package/src/sdk/bus/telegram-daemon.ts +916 -23
  171. package/src/sdk/bus/telegram-reference.ts +40 -3
  172. package/src/sdk/bus/threaded-render.ts +48 -3
  173. package/src/sdk/bus/topic-registry.ts +20 -0
  174. package/src/sdk/host/query/revision-store.ts +36 -2
  175. package/src/sdk/host/reverse-leases.ts +44 -7
  176. package/src/sdk/prompt-status.ts +16 -3
  177. package/src/sdk/protocol/operation-inventory.generated.json +22 -0
  178. package/src/sdk/session.ts +88 -9
  179. package/src/session/agent-session.ts +822 -172
  180. package/src/session/artifacts.ts +22 -0
  181. package/src/session/auth-broker-config.ts +10 -3
  182. package/src/session/blob-store.ts +915 -24
  183. package/src/session/internal/managed-session-scope.ts +44 -6
  184. package/src/session/internal/managed-session-storage.ts +48 -6
  185. package/src/session/session-manager.ts +1614 -435
  186. package/src/session/session-storage.ts +9 -1
  187. package/src/session/streaming-output.ts +238 -68
  188. package/src/setup/credential-import.ts +75 -13
  189. package/src/skill-state/active-state.ts +32 -0
  190. package/src/skill-state/workflow-hud.ts +17 -0
  191. package/src/skill-state/workflow-mutation-guard.ts +496 -2
  192. package/src/task/executor.ts +122 -39
  193. package/src/task/index.ts +23 -14
  194. package/src/task/provider-retry-status.ts +16 -0
  195. package/src/task/receipt.ts +23 -7
  196. package/src/task/render.ts +6 -2
  197. package/src/task/types.ts +108 -0
  198. package/src/tools/ask-answer-registry.ts +12 -6
  199. package/src/tools/ask.ts +86 -55
  200. package/src/tools/ast-edit.ts +1 -0
  201. package/src/tools/ast-grep.ts +1 -0
  202. package/src/tools/bash-interactive.ts +8 -3
  203. package/src/tools/bash.ts +503 -60
  204. package/src/tools/find.ts +1 -0
  205. package/src/tools/image-gen.ts +29 -5
  206. package/src/tools/index.ts +4 -0
  207. package/src/tools/output-meta.ts +108 -14
  208. package/src/tools/path-utils.ts +3 -2
  209. package/src/tools/read.ts +18 -0
  210. package/src/tools/review.ts +5 -7
  211. package/src/tools/search.ts +1 -0
  212. package/src/tools/subagent-render.ts +6 -2
  213. package/src/tools/subagent.ts +6 -0
  214. package/src/tools/telegram-send.ts +6 -3
  215. package/src/utils/gh.ts +39 -0
  216. package/src/utils/image-resize.ts +3 -0
  217. package/src/web/search/providers/anthropic.ts +4 -4
  218. package/src/web/search/providers/kimi.ts +15 -5
  219. package/src/web/search/providers/searxng.ts +47 -4
  220. package/src/web/search/providers/xai.ts +7 -2
  221. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
  222. package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
  223. package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * Credential-free ACP fixture launcher. The loopback endpoint is deliberately
5
+ * deterministic; the child remains the production `gjc --mode acp` surface.
6
+ */
7
+ import * as fs from "node:fs/promises";
8
+ import * as os from "node:os";
9
+ import * as path from "node:path";
10
+
11
+ const root = path.resolve(import.meta.dir, "..", "..", "..");
12
+ const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "gjc-acp-conformance-"));
13
+ let child: ReturnType<typeof Bun.spawn> | undefined;
14
+ let server: ReturnType<typeof Bun.serve> | undefined;
15
+
16
+ /**
17
+ * Deterministic replies for the pinned `acp-core-v1` corpus. Each branch mirrors one
18
+ * upstream case expectation; nothing here reaches the network.
19
+ */
20
+ function response(prompt: string): string {
21
+ if (prompt.includes("inspect-prompt")) return `received blocks: {"type":"resource"}`;
22
+ if (prompt.includes("this-command-does-not-exist")) return "unrecognized prompt";
23
+ if (/^\s*read\b/m.test(prompt)) return "read README.md: acpx";
24
+ if (/^\s*write\b/m.test(prompt)) {
25
+ const target = /write\s+(\S+)/.exec(prompt)?.[1] ?? "file";
26
+ return `wrote ${target}`;
27
+ }
28
+ const echo = /^\s*echo\s*(.*)$/m.exec(prompt);
29
+ if (echo) return echo[1] || "echo";
30
+ return "ok";
31
+ }
32
+
33
+ function sse(value: unknown): string {
34
+ return `data: ${JSON.stringify(value)}\n\n`;
35
+ }
36
+
37
+ function completion(delta: Record<string, unknown>, finishReason: string | null = null): string {
38
+ return sse({ id: "gjc-conformance", choices: [{ delta, finish_reason: finishReason }] });
39
+ }
40
+
41
+ function toolCall(name: "read" | "write", arguments_: Record<string, string>): string {
42
+ return completion(
43
+ {
44
+ tool_calls: [
45
+ {
46
+ index: 0,
47
+ id: `conformance-${name}`,
48
+ type: "function",
49
+ function: { name, arguments: JSON.stringify(arguments_) },
50
+ },
51
+ ],
52
+ },
53
+ "tool_calls",
54
+ );
55
+ }
56
+
57
+ function textCompletion(text: string): string {
58
+ return `${completion({ content: text })}${completion({}, "stop")}data: [DONE]\n\n`;
59
+ }
60
+
61
+ async function cleanup(code = 0): Promise<never> {
62
+ server?.stop(true);
63
+ if (child && child.exitCode === null) child.kill();
64
+ await fs.rm(agentDir, { recursive: true, force: true });
65
+ process.exit(code);
66
+ }
67
+
68
+ server = Bun.serve({
69
+ hostname: "127.0.0.1",
70
+ port: 0,
71
+ async fetch(request) {
72
+ const body = (await request.json().catch(() => ({}))) as {
73
+ messages?: Array<{ role?: string; content?: unknown }>;
74
+ };
75
+ // OpenAI-style content is either a plain string or an array of typed blocks.
76
+ const textOf = (content: unknown): string =>
77
+ typeof content === "string"
78
+ ? content
79
+ : Array.isArray(content)
80
+ ? content
81
+ .map(block =>
82
+ typeof block === "object" &&
83
+ block !== null &&
84
+ typeof (block as { text?: unknown }).text === "string"
85
+ ? (block as { text: string }).text
86
+ : "",
87
+ )
88
+ .join("")
89
+ : "";
90
+ const messages = body.messages ?? [];
91
+ const prompt =
92
+ [...messages]
93
+ .reverse()
94
+ .filter(message => message.role === "user")
95
+ .map(message => textOf(message.content))
96
+ .find(text => text.length > 0) ?? "";
97
+ const hasToolResult = messages.some(message => message.role === "tool");
98
+ const isRead = /^\s*read\b/m.test(prompt);
99
+ const isWrite = /^\s*write\b/m.test(prompt);
100
+ const isLateTool = prompt.includes("late-tool 40 follow-up");
101
+
102
+ if (prompt.includes("sleep")) {
103
+ const requested = Number(/\bsleep\s+(\d+)/.exec(prompt)?.[1] ?? 0);
104
+ const delay = Math.min(Math.max(requested, 0), 30_000);
105
+ const stream = new ReadableStream<Uint8Array>({
106
+ start(controller) {
107
+ let closed = false;
108
+ const close = () => {
109
+ if (closed) return;
110
+ closed = true;
111
+ controller.close();
112
+ };
113
+ request.signal.addEventListener("abort", close, { once: true });
114
+ void Bun.sleep(delay).then(() => {
115
+ if (closed) return;
116
+ controller.enqueue(new TextEncoder().encode(textCompletion("slept")));
117
+ close();
118
+ });
119
+ },
120
+ });
121
+ return new Response(stream, { headers: { "content-type": "text/event-stream" } });
122
+ }
123
+
124
+ if ((isRead || isWrite || isLateTool) && !hasToolResult) {
125
+ if (isLateTool) {
126
+ const prefix = `${completion({ content: "preparing" })}${completion({ content: " writing now" })}${completion({ content: " before tool" })}`;
127
+ return new Response(
128
+ `${prefix}${toolCall("write", { path: ".acpx-conformance-late.txt", content: "late" })}data: [DONE]\n\n`,
129
+ {
130
+ headers: { "content-type": "text/event-stream" },
131
+ },
132
+ );
133
+ }
134
+ // Neutral pre-tool text: every corpus assertion below must come from the real
135
+ // tool result, never from a marker announced before the tool ran.
136
+ const provisional = isRead ? "checking the requested file" : "applying the requested write";
137
+ return new Response(
138
+ `${completion({ content: provisional })}${toolCall(
139
+ isRead ? "read" : "write",
140
+ isRead
141
+ ? { path: "README.md" }
142
+ : {
143
+ path: /write\s+(\S+)/.exec(prompt)?.[1] ?? "file",
144
+ content: /write\s+\S+\s*(.*)/.exec(prompt)?.[1] ?? "",
145
+ },
146
+ )}data: [DONE]\n\n`,
147
+ { headers: { "content-type": "text/event-stream" } },
148
+ );
149
+ }
150
+
151
+ // Derive the reply from what the tool actually returned so a broken permission
152
+ // bridge, filesystem bridge, or tool result cannot still satisfy the corpus.
153
+ const toolText = messages
154
+ .filter(message => message.role === "tool")
155
+ .map(message => textOf(message.content))
156
+ .join("\n");
157
+ const toolDenied = /rejected by user|permission denied|not permitted/i.test(toolText);
158
+ const reply = toolDenied
159
+ ? `permission denied: ${toolText.slice(0, 200)}`
160
+ : isRead
161
+ ? `read README.md: ${toolText.trim().slice(0, 200)}`
162
+ : isWrite
163
+ ? `wrote ${/write\s+(\S+)/.exec(prompt)?.[1] ?? "file"}: ${toolText.trim().slice(0, 120)}`
164
+ : isLateTool
165
+ ? `writing now: ${toolText.trim().slice(0, 120)}`
166
+ : response(prompt);
167
+ return new Response(textCompletion(reply), { headers: { "content-type": "text/event-stream" } });
168
+ },
169
+ });
170
+
171
+ await Bun.write(
172
+ path.join(agentDir, "models.json"),
173
+ JSON.stringify({
174
+ providers: {
175
+ conformance: {
176
+ api: "openai-completions",
177
+ baseUrl: `http://127.0.0.1:${server.port}/v1`,
178
+ auth: "none",
179
+ models: [{ id: "fixture", name: "fixture", input: ["text"], contextWindow: 1_000_000, maxTokens: 8192 }],
180
+ },
181
+ },
182
+ }),
183
+ );
184
+
185
+ // The pinned corpus reads `README.md` from the session cwd the runner passes on
186
+ // `session/new`. The runner creates that directory but seeds no content, so seed it
187
+ // here (never inside the upstream corpus) when the harness names it.
188
+ const scratchCwd = process.env.GJC_ACP_CONFORMANCE_CWD?.trim();
189
+ if (scratchCwd) {
190
+ const scratchReadme = path.join(scratchCwd, "README.md");
191
+ if (!(await Bun.file(scratchReadme).exists())) await Bun.write(scratchReadme, "acpx conformance workspace\n");
192
+ }
193
+
194
+ child = Bun.spawn(
195
+ [
196
+ "bun",
197
+ path.join(root, "packages", "coding-agent", "src", "cli.ts"),
198
+ "--mode",
199
+ "acp",
200
+ "--model",
201
+ "conformance/fixture",
202
+ ],
203
+ {
204
+ cwd: root,
205
+ env: { ...process.env, GJC_CODING_AGENT_DIR: agentDir, PI_CODING_AGENT_DIR: agentDir },
206
+ stdin: "inherit",
207
+ stdout: "inherit",
208
+ stderr: "inherit",
209
+ },
210
+ );
211
+ process.once("SIGTERM", () => {
212
+ void cleanup(0);
213
+ });
214
+ process.once("SIGINT", () => {
215
+ void cleanup(0);
216
+ });
217
+ await child.exited;
218
+ await cleanup(child.exitCode ?? 1);
@@ -0,0 +1,306 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import type { GjcLifecycleContext } from "../src/extensibility/gjc-plugins/lifecycle";
4
+ import type { GjcRuntimeSnapshotProvider } from "../src/extensibility/gjc-plugins/runtime-quarantine";
5
+ import type {
6
+ GjcBundleIdentity,
7
+ GjcBundleSummary,
8
+ GjcLifecycleResult,
9
+ GjcToggleResult,
10
+ GjcUpdateApplyResult,
11
+ GjcUpdatePreview,
12
+ } from "../src/extensibility/gjc-plugins/types";
13
+ import { type GjcBundleLifecyclePort, GjcBundleSettingsComponent } from "../src/modes/components/gjc-bundle-settings";
14
+ import { setTheme } from "../src/modes/theme/theme";
15
+ import {
16
+ GJC_BUNDLE_SETTINGS_ENTRIES,
17
+ GJC_BUNDLE_SETTINGS_STATES,
18
+ GJC_BUNDLE_SETTINGS_VIEWPORTS,
19
+ type GjcBundleSettingsFixture,
20
+ } from "../test/fixtures/gjc-bundles-settings-cases";
21
+
22
+ export const GJC_BUNDLE_SETTINGS_CAPTURE_FILES = [
23
+ "terminal.txt",
24
+ "terminal-ansi.txt",
25
+ "terminal.html",
26
+ "metadata.json",
27
+ ] as const;
28
+
29
+ type CaptureFileName = (typeof GJC_BUNDLE_SETTINGS_CAPTURE_FILES)[number];
30
+
31
+ type FixtureEntry = (typeof GJC_BUNDLE_SETTINGS_ENTRIES)[number];
32
+
33
+ type CapturePlanItem = {
34
+ entryId: string;
35
+ fileName: CaptureFileName;
36
+ };
37
+
38
+ type CaptureMetadata = {
39
+ entryId: string;
40
+ stateId: string;
41
+ viewport: { id: string; cols: number; rows: number };
42
+ variant: { renderMode: string };
43
+ sha256: Record<Exclude<CaptureFileName, "metadata.json">, string>;
44
+ };
45
+
46
+ /**
47
+ * Locator-shaped content that must never reach a capture. Applied to the
48
+ * rendered terminal text, which is the only surface that can carry a leaked
49
+ * locator; generated HTML/CSS legitimately contains `#rrggbb` colors and
50
+ * escaped entities, so it is checked against the same rules via its source
51
+ * text rather than its markup.
52
+ */
53
+ const FORBIDDEN_CAPTURE_CONTENT = /:\/\/user:|@[^\s/]+|\?[^\s]*=|token|\/Users\/|\/home\//i;
54
+
55
+ function sha256(content: string): string {
56
+ return new Bun.CryptoHasher("sha256").update(content).digest("hex");
57
+ }
58
+
59
+ function json(value: unknown): string {
60
+ return `${JSON.stringify(value, null, 2)}\n`;
61
+ }
62
+
63
+ function escapeHtml(text: string): string {
64
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
65
+ }
66
+
67
+ function ansiToHtml(ansi: string): string {
68
+ const sgr = /\x1b\[([0-9;]*)m/g;
69
+ let html = "";
70
+ let offset = 0;
71
+ let style = "";
72
+ for (const match of ansi.matchAll(sgr)) {
73
+ html += `<span style="${style}">${escapeHtml(ansi.slice(offset, match.index))}</span>`;
74
+ offset = (match.index ?? 0) + match[0].length;
75
+ const codes = (match[1] || "0").split(";").map(Number);
76
+ for (let index = 0; index < codes.length; index += 1) {
77
+ const code = codes[index];
78
+ if (code === 0) style = "";
79
+ else if (code === 1) style = `${style}font-weight:700;`;
80
+ else if (code === 2) style = `${style}opacity:.65;`;
81
+ else if (code === 22) style = style.replace("font-weight:700;", "").replace("opacity:.65;", "");
82
+ else if (code === 38 && codes[index + 1] === 2) {
83
+ const [red, green, blue] = codes.slice(index + 2, index + 5);
84
+ if ([red, green, blue].every(Number.isInteger)) style = `${style}color:rgb(${red},${green},${blue});`;
85
+ index += 4;
86
+ } else if (code === 39) style = style.replace(/color:[^;]+;/g, "");
87
+ }
88
+ }
89
+ html += `<span style="${style}">${escapeHtml(ansi.slice(offset))}</span>`;
90
+ return `<!doctype html>
91
+ <html lang="en">
92
+ <head><meta charset="utf-8"><meta name="color-scheme" content="dark"><title>GJC Bundles settings</title><style>body{margin:0;background:#111;color:#eee}pre{margin:0;padding:1em;white-space:pre-wrap;font-family:ui-monospace,monospace;line-height:1.2}</style></head>
93
+ <body><pre>${html}</pre></body>
94
+ </html>
95
+ `;
96
+ }
97
+
98
+ function asciiText(text: string): string {
99
+ return text
100
+ .replaceAll("─", "-")
101
+ .replaceAll("…", "...")
102
+ .replaceAll("·", ".")
103
+ .replaceAll("→", "->")
104
+ .replaceAll("−", "-");
105
+ }
106
+
107
+ function cloneSummary(summary: GjcBundleSummary): GjcBundleSummary {
108
+ return {
109
+ ...summary,
110
+ identity: { ...summary.identity },
111
+ source: { ...summary.source },
112
+ surfaces: summary.surfaces.map(surface => ({ ...surface })),
113
+ };
114
+ }
115
+
116
+ class FixtureLifecyclePort implements GjcBundleLifecyclePort {
117
+ constructor(private readonly fixture: GjcBundleSettingsFixture) {}
118
+
119
+ async listGjcBundles(_ctx: GjcLifecycleContext): Promise<GjcBundleSummary[]> {
120
+ return this.fixture.bundles.map(cloneSummary);
121
+ }
122
+
123
+ async getGjcBundle(
124
+ _ctx: GjcLifecycleContext,
125
+ identity: GjcBundleIdentity,
126
+ ): Promise<GjcLifecycleResult<GjcBundleSummary>> {
127
+ const summary = this.fixture.bundles.find(
128
+ bundle =>
129
+ bundle.identity.kind === identity.kind &&
130
+ bundle.identity.scope === identity.scope &&
131
+ bundle.identity.name === identity.name,
132
+ );
133
+ return summary
134
+ ? { ok: true, value: cloneSummary(summary) }
135
+ : { ok: false, error: { code: "not_installed", message: "Bundle is not installed." } };
136
+ }
137
+
138
+ async previewGjcBundleUpdate(
139
+ _ctx: GjcLifecycleContext,
140
+ _identity: GjcBundleIdentity,
141
+ ): Promise<GjcLifecycleResult<GjcUpdatePreview>> {
142
+ return this.fixture.updatePreview
143
+ ? { ok: true, value: this.fixture.updatePreview }
144
+ : { ok: false, error: { code: "source_unsupported", message: "Update is unavailable." } };
145
+ }
146
+
147
+ async applyGjcBundleUpdate(
148
+ _ctx: GjcLifecycleContext,
149
+ _token: GjcUpdatePreview["token"],
150
+ ): Promise<GjcLifecycleResult<GjcUpdateApplyResult>> {
151
+ return { ok: false, error: { code: "stale_candidate", message: "Capture fixtures do not apply updates." } };
152
+ }
153
+
154
+ async setGjcBundleEnabled(
155
+ _ctx: GjcLifecycleContext,
156
+ _identity: GjcBundleIdentity,
157
+ _enabled: boolean,
158
+ ): Promise<GjcLifecycleResult<GjcToggleResult>> {
159
+ return { ok: false, error: { code: "invalid_target", message: "Capture fixtures do not mutate bundles." } };
160
+ }
161
+
162
+ async setGjcBundleSurfaceEnabled(
163
+ _ctx: GjcLifecycleContext,
164
+ _identity: GjcBundleIdentity,
165
+ _surfaceId: string,
166
+ _enabled: boolean,
167
+ ): Promise<GjcLifecycleResult<GjcToggleResult>> {
168
+ return { ok: false, error: { code: "invalid_target", message: "Capture fixtures do not mutate surfaces." } };
169
+ }
170
+ }
171
+
172
+ function fixtureFor(stateId: string): GjcBundleSettingsFixture {
173
+ const state = GJC_BUNDLE_SETTINGS_STATES.find(candidate => candidate.id === stateId);
174
+ if (!state) throw new Error(`Unknown GJC Bundle settings state: ${stateId}`);
175
+ return state.fixture;
176
+ }
177
+
178
+ function viewportFor(viewportId: string): { id: string; cols: number; rows: number } {
179
+ const viewport = GJC_BUNDLE_SETTINGS_VIEWPORTS.find(candidate => candidate.id === viewportId);
180
+ if (viewport) return viewport;
181
+ if (viewportId === "48x36") return { id: "48x36", cols: 48, rows: 36 };
182
+ throw new Error(`Unknown GJC Bundle settings viewport: ${viewportId}`);
183
+ }
184
+
185
+ async function settle(): Promise<void> {
186
+ for (let index = 0; index < 8; index += 1) await Promise.resolve();
187
+ }
188
+
189
+ export async function renderGjcBundleSettingsEntry(
190
+ entry: FixtureEntry,
191
+ ): Promise<{ terminalText: string; terminalAnsiText: string; viewport: { id: string; cols: number; rows: number } }> {
192
+ const fixture = fixtureFor(entry.stateId);
193
+ const viewport = viewportFor(entry.viewportId);
194
+ const runtime: GjcRuntimeSnapshotProvider = { current: () => fixture.runtime };
195
+ await setTheme("red-claw");
196
+ const component = new GjcBundleSettingsComponent(
197
+ "/fixture/project",
198
+ { onClose: () => {} },
199
+ {
200
+ lifecycle: new FixtureLifecyclePort(fixture),
201
+ runtimeSnapshotProvider: runtime,
202
+ activationGeneration: 7,
203
+ },
204
+ );
205
+ await settle();
206
+ const rendered = component.render(viewport.cols).join("\n");
207
+ component.dispose();
208
+ const terminalAnsiText = entry.renderMode === "ascii-no-color" ? asciiText(Bun.stripANSI(rendered)) : rendered;
209
+ const terminalText = Bun.stripANSI(terminalAnsiText);
210
+ return { terminalText, terminalAnsiText, viewport };
211
+ }
212
+
213
+ export function gjcBundleSettingsCapturePlan(
214
+ entries: readonly FixtureEntry[] = GJC_BUNDLE_SETTINGS_ENTRIES,
215
+ ): CapturePlanItem[] {
216
+ return entries.flatMap(entry =>
217
+ GJC_BUNDLE_SETTINGS_CAPTURE_FILES.map(fileName => ({ entryId: entry.entryId, fileName })),
218
+ );
219
+ }
220
+
221
+ function assertSafeContent(label: string, content: string): void {
222
+ if (FORBIDDEN_CAPTURE_CONTENT.test(content)) throw new Error(`Unsafe locator content in ${label}`);
223
+ }
224
+
225
+ async function artifactContents(entry: FixtureEntry): Promise<Record<CaptureFileName, string>> {
226
+ const rendered = await renderGjcBundleSettingsEntry(entry);
227
+ const terminalHtml = ansiToHtml(rendered.terminalAnsiText);
228
+ const metadata: CaptureMetadata = {
229
+ entryId: entry.entryId,
230
+ stateId: entry.stateId,
231
+ viewport: rendered.viewport,
232
+ variant: { renderMode: entry.renderMode },
233
+ sha256: {
234
+ "terminal.txt": sha256(rendered.terminalText),
235
+ "terminal-ansi.txt": sha256(rendered.terminalAnsiText),
236
+ "terminal.html": sha256(terminalHtml),
237
+ },
238
+ };
239
+ return {
240
+ "terminal.txt": rendered.terminalText,
241
+ "terminal-ansi.txt": rendered.terminalAnsiText,
242
+ "terminal.html": terminalHtml,
243
+ "metadata.json": json(metadata),
244
+ };
245
+ }
246
+
247
+ async function writeEntry(entry: FixtureEntry, outputRoot: string): Promise<void> {
248
+ const artifacts = await artifactContents(entry);
249
+ // The HTML artifact is a pure rendering of the ANSI text, so proving the two
250
+ // text surfaces and the metadata are clean proves the whole entry is clean.
251
+ for (const name of ["terminal.txt", "terminal-ansi.txt", "metadata.json"] as const) {
252
+ assertSafeContent(`${entry.entryId}/${name}`, artifacts[name]);
253
+ }
254
+ const directory = path.join(outputRoot, entry.entryId);
255
+ await fs.mkdir(directory, { recursive: true });
256
+ await Promise.all(
257
+ GJC_BUNDLE_SETTINGS_CAPTURE_FILES.map(fileName => Bun.write(path.join(directory, fileName), artifacts[fileName])),
258
+ );
259
+ }
260
+
261
+ export async function verifyGjcBundleSettingsCapture(outputRoot: string): Promise<void> {
262
+ for (const entry of GJC_BUNDLE_SETTINGS_ENTRIES) {
263
+ const directory = path.join(outputRoot, entry.entryId);
264
+ const names = (await fs.readdir(directory)).sort();
265
+ if (
266
+ names.length !== GJC_BUNDLE_SETTINGS_CAPTURE_FILES.length ||
267
+ names.some((name, index) => name !== GJC_BUNDLE_SETTINGS_CAPTURE_FILES.slice().sort()[index])
268
+ ) {
269
+ throw new Error(`Expected exactly four capture files for ${entry.entryId}`);
270
+ }
271
+ const artifacts = await artifactContents(entry);
272
+ for (const fileName of GJC_BUNDLE_SETTINGS_CAPTURE_FILES) {
273
+ const content = await Bun.file(path.join(directory, fileName)).text();
274
+ assertSafeContent(`${entry.entryId}/${fileName}`, content);
275
+ if (content !== artifacts[fileName])
276
+ throw new Error(`Capture does not match deterministic fixture render for ${entry.entryId}/${fileName}`);
277
+ }
278
+ const metadata = JSON.parse(artifacts["metadata.json"]) as CaptureMetadata;
279
+ for (const fileName of ["terminal.txt", "terminal-ansi.txt", "terminal.html"] as const) {
280
+ if (metadata.sha256[fileName] !== sha256(artifacts[fileName]))
281
+ throw new Error(`SHA-256 mismatch for ${entry.entryId}/${fileName}`);
282
+ }
283
+ }
284
+ }
285
+
286
+ function parseArgs(args: string[]): { mode: "capture" | "verify"; outputRoot: string } {
287
+ if (args.length === 2 && args[0] === "--output" && args[1]) return { mode: "capture", outputRoot: args[1] };
288
+ if (args.length === 2 && args[0] === "--verify" && args[1]) return { mode: "verify", outputRoot: args[1] };
289
+ throw new Error("Usage: bun scripts/capture-gjc-bundle-settings.ts --output <directory> | --verify <directory>");
290
+ }
291
+
292
+ async function main(): Promise<void> {
293
+ const { mode, outputRoot } = parseArgs(process.argv.slice(2));
294
+ const resolvedOutputRoot = path.resolve(outputRoot);
295
+ if (mode === "verify") {
296
+ await verifyGjcBundleSettingsCapture(resolvedOutputRoot);
297
+ process.stdout.write(
298
+ `Verified ${GJC_BUNDLE_SETTINGS_ENTRIES.length} deterministic GJC Bundle settings entries.\n`,
299
+ );
300
+ return;
301
+ }
302
+ for (const entry of GJC_BUNDLE_SETTINGS_ENTRIES) await writeEntry(entry, resolvedOutputRoot);
303
+ process.stdout.write(`Captured ${GJC_BUNDLE_SETTINGS_ENTRIES.length} deterministic GJC Bundle settings entries.\n`);
304
+ }
305
+
306
+ if (import.meta.main) await main();
@@ -125,6 +125,7 @@ const LOCKED_EXCLUSIONS: Readonly<Record<string, string>> = {
125
125
  "agent_session:getActiveModelProfile": "internal accessor/plumbing, not a user-facing control seam",
126
126
  "agent_session:getSessionDefaultModelSelector": "internal accessor/plumbing, not a user-facing control seam",
127
127
  "agent_session:recordResumeDefaultModel": "internal accessor/plumbing, not a user-facing control seam",
128
+ "agent_session:resolveConfiguredDefaultModel": "internal accessor/plumbing, not a user-facing control seam",
128
129
  "agent_session:setModelTemporary": "internal accessor/plumbing, not a user-facing control seam",
129
130
  "agent_session:setModelTemporaryForControl":
130
131
  "internal Telegram control wrapper over the reviewed model.set seam, not an independent public SDK operation",
@@ -150,6 +151,8 @@ const LOCKED_EXCLUSIONS: Readonly<Record<string, string>> = {
150
151
  "agent_session:runIdleCompaction": "internal accessor/plumbing, not a user-facing control seam",
151
152
  "agent_session:abortBranchSummary": "internal accessor/plumbing, not a user-facing control seam",
152
153
  "agent_session:abortHandoff": "internal accessor/plumbing, not a user-facing control seam",
154
+ "agent_session:abortPromptAndWait":
155
+ "internal SDK prompt-terminalization resource fence over a host-captured run handle, not an independent public SDK control seam",
153
156
  "agent_session:prepareContributionPrep": "internal accessor/plumbing, not a user-facing control seam",
154
157
  "agent_session:setResourceSampler": "internal accessor/plumbing, not a user-facing control seam",
155
158
  "agent_session:setRetainedMemorySampler": "internal accessor/plumbing, not a user-facing control seam",