@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,285 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import * as fs from "node:fs/promises";
4
+ import * as os from "node:os";
5
+ import * as path from "node:path";
6
+
7
+ export const ACPX_VERSION = "0.13.0";
8
+ export const ACPX_GIT_HEAD = "47dc1c56b20da3c248a4a1b5c5106f52e65e6594";
9
+ /** `conformance/profiles/acp-core-v1.json#required_cases` at the pinned commit. */
10
+ export const ACP_CORE_V1_CASE_IDS = [
11
+ "acp.v1.initialize.handshake",
12
+ "acp.v1.session.new.basic",
13
+ "acp.v1.session.prompt.single_turn",
14
+ "acp.v1.session.update.termination",
15
+ "acp.v1.session.cancel.in_flight",
16
+ "acp.v1.session.cancel.idle",
17
+ "acp.v1.session.prompt.multi_turn",
18
+ "acp.v1.errors.invalid_params",
19
+ "acp.v1.errors.invalid_prompt_session_type",
20
+ "acp.v1.errors.permission_denied",
21
+ "acp.v1.errors.permission_denied.write",
22
+ "acp.v1.errors.unknown_session",
23
+ "acp.v1.session.prompt.echo_empty",
24
+ "acp.v1.session.prompt.unrecognized",
25
+ "acp.v1.errors.invalid_params.cwd_null",
26
+ "acp.v1.session.prompt.structured_blocks",
27
+ "acp.v1.permissions.read.approved",
28
+ "acp.v1.permissions.write.approved",
29
+ "acp.v1.session.prompt.background_completion",
30
+ "acp.v1.session.cancel.followup_prompt",
31
+ "acp.v1.session.prompt.post_success_drain",
32
+ ] as const;
33
+
34
+ export type AcpxMetadata = { version: string; gitHead?: string };
35
+ export type AcpxCheckout = { root: string; head: string };
36
+ export type CaseResult = { id: string; passed: boolean; [key: string]: unknown };
37
+ export type UpstreamReport = {
38
+ profileId: string;
39
+ results: CaseResult[];
40
+ totals?: { cases: number; passed: number; failed: number };
41
+ [key: string]: unknown;
42
+ };
43
+ export type ConformanceReport = {
44
+ command: string[];
45
+ cwd: string;
46
+ gjc: { commit: string; dirty: boolean };
47
+ acpx: { version: string; gitHead: string };
48
+ profile: string;
49
+ agentCommand: string;
50
+ matrix: CaseResult[];
51
+ totals: { cases: number; passed: number; failed: number };
52
+ };
53
+
54
+ export interface RunAcpConformanceOptions {
55
+ agentCommand: string;
56
+ reportPath: string;
57
+ format?: "json";
58
+ cwd?: string;
59
+ profile?: string;
60
+ fetchMetadata?: () => Promise<AcpxMetadata>;
61
+ checkout?: () => Promise<AcpxCheckout>;
62
+ runRunner?: (options: { command: string[]; cwd: string; sessionCwd?: string }) => Promise<void>;
63
+ readReport?: (reportPath: string) => Promise<unknown>;
64
+ writeReport?: (reportPath: string, report: ConformanceReport) => Promise<void>;
65
+ }
66
+
67
+ function assertProvenance(metadata: AcpxMetadata, checkout: AcpxCheckout): void {
68
+ if (metadata.version !== ACPX_VERSION)
69
+ throw new Error(`Expected acpx@${ACPX_VERSION}; received ${metadata.version}.`);
70
+ if (metadata.gitHead !== ACPX_GIT_HEAD)
71
+ throw new Error(`Expected acpx gitHead ${ACPX_GIT_HEAD}; received ${metadata.gitHead ?? "missing"}.`);
72
+ if (checkout.head !== ACPX_GIT_HEAD)
73
+ throw new Error(`Expected source checkout HEAD ${ACPX_GIT_HEAD}; received ${checkout.head}.`);
74
+ }
75
+
76
+ function validateResults(report: unknown, profile: string): CaseResult[] {
77
+ if (!report || typeof report !== "object") throw new Error("Conformance runner report is missing or malformed.");
78
+ const value = report as Partial<UpstreamReport>;
79
+ if (value.profileId !== profile || !Array.isArray(value.results))
80
+ throw new Error("Conformance runner report is missing profileId or results.");
81
+ const expected = new Set<string>(ACP_CORE_V1_CASE_IDS);
82
+ const seen = new Set<string>();
83
+ for (const result of value.results) {
84
+ if (!result || typeof result.id !== "string" || typeof result.passed !== "boolean")
85
+ throw new Error("Conformance runner report contains a malformed case result.");
86
+ if (seen.has(result.id)) throw new Error(`Conformance runner report has duplicate case ID: ${result.id}.`);
87
+ seen.add(result.id);
88
+ if (!expected.has(result.id)) throw new Error(`Conformance runner report has unexpected case ID: ${result.id}.`);
89
+ if (!result.passed) throw new Error(`Conformance case failed: ${result.id}.`);
90
+ }
91
+ for (const id of ACP_CORE_V1_CASE_IDS)
92
+ if (!seen.has(id)) throw new Error(`Conformance runner report is missing required case ID: ${id}.`);
93
+ return value.results;
94
+ }
95
+
96
+ /**
97
+ * Runs the pinned upstream conformance suite against a real ACP agent command.
98
+ *
99
+ * `--cwd` must be a real path, not one reached through a symlink (macOS `/tmp` is a
100
+ * link to `/private/tmp`): the upstream client enforces its session cwd root against
101
+ * the resolved path, so a symlinked workspace fails the client-authority cases.
102
+ */
103
+ export async function runAcpConformance(options: RunAcpConformanceOptions): Promise<ConformanceReport> {
104
+ const profile = options.profile ?? "acp-core-v1";
105
+ const requestedCwd = options.cwd ?? process.cwd();
106
+ const resolvedCwd = await fs.realpath(requestedCwd).catch(() => requestedCwd);
107
+ if (path.resolve(requestedCwd) !== resolvedCwd)
108
+ throw new Error(
109
+ `--cwd must be a real path; ${requestedCwd} resolves to ${resolvedCwd} and the client rejects paths outside its session cwd root.`,
110
+ );
111
+ if (profile !== "acp-core-v1") throw new Error(`Unsupported conformance profile: ${profile}.`);
112
+ // The upstream runner spawns the agent from its own checkout, so any repo-relative
113
+ // path in the command must be resolved against this repository first.
114
+ const agentCommand = options.agentCommand
115
+ .split(" ")
116
+ .map(token => (token.endsWith(".ts") && !path.isAbsolute(token) ? path.resolve(token) : token))
117
+ .join(" ");
118
+ const metadata = await (options.fetchMetadata ?? fetchNpmMetadata)();
119
+ const checkout = await (options.checkout ?? checkoutAcpxSource)();
120
+ assertProvenance(metadata, checkout);
121
+ const upstreamReportPath = path.join(os.tmpdir(), `gjc-acpx-${crypto.randomUUID()}.json`);
122
+ const command = [
123
+ "bun",
124
+ path.join(checkout.root, "conformance", "runner", "run.ts"),
125
+ "--profile",
126
+ path.join(checkout.root, "conformance", "profiles", `${profile}.json`),
127
+ "--cases-dir",
128
+ path.join(checkout.root, "conformance", "cases"),
129
+ "--agent-command",
130
+ agentCommand,
131
+ "--format",
132
+ "json",
133
+ "--report",
134
+ upstreamReportPath,
135
+ "--cwd",
136
+ options.cwd ?? process.cwd(),
137
+ ];
138
+ // A failing run still writes its report. Preserve that matrix as CI evidence before
139
+ // surfacing the failure, so the uploaded artifact explains what actually broke.
140
+ let runnerFailure: unknown;
141
+ try {
142
+ await (options.runRunner ?? runRunner)({ command, cwd: checkout.root, sessionCwd: options.cwd ?? process.cwd() });
143
+ } catch (error) {
144
+ runnerFailure = error;
145
+ }
146
+ const upstream = await (options.readReport ?? readReport)(upstreamReportPath).catch(() => undefined);
147
+ if (runnerFailure !== undefined) {
148
+ if (upstream && typeof upstream === "object") {
149
+ const failed = upstream as Partial<UpstreamReport>;
150
+ await (options.writeReport ?? writeReport)(options.reportPath, {
151
+ command,
152
+ cwd: options.cwd ?? process.cwd(),
153
+ gjc: gjcIdentity(),
154
+ acpx: { version: metadata.version, gitHead: metadata.gitHead as string },
155
+ profile,
156
+ agentCommand,
157
+ matrix: Array.isArray(failed.results) ? failed.results : [],
158
+ totals: failed.totals ?? { cases: 0, passed: 0, failed: 0 },
159
+ });
160
+ }
161
+ throw runnerFailure;
162
+ }
163
+ const matrix = validateResults(upstream, profile);
164
+ const report: ConformanceReport = {
165
+ command,
166
+ cwd: options.cwd ?? process.cwd(),
167
+ gjc: gjcIdentity(),
168
+ acpx: { version: metadata.version, gitHead: metadata.gitHead! },
169
+ profile,
170
+ agentCommand,
171
+ matrix,
172
+ totals: { cases: matrix.length, passed: matrix.length, failed: 0 },
173
+ };
174
+ await (options.writeReport ?? writeReport)(options.reportPath, report);
175
+ return report;
176
+ }
177
+
178
+ function gjcIdentity(): { commit: string; dirty: boolean } {
179
+ try {
180
+ const commit = Bun.spawnSync(["git", "rev-parse", "HEAD"]).stdout.toString().trim();
181
+ const dirty = Bun.spawnSync(["git", "status", "--porcelain"]).stdout.toString().trim().length > 0;
182
+ return { commit, dirty };
183
+ } catch {
184
+ return { commit: "unknown", dirty: true };
185
+ }
186
+ }
187
+
188
+ async function fetchNpmMetadata(): Promise<AcpxMetadata> {
189
+ const response = await fetch(`https://registry.npmjs.org/acpx/${ACPX_VERSION}`);
190
+ if (!response.ok)
191
+ throw new Error(`Unable to resolve acpx@${ACPX_VERSION}: ${response.status} ${response.statusText}.`);
192
+ return (await response.json()) as AcpxMetadata;
193
+ }
194
+
195
+ async function commandOutput(command: string[], cwd?: string, env?: Record<string, string>): Promise<string> {
196
+ const child = Bun.spawn(command, {
197
+ cwd,
198
+ stdout: "pipe",
199
+ stderr: "pipe",
200
+ ...(env ? { env: { ...process.env, ...env } } : {}),
201
+ });
202
+ const [code, stdout, stderr] = await Promise.all([
203
+ child.exited,
204
+ new Response(child.stdout).text(),
205
+ new Response(child.stderr).text(),
206
+ ]);
207
+ if (code !== 0)
208
+ throw new Error(`${command.join(" ")} failed (exit ${code}).\nstdout:\n${stdout}\nstderr:\n${stderr}`);
209
+ return stdout.trim();
210
+ }
211
+
212
+ async function checkoutAcpxSource(): Promise<AcpxCheckout> {
213
+ const root = path.join(os.homedir(), ".cache", "gjc", "acpx", ACPX_GIT_HEAD);
214
+ try {
215
+ await fs.access(path.join(root, ".git"));
216
+ } catch {
217
+ await fs.mkdir(path.dirname(root), { recursive: true });
218
+ await commandOutput(["git", "clone", "https://github.com/openclaw/acpx.git", root]);
219
+ }
220
+ await commandOutput(["git", "fetch", "--depth", "1", "origin", ACPX_GIT_HEAD], root);
221
+ await commandOutput(["git", "checkout", "--detach", ACPX_GIT_HEAD], root);
222
+ // A reused cache must match the pin exactly; local edits would invalidate the
223
+ // provenance the report claims.
224
+ await commandOutput(["git", "reset", "--hard", ACPX_GIT_HEAD], root);
225
+ await commandOutput(["git", "clean", "-fdx", "--exclude=node_modules"], root);
226
+ const status = await commandOutput(["git", "status", "--porcelain"], root);
227
+ if (status.length > 0) throw new Error(`acpx source checkout is not clean at ${ACPX_GIT_HEAD}.`);
228
+ return { root, head: await commandOutput(["git", "rev-parse", "HEAD"], root) };
229
+ }
230
+
231
+ async function runRunner(options: { command: string[]; cwd: string; sessionCwd?: string }): Promise<void> {
232
+ // The fixture seeds the corpus scratch workspace; the runner only creates it.
233
+ await commandOutput(
234
+ options.command,
235
+ options.cwd,
236
+ options.sessionCwd ? { GJC_ACP_CONFORMANCE_CWD: options.sessionCwd } : undefined,
237
+ );
238
+ }
239
+ async function readReport(reportPath: string): Promise<unknown> {
240
+ return await Bun.file(reportPath).json();
241
+ }
242
+ async function writeReport(reportPath: string, report: ConformanceReport): Promise<void> {
243
+ await fs.mkdir(path.dirname(reportPath), { recursive: true });
244
+ await Bun.write(reportPath, `${JSON.stringify(report, null, 2)}\n`);
245
+ }
246
+
247
+ function parseCli(argv: string[]): RunAcpConformanceOptions {
248
+ const values: Partial<RunAcpConformanceOptions> = { format: "json", profile: "acp-core-v1" };
249
+ for (let index = 0; index < argv.length; index++) {
250
+ const flag = argv[index];
251
+ if (flag === "--format") {
252
+ if (argv[++index] !== "json") throw new Error("--format must be json.");
253
+ continue;
254
+ }
255
+ if (flag === "--agent-command") {
256
+ values.agentCommand = argv[++index];
257
+ continue;
258
+ }
259
+ if (flag === "--report") {
260
+ values.reportPath = argv[++index];
261
+ continue;
262
+ }
263
+ if (flag === "--cwd") {
264
+ values.cwd = argv[++index];
265
+ continue;
266
+ }
267
+ if (flag === "--profile") {
268
+ values.profile = argv[++index];
269
+ continue;
270
+ }
271
+ throw new Error(`Unknown argument: ${flag}.`);
272
+ }
273
+ if (!values.agentCommand || !values.reportPath)
274
+ throw new Error(
275
+ "Usage: conformance:run --agent-command <command> --format json --report <path> [--cwd <path>] [--profile acp-core-v1]",
276
+ );
277
+ return values as RunAcpConformanceOptions;
278
+ }
279
+
280
+ if (import.meta.main) {
281
+ runAcpConformance(parseCli(process.argv.slice(2))).catch(error => {
282
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
283
+ process.exitCode = 1;
284
+ });
285
+ }
@@ -32,6 +32,8 @@ export interface AsyncJob {
32
32
  promise: Promise<void>;
33
33
  resultText?: string;
34
34
  errorText?: string;
35
+ /** Safe, bounded cause when session setup failed before the LLM began work. */
36
+ setupFailureSummary?: string;
35
37
  metadata?: AsyncJobMetadata;
36
38
  /**
37
39
  * Registry id of the agent that registered the job (e.g. "0-Main",
@@ -69,9 +71,12 @@ export interface AsyncJobMetadata {
69
71
  * non-terminal and non-delivering: the run suspended at a safe boundary and the
70
72
  * subagent can be resumed from its persisted sessionFile. `completed` always
71
73
  * wins a race with a late pause because the run returns it once it has actually
72
- * finished.
74
+ * finished. A `failed` outcome retains a safe setup diagnostic for receipt rendering.
73
75
  */
74
- export type SubagentRunOutcome = { kind: "completed"; text: string } | { kind: "paused"; note?: string };
76
+ export type SubagentRunOutcome =
77
+ | { kind: "completed"; text: string }
78
+ | { kind: "failed"; text: string; setupFailureSummary?: string }
79
+ | { kind: "paused"; note?: string };
75
80
 
76
81
  /** Canonical lifecycle of a subagent across pause/resume cycles. */
77
82
  export type SubagentLifecycle = "running" | "paused" | "queued" | "completed" | "failed" | "cancelled";
@@ -146,7 +151,11 @@ export interface SubagentRecord {
146
151
  historicalJobIds: string[];
147
152
  status: SubagentLifecycle;
148
153
  sessionFile: string | null;
149
- /** False for ephemeral sessions (no persistent artifacts dir). */
154
+ /**
155
+ * Explicit veto, not a complete availability result. False always denies;
156
+ * true still requires an owner-compatible descriptor or non-blank session
157
+ * file, followed by a separately available runner (`no_runner` otherwise).
158
+ */
150
159
  resumable: boolean;
151
160
  queued?: { ownerId?: string; seq: number; message?: string; createdAt: number };
152
161
  /** Resolved model the subagent was asked to use, e.g. "openai-codex/gpt-5.5". */
@@ -176,6 +185,22 @@ function sessionFileFromResumeDescriptorData(data: unknown): string | null {
176
185
  return typeof sessionFile === "string" && sessionFile.trim().length > 0 ? sessionFile : null;
177
186
  }
178
187
 
188
+ /**
189
+ * Derive retained context from an already owner-compatible descriptor or a
190
+ * legacy session file. A descriptor is sufficient even when `sessionFile` is
191
+ * null because the descriptor is the payload the resume runner consumes.
192
+ */
193
+ function hasRetainedResumeContext(input: {
194
+ resumable: boolean;
195
+ sessionFile: string | null;
196
+ descriptor: ResumeDescriptor | undefined;
197
+ }): boolean {
198
+ if (!input.resumable) return false;
199
+ return (
200
+ input.descriptor !== undefined || (typeof input.sessionFile === "string" && input.sessionFile.trim().length > 0)
201
+ );
202
+ }
203
+
179
204
  /** A pending resume awaiting a free concurrency slot. */
180
205
  interface ResumeQueueEntry {
181
206
  subagentId: string;
@@ -499,7 +524,7 @@ export class AsyncJobManager {
499
524
  typeof result === "string" ? { kind: "completed", text: result } : result;
500
525
 
501
526
  if (job.status === "cancelled") {
502
- job.resultText = outcome.kind === "completed" ? outcome.text : outcome.note;
527
+ job.resultText = outcome.kind === "paused" ? outcome.note : outcome.text;
503
528
  this.#runLifecycle(id, "terminal", job);
504
529
  this.#scheduleEviction(id);
505
530
  this.#markRecordTerminal(id, "cancelled");
@@ -509,7 +534,7 @@ export class AsyncJobManager {
509
534
  if (outcome.kind === "paused") {
510
535
  // Sole canonical writer of the running -> paused transition. No
511
536
  // delivery and no eviction scheduling: a paused subagent stays
512
- // listed and resumable from its sessionFile.
537
+ // listed and resumable from its retained resume context.
513
538
  job.status = "paused";
514
539
  this.#freezeEndTime(job);
515
540
  if (outcome.note) job.resultText = outcome.note;
@@ -517,6 +542,18 @@ export class AsyncJobManager {
517
542
  this.#drainResumeQueue();
518
543
  return;
519
544
  }
545
+ if (outcome.kind === "failed") {
546
+ job.status = "failed";
547
+ job.setupFailureSummary = outcome.setupFailureSummary;
548
+ this.#freezeEndTime(job);
549
+ job.errorText = outcome.text;
550
+ this.#enqueueDelivery(id, outcome.text);
551
+ this.#runLifecycle(id, "terminal", job);
552
+ this.#scheduleEviction(id);
553
+ this.#markRecordTerminal(id, "failed");
554
+ this.#drainResumeQueue();
555
+ return;
556
+ }
520
557
 
521
558
  job.status = "completed";
522
559
  this.#freezeEndTime(job);
@@ -684,7 +721,9 @@ export class AsyncJobManager {
684
721
  historicalJobIds: [],
685
722
  status: "completed",
686
723
  sessionFile,
687
- resumable: sessionFile !== null,
724
+ // The synthesized record copies this descriptor's owner, so the
725
+ // descriptor is owner-compatible with the record by construction.
726
+ resumable: hasRetainedResumeContext({ resumable: true, sessionFile, descriptor }),
688
727
  };
689
728
  this.#subagentRecords.set(record.subagentId, record);
690
729
  return record;
@@ -765,13 +804,24 @@ export class AsyncJobManager {
765
804
  }
766
805
 
767
806
  /**
768
- * Resolve the resume runner for a subagent: prefer the per-descriptor runner
769
- * captured at registration time (the originating parent's execution authority),
770
- * falling back to the process-global runner only for descriptors registered
771
- * without one.
807
+ * Resolve a descriptor only when `ownerId` matches the record by strict
808
+ * equality. Two undefined owners match; a distinguishably foreign descriptor
809
+ * is treated as absent for eligibility, runner selection, and execution.
772
810
  */
773
- #resolveResumeRunner(subagentId: string): ResumeRunner | undefined {
774
- return this.#descriptorResumeRunners.get(subagentId) ?? this.#resumeRunner;
811
+ #descriptorForRecord(rec: SubagentRecord): ResumeDescriptor | undefined {
812
+ const descriptor = this.#resumeDescriptors.get(rec.subagentId);
813
+ return descriptor !== undefined && descriptor.ownerId === rec.ownerId ? descriptor : undefined;
814
+ }
815
+
816
+ /**
817
+ * Resolve the resume runner for a record and its already owner-compatible
818
+ * descriptor. Per-descriptor authority is unavailable when the descriptor is foreign.
819
+ */
820
+ #resolveResumeRunner(rec: SubagentRecord, descriptor: ResumeDescriptor | undefined): ResumeRunner | undefined {
821
+ return (
822
+ (descriptor !== undefined ? this.#descriptorResumeRunners.get(rec.subagentId) : undefined) ??
823
+ this.#resumeRunner
824
+ );
775
825
  }
776
826
 
777
827
  getResumeDescriptor(subagentId: string, filter?: AsyncJobFilter): ResumeDescriptor | undefined {
@@ -1045,7 +1095,11 @@ export class AsyncJobManager {
1045
1095
  return { ok: true, status: rec.status };
1046
1096
  }
1047
1097
 
1048
- /** Resume a non-running subagent from its sessionFile, optionally injecting a message first. */
1098
+ /**
1099
+ * Resume a non-running subagent from retained context: an owner-compatible
1100
+ * descriptor or a legacy session file. Workflow routing keeps `not_found`,
1101
+ * `context_unavailable`, `no_runner`, and `resume_failed` distinct.
1102
+ */
1049
1103
  resumeSubagent(
1050
1104
  subagentId: string,
1051
1105
  filter?: AsyncJobFilter,
@@ -1066,8 +1120,11 @@ export class AsyncJobManager {
1066
1120
  }
1067
1121
  return { ok: false, status: "queued", reason: "already_queued" };
1068
1122
  }
1069
- if (!rec.resumable || !rec.sessionFile) return { ok: false, reason: "context_unavailable" };
1070
- if (!this.#resolveResumeRunner(rec.subagentId)) return { ok: false, reason: "no_runner" };
1123
+ const descriptor = this.#descriptorForRecord(rec);
1124
+ if (!hasRetainedResumeContext({ resumable: rec.resumable, sessionFile: rec.sessionFile, descriptor })) {
1125
+ return { ok: false, reason: "context_unavailable" };
1126
+ }
1127
+ if (!this.#resolveResumeRunner(rec, descriptor)) return { ok: false, reason: "no_runner" };
1071
1128
  if (this.getRunningJobs().length >= this.#maxRunningJobs) {
1072
1129
  const seq = ++this.#resumeSeq;
1073
1130
  rec.status = "queued";
@@ -1081,12 +1138,13 @@ export class AsyncJobManager {
1081
1138
  });
1082
1139
  return { ok: true, queued: true, status: "queued" };
1083
1140
  }
1084
- return this.#startResume(rec, message);
1141
+ return this.#startResume(rec, message, descriptor);
1085
1142
  }
1086
1143
 
1087
1144
  #startResume(
1088
1145
  rec: SubagentRecord,
1089
- message?: string,
1146
+ message: string | undefined,
1147
+ descriptor: ResumeDescriptor | undefined,
1090
1148
  ): { ok: boolean; status?: SubagentLifecycle; jobId?: string; reason?: string } {
1091
1149
  if (this.#isOwnerSubagentShutdownFenced(rec.ownerId)) {
1092
1150
  return { ok: false, status: rec.status, reason: "owner_shutdown_in_progress" };
@@ -1095,8 +1153,8 @@ export class AsyncJobManager {
1095
1153
  // Clear any retained progress from the previous run so a resumed subagent
1096
1154
  // never renders the prior run's tool/output as live before it emits again.
1097
1155
  this.#subagentProgress.delete(rec.subagentId);
1098
- const runner = this.#resolveResumeRunner(rec.subagentId);
1099
- const newJobId = runner?.(rec.subagentId, message, this.#resumeDescriptors.get(rec.subagentId));
1156
+ const runner = this.#resolveResumeRunner(rec, descriptor);
1157
+ const newJobId = runner?.(rec.subagentId, message, descriptor);
1100
1158
  if (!newJobId) return { ok: false, reason: "resume_failed" };
1101
1159
  if (prevJobId && prevJobId !== newJobId) rec.historicalJobIds.push(prevJobId);
1102
1160
  rec.currentJobId = newJobId;
@@ -1122,7 +1180,7 @@ export class AsyncJobManager {
1122
1180
  continue;
1123
1181
  }
1124
1182
  try {
1125
- const result = this.#startResume(rec, entry.message);
1183
+ const result = this.#startResume(rec, entry.message, this.#descriptorForRecord(rec));
1126
1184
  if (result.reason === "owner_shutdown_in_progress") {
1127
1185
  index += 1;
1128
1186
  continue;
@@ -93,7 +93,7 @@ Available Tools (default-enabled unless noted):
93
93
  bash - Execute bash commands
94
94
  edit - Edit files with find/replace
95
95
  write - Write files (creates/overwrites)
96
- grep - Search file contents
96
+ search - Search file contents
97
97
  find - Find files by glob pattern
98
98
  lsp - Language server protocol (code intelligence)
99
99
  python - Execute Python code (requires: ${APP_NAME} setup python)