@gajae-code/coding-agent 0.2.5 → 0.3.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 (234) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/types/async/job-manager.d.ts +91 -2
  3. package/dist/types/cli/args.d.ts +1 -1
  4. package/dist/types/commands/deep-interview.d.ts +3 -0
  5. package/dist/types/commands/harness.d.ts +37 -0
  6. package/dist/types/config/keybindings.d.ts +5 -0
  7. package/dist/types/config/settings-schema.d.ts +10 -4
  8. package/dist/types/config/settings.d.ts +2 -0
  9. package/dist/types/debug/crash-diagnostics.d.ts +45 -0
  10. package/dist/types/debug/runtime-gauges.d.ts +6 -0
  11. package/dist/types/deep-interview/render-middleware.d.ts +6 -0
  12. package/dist/types/eval/py/executor.d.ts +2 -0
  13. package/dist/types/eval/py/kernel.d.ts +2 -0
  14. package/dist/types/exec/bash-executor.d.ts +10 -0
  15. package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
  16. package/dist/types/extensibility/extensions/types.d.ts +6 -0
  17. package/dist/types/extensibility/shared-events.d.ts +1 -0
  18. package/dist/types/gjc-runtime/cli-write-receipt.d.ts +24 -0
  19. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -0
  20. package/dist/types/gjc-runtime/state-graph.d.ts +4 -0
  21. package/dist/types/gjc-runtime/state-migrations.d.ts +33 -0
  22. package/dist/types/gjc-runtime/state-renderer.d.ts +65 -0
  23. package/dist/types/gjc-runtime/state-runtime.d.ts +2 -0
  24. package/dist/types/gjc-runtime/state-schema.d.ts +317 -0
  25. package/dist/types/gjc-runtime/state-validation.d.ts +6 -0
  26. package/dist/types/gjc-runtime/state-writer.d.ts +147 -0
  27. package/dist/types/gjc-runtime/team-runtime.d.ts +81 -7
  28. package/dist/types/gjc-runtime/workflow-command-ref.d.ts +43 -0
  29. package/dist/types/gjc-runtime/workflow-manifest.d.ts +54 -0
  30. package/dist/types/harness-control-plane/classifier.d.ts +13 -0
  31. package/dist/types/harness-control-plane/control-endpoint.d.ts +31 -0
  32. package/dist/types/harness-control-plane/finalize.d.ts +47 -0
  33. package/dist/types/harness-control-plane/frame-mapper.d.ts +29 -0
  34. package/dist/types/harness-control-plane/operate.d.ts +35 -0
  35. package/dist/types/harness-control-plane/owner.d.ts +46 -0
  36. package/dist/types/harness-control-plane/preserve.d.ts +19 -0
  37. package/dist/types/harness-control-plane/receipts.d.ts +88 -0
  38. package/dist/types/harness-control-plane/rpc-adapter.d.ts +66 -0
  39. package/dist/types/harness-control-plane/seams.d.ts +21 -0
  40. package/dist/types/harness-control-plane/session-lease.d.ts +65 -0
  41. package/dist/types/harness-control-plane/state-machine.d.ts +19 -0
  42. package/dist/types/harness-control-plane/storage.d.ts +53 -0
  43. package/dist/types/harness-control-plane/types.d.ts +162 -0
  44. package/dist/types/hooks/skill-keywords.d.ts +2 -1
  45. package/dist/types/hooks/skill-state.d.ts +23 -29
  46. package/dist/types/internal-urls/agent-protocol.d.ts +2 -2
  47. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
  48. package/dist/types/internal-urls/registry-helpers.d.ts +8 -7
  49. package/dist/types/internal-urls/types.d.ts +4 -0
  50. package/dist/types/lsp/index.d.ts +10 -10
  51. package/dist/types/modes/bridge/auth.d.ts +12 -0
  52. package/dist/types/modes/bridge/bridge-client-bridge.d.ts +9 -0
  53. package/dist/types/modes/bridge/bridge-mode.d.ts +44 -0
  54. package/dist/types/modes/bridge/bridge-ui-context.d.ts +88 -0
  55. package/dist/types/modes/bridge/event-stream.d.ts +8 -0
  56. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  57. package/dist/types/modes/components/hook-selector.d.ts +1 -0
  58. package/dist/types/modes/components/jobs-overlay-model.d.ts +31 -0
  59. package/dist/types/modes/components/jobs-overlay.d.ts +30 -0
  60. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  61. package/dist/types/modes/components/status-line.d.ts +2 -0
  62. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  63. package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
  64. package/dist/types/modes/index.d.ts +1 -0
  65. package/dist/types/modes/interactive-mode.d.ts +2 -0
  66. package/dist/types/modes/jobs-observer.d.ts +57 -0
  67. package/dist/types/modes/rpc/host-tools.d.ts +1 -16
  68. package/dist/types/modes/rpc/host-uris.d.ts +1 -38
  69. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +20 -0
  70. package/dist/types/modes/shared/agent-wire/command-validation.d.ts +2 -0
  71. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +24 -0
  72. package/dist/types/modes/shared/agent-wire/handshake.d.ts +46 -0
  73. package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +16 -0
  74. package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +17 -0
  75. package/dist/types/modes/shared/agent-wire/protocol.d.ts +44 -0
  76. package/dist/types/modes/shared/agent-wire/responses.d.ts +4 -0
  77. package/dist/types/modes/shared/agent-wire/scopes.d.ts +18 -0
  78. package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +42 -0
  79. package/dist/types/modes/shared/agent-wire/ui-result.d.ts +27 -0
  80. package/dist/types/modes/types.d.ts +2 -0
  81. package/dist/types/sdk.d.ts +4 -0
  82. package/dist/types/session/agent-session.d.ts +19 -1
  83. package/dist/types/skill-state/active-state.d.ts +2 -0
  84. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
  85. package/dist/types/skill-state/workflow-state-contract.d.ts +25 -2
  86. package/dist/types/skill-state/workflow-state-version.d.ts +3 -0
  87. package/dist/types/task/executor.d.ts +3 -0
  88. package/dist/types/task/id.d.ts +7 -0
  89. package/dist/types/task/index.d.ts +5 -0
  90. package/dist/types/task/receipt.d.ts +85 -0
  91. package/dist/types/task/spawn-gate.d.ts +38 -0
  92. package/dist/types/task/types.d.ts +198 -14
  93. package/dist/types/tools/cron.d.ts +6 -0
  94. package/dist/types/tools/index.d.ts +2 -0
  95. package/dist/types/tools/path-utils.d.ts +1 -0
  96. package/dist/types/tools/subagent.d.ts +26 -1
  97. package/package.json +7 -7
  98. package/scripts/build-binary.ts +7 -0
  99. package/src/async/job-manager.ts +334 -6
  100. package/src/cli/args.ts +9 -2
  101. package/src/cli/auth-broker-cli.ts +1 -0
  102. package/src/cli/config-cli.ts +10 -2
  103. package/src/cli.ts +2 -0
  104. package/src/commands/deep-interview.ts +1 -0
  105. package/src/commands/harness.ts +862 -0
  106. package/src/commands/launch.ts +2 -2
  107. package/src/commands/state.ts +2 -1
  108. package/src/commands/team.ts +54 -39
  109. package/src/config/keybindings.ts +6 -0
  110. package/src/config/settings-schema.ts +13 -3
  111. package/src/config/settings.ts +5 -0
  112. package/src/dap/client.ts +17 -3
  113. package/src/debug/crash-diagnostics.ts +223 -0
  114. package/src/debug/runtime-gauges.ts +20 -0
  115. package/src/deep-interview/render-middleware.ts +372 -0
  116. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  117. package/src/defaults/gjc/skills/ralplan/SKILL.md +31 -2
  118. package/src/defaults/gjc/skills/team/SKILL.md +47 -21
  119. package/src/defaults/gjc/skills/ultragoal/SKILL.md +106 -13
  120. package/src/eval/py/executor.ts +21 -1
  121. package/src/eval/py/kernel.ts +15 -0
  122. package/src/exec/bash-executor.ts +41 -0
  123. package/src/extensibility/custom-tools/types.ts +1 -0
  124. package/src/extensibility/extensions/types.ts +6 -0
  125. package/src/extensibility/shared-events.ts +1 -0
  126. package/src/gjc-runtime/cli-write-receipt.ts +31 -0
  127. package/src/gjc-runtime/deep-interview-runtime.ts +98 -42
  128. package/src/gjc-runtime/goal-mode-request.ts +11 -3
  129. package/src/gjc-runtime/ralplan-runtime.ts +235 -43
  130. package/src/gjc-runtime/state-graph.ts +86 -0
  131. package/src/gjc-runtime/state-migrations.ts +179 -0
  132. package/src/gjc-runtime/state-renderer.ts +345 -0
  133. package/src/gjc-runtime/state-runtime.ts +1155 -46
  134. package/src/gjc-runtime/state-schema.ts +192 -0
  135. package/src/gjc-runtime/state-validation.ts +49 -0
  136. package/src/gjc-runtime/state-writer.ts +749 -0
  137. package/src/gjc-runtime/team-runtime.ts +1255 -189
  138. package/src/gjc-runtime/ultragoal-runtime.ts +460 -43
  139. package/src/gjc-runtime/workflow-command-ref.ts +239 -0
  140. package/src/gjc-runtime/workflow-manifest.generated.json +1601 -0
  141. package/src/gjc-runtime/workflow-manifest.ts +427 -0
  142. package/src/harness-control-plane/classifier.ts +128 -0
  143. package/src/harness-control-plane/control-endpoint.ts +148 -0
  144. package/src/harness-control-plane/finalize.ts +222 -0
  145. package/src/harness-control-plane/frame-mapper.ts +286 -0
  146. package/src/harness-control-plane/operate.ts +225 -0
  147. package/src/harness-control-plane/owner.ts +600 -0
  148. package/src/harness-control-plane/preserve.ts +102 -0
  149. package/src/harness-control-plane/receipts.ts +216 -0
  150. package/src/harness-control-plane/rpc-adapter.ts +276 -0
  151. package/src/harness-control-plane/seams.ts +39 -0
  152. package/src/harness-control-plane/session-lease.ts +388 -0
  153. package/src/harness-control-plane/state-machine.ts +98 -0
  154. package/src/harness-control-plane/storage.ts +257 -0
  155. package/src/harness-control-plane/types.ts +214 -0
  156. package/src/hooks/skill-keywords.ts +4 -2
  157. package/src/hooks/skill-state.ts +197 -64
  158. package/src/internal-urls/agent-protocol.ts +68 -21
  159. package/src/internal-urls/artifact-protocol.ts +12 -17
  160. package/src/internal-urls/docs-index.generated.ts +3 -2
  161. package/src/internal-urls/registry-helpers.ts +19 -16
  162. package/src/internal-urls/types.ts +4 -0
  163. package/src/lsp/client.ts +18 -2
  164. package/src/main.ts +21 -5
  165. package/src/modes/bridge/auth.ts +41 -0
  166. package/src/modes/bridge/bridge-client-bridge.ts +47 -0
  167. package/src/modes/bridge/bridge-mode.ts +520 -0
  168. package/src/modes/bridge/bridge-ui-context.ts +200 -0
  169. package/src/modes/bridge/event-stream.ts +70 -0
  170. package/src/modes/components/assistant-message.ts +5 -1
  171. package/src/modes/components/custom-editor.ts +101 -0
  172. package/src/modes/components/hook-selector.ts +133 -20
  173. package/src/modes/components/jobs-overlay-model.ts +109 -0
  174. package/src/modes/components/jobs-overlay.ts +172 -0
  175. package/src/modes/components/status-line/presets.ts +7 -5
  176. package/src/modes/components/status-line/segments.ts +25 -0
  177. package/src/modes/components/status-line/types.ts +2 -0
  178. package/src/modes/components/status-line.ts +9 -1
  179. package/src/modes/controllers/event-controller.ts +71 -6
  180. package/src/modes/controllers/extension-ui-controller.ts +43 -1
  181. package/src/modes/controllers/input-controller.ts +105 -9
  182. package/src/modes/controllers/selector-controller.ts +31 -1
  183. package/src/modes/index.ts +1 -0
  184. package/src/modes/interactive-mode.ts +28 -0
  185. package/src/modes/jobs-observer.ts +204 -0
  186. package/src/modes/rpc/host-tools.ts +1 -186
  187. package/src/modes/rpc/host-uris.ts +1 -235
  188. package/src/modes/rpc/rpc-client.ts +25 -10
  189. package/src/modes/rpc/rpc-mode.ts +12 -381
  190. package/src/modes/shared/agent-wire/command-dispatch.ts +341 -0
  191. package/src/modes/shared/agent-wire/command-validation.ts +131 -0
  192. package/src/modes/shared/agent-wire/event-envelope.ts +108 -0
  193. package/src/modes/shared/agent-wire/handshake.ts +117 -0
  194. package/src/modes/shared/agent-wire/host-tool-bridge.ts +194 -0
  195. package/src/modes/shared/agent-wire/host-uri-bridge.ts +236 -0
  196. package/src/modes/shared/agent-wire/protocol.ts +96 -0
  197. package/src/modes/shared/agent-wire/responses.ts +17 -0
  198. package/src/modes/shared/agent-wire/scopes.ts +89 -0
  199. package/src/modes/shared/agent-wire/ui-request-broker.ts +150 -0
  200. package/src/modes/shared/agent-wire/ui-result.ts +48 -0
  201. package/src/modes/types.ts +2 -0
  202. package/src/prompts/agents/executor.md +13 -0
  203. package/src/prompts/tools/subagent.md +39 -4
  204. package/src/prompts/tools/task-summary.md +3 -9
  205. package/src/prompts/tools/task.md +5 -1
  206. package/src/sdk.ts +8 -0
  207. package/src/session/agent-session.ts +445 -71
  208. package/src/session/session-manager.ts +13 -1
  209. package/src/skill-state/active-state.ts +58 -65
  210. package/src/skill-state/deep-interview-mutation-guard.ts +114 -17
  211. package/src/skill-state/initial-phase.ts +2 -0
  212. package/src/skill-state/workflow-state-contract.ts +33 -4
  213. package/src/skill-state/workflow-state-version.ts +3 -0
  214. package/src/slash-commands/builtin-registry.ts +8 -0
  215. package/src/task/executor.ts +79 -13
  216. package/src/task/id.ts +33 -0
  217. package/src/task/index.ts +376 -74
  218. package/src/task/output-manager.ts +5 -4
  219. package/src/task/receipt.ts +297 -0
  220. package/src/task/render.ts +54 -134
  221. package/src/task/spawn-gate.ts +132 -0
  222. package/src/task/types.ts +104 -10
  223. package/src/tools/ask.ts +88 -27
  224. package/src/tools/ast-edit.ts +1 -0
  225. package/src/tools/ast-grep.ts +1 -0
  226. package/src/tools/bash.ts +1 -1
  227. package/src/tools/cron.ts +48 -0
  228. package/src/tools/find.ts +4 -1
  229. package/src/tools/index.ts +2 -0
  230. package/src/tools/path-utils.ts +3 -2
  231. package/src/tools/read.ts +1 -0
  232. package/src/tools/search.ts +1 -0
  233. package/src/tools/skill.ts +6 -1
  234. package/src/tools/subagent.ts +423 -79
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Harness receipt schemas + validators (M7).
3
+ *
4
+ * Receipts are NEW schemas that FOLLOW the .gjc/state + ultragoal-ledger patterns
5
+ * (atomic, append-indexed, immutable) — not drop-in reuse. Every receipt carries a
6
+ * canonical-JSON sha256 over its content (excluding the hash field) plus referenced
7
+ * artifact hashes; validators recompute and FAIL CLOSED on tamper/mismatch.
8
+ *
9
+ * Families:
10
+ * - vanish captures dirty/unknown delta before any restart/fallback (data-loss gate)
11
+ * - prompt-acceptance proves single-flight acceptance (idle pre-state -> ack -> next agent_start)
12
+ * - validation records a verification command result for a specific commit
13
+ * - completion the finalize gate: receipt-valid + commit + PR/issue + validations
14
+ */
15
+ import { createHash } from "node:crypto";
16
+ import type { GitDelta, ReceiptFamily, RecoveryClassification } from "./types";
17
+
18
+ export interface ReceiptSubject {
19
+ workspace: string;
20
+ branch: string | null;
21
+ head: string | null;
22
+ commit: string | null;
23
+ }
24
+
25
+ export interface ReceiptEnvelope<E = Record<string, unknown>> {
26
+ receiptId: string;
27
+ schemaVersion: number;
28
+ sessionId: string;
29
+ family: ReceiptFamily;
30
+ valid: boolean;
31
+ createdAt: string;
32
+ source: string;
33
+ subject: ReceiptSubject;
34
+ evidence: E;
35
+ /** Hashes of out-of-line artifacts (diff patches, validation logs) folded into the receipt hash. */
36
+ artifactHashes: Record<string, string>;
37
+ sha256: string;
38
+ }
39
+
40
+ export const RECEIPT_SCHEMA_VERSION = 1 as const;
41
+
42
+ /** Deterministic stringify with sorted keys (stable hash basis). */
43
+ function canonicalJson(value: unknown): string {
44
+ if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
45
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
46
+ const obj = value as Record<string, unknown>;
47
+ const keys = Object.keys(obj).sort();
48
+ return `{${keys.map(k => `${JSON.stringify(k)}:${canonicalJson(obj[k])}`).join(",")}}`;
49
+ }
50
+
51
+ export function sha256Hex(input: string): string {
52
+ return createHash("sha256").update(input).digest("hex");
53
+ }
54
+
55
+ /** Hash basis = canonical JSON of the receipt without `sha256`. */
56
+ function hashBasis(receipt: Omit<ReceiptEnvelope<unknown>, "sha256">): string {
57
+ return canonicalJson(receipt);
58
+ }
59
+
60
+ export interface BuildReceiptInput<E> {
61
+ receiptId: string;
62
+ sessionId: string;
63
+ family: ReceiptFamily;
64
+ source: string;
65
+ subject: ReceiptSubject;
66
+ evidence: E;
67
+ artifactHashes?: Record<string, string>;
68
+ createdAt?: string;
69
+ valid?: boolean;
70
+ }
71
+
72
+ export function buildReceipt<E>(input: BuildReceiptInput<E>): ReceiptEnvelope<E> {
73
+ const base: Omit<ReceiptEnvelope<E>, "sha256"> = {
74
+ receiptId: input.receiptId,
75
+ schemaVersion: RECEIPT_SCHEMA_VERSION,
76
+ sessionId: input.sessionId,
77
+ family: input.family,
78
+ valid: input.valid ?? true,
79
+ createdAt: input.createdAt ?? new Date().toISOString(),
80
+ source: input.source,
81
+ subject: input.subject,
82
+ evidence: input.evidence,
83
+ artifactHashes: input.artifactHashes ?? {},
84
+ };
85
+ return { ...base, sha256: sha256Hex(hashBasis(base)) };
86
+ }
87
+
88
+ export interface ValidationOutcome {
89
+ valid: boolean;
90
+ reasons: string[];
91
+ }
92
+
93
+ /** Recompute the hash and run structural family checks. Fail-closed. */
94
+ export function validateReceipt(receipt: ReceiptEnvelope<unknown>): ValidationOutcome {
95
+ const reasons: string[] = [];
96
+ const { sha256, ...rest } = receipt;
97
+ if (sha256Hex(hashBasis(rest)) !== sha256) reasons.push("hash-mismatch");
98
+ if (receipt.schemaVersion !== RECEIPT_SCHEMA_VERSION) reasons.push("schema-version-mismatch");
99
+ const familyReasons = validateFamily(receipt);
100
+ reasons.push(...familyReasons);
101
+ return { valid: reasons.length === 0, reasons };
102
+ }
103
+
104
+ // ---- Family evidence shapes ---------------------------------------------------
105
+
106
+ export interface VanishEvidence {
107
+ classification: RecoveryClassification;
108
+ gitDelta: GitDelta;
109
+ gitStatusPorcelain: string;
110
+ untrackedManifest: { path: string; size: number; sha256: string }[];
111
+ preservation: "snapshot" | "stash" | "block";
112
+ stashRef: string | null;
113
+ snapshotComplete: boolean;
114
+ forbiddenActions: string[];
115
+ }
116
+
117
+ export interface PromptAcceptanceEvidence {
118
+ promptSha256: string;
119
+ rpcCommandId: string;
120
+ preSubmitState: { isStreaming: boolean; steeringQueueDepth: number; followupQueueDepth: number };
121
+ preSubmitCursor: number;
122
+ agentStartCursor: number;
123
+ acceptedAt: string;
124
+ singleFlight: true;
125
+ }
126
+
127
+ export interface ValidationEvidence {
128
+ command: string;
129
+ exactCommand: string;
130
+ cwd: string;
131
+ exitStatus: number;
132
+ pass: boolean;
133
+ commitUnderTest: string | null;
134
+ }
135
+
136
+ export interface CompletionEvidence {
137
+ finalCommit: string;
138
+ branch: string;
139
+ prUrl: string | null;
140
+ issueArtifact: string | null;
141
+ requiredValidationReceiptIds: string[];
142
+ finalLifecycle: string;
143
+ finalizedAt: string;
144
+ blockers: string[];
145
+ }
146
+
147
+ function validateFamily(receipt: ReceiptEnvelope<unknown>): string[] {
148
+ switch (receipt.family) {
149
+ case "vanish":
150
+ return validateVanish(receipt.evidence as VanishEvidence);
151
+ case "prompt-acceptance":
152
+ return validatePromptAcceptance(receipt.evidence as PromptAcceptanceEvidence);
153
+ case "validation":
154
+ return validateValidation(receipt.evidence as ValidationEvidence);
155
+ case "completion":
156
+ return validateCompletion(receipt.evidence as CompletionEvidence);
157
+ default:
158
+ return [`unknown-family:${receipt.family}`];
159
+ }
160
+ }
161
+
162
+ function validateVanish(e: VanishEvidence): string[] {
163
+ const reasons: string[] = [];
164
+ if (!e || typeof e.gitDelta !== "string") return ["vanish-missing-evidence"];
165
+ const protectedDelta = e.gitDelta === "dirty" || e.gitDelta === "unknown";
166
+ if (protectedDelta) {
167
+ // Hard data-loss invariant: a dirty/unknown delta must be preserved (never blocked-away),
168
+ // and the destructive actions must be explicitly forbidden.
169
+ if (e.preservation === "block") reasons.push("vanish-dirty-must-preserve-not-block");
170
+ for (const action of ["restart-clean", "delete", "reset"]) {
171
+ if (!Array.isArray(e.forbiddenActions) || !e.forbiddenActions.includes(action)) {
172
+ reasons.push(`vanish-must-forbid-${action}`);
173
+ }
174
+ }
175
+ }
176
+ if (e.preservation === "snapshot" && !e.snapshotComplete) reasons.push("vanish-snapshot-incomplete");
177
+ if (e.preservation === "stash" && !e.stashRef) reasons.push("vanish-stash-missing-ref");
178
+ return reasons;
179
+ }
180
+
181
+ function validatePromptAcceptance(e: PromptAcceptanceEvidence): string[] {
182
+ const reasons: string[] = [];
183
+ if (!e) return ["acceptance-missing-evidence"];
184
+ if (e.singleFlight !== true) reasons.push("acceptance-not-single-flight");
185
+ if (e.preSubmitState?.isStreaming) reasons.push("acceptance-pre-state-streaming");
186
+ if ((e.preSubmitState?.steeringQueueDepth ?? 1) !== 0) reasons.push("acceptance-steering-queue-nonempty");
187
+ if ((e.preSubmitState?.followupQueueDepth ?? 1) !== 0) reasons.push("acceptance-followup-queue-nonempty");
188
+ if (!(e.agentStartCursor > e.preSubmitCursor)) reasons.push("acceptance-agent-start-not-after-cursor");
189
+ return reasons;
190
+ }
191
+
192
+ function validateValidation(e: ValidationEvidence): string[] {
193
+ if (!e || typeof e.exactCommand !== "string") return ["validation-missing-evidence"];
194
+ return e.pass ? [] : ["validation-failed"];
195
+ }
196
+
197
+ function validateCompletion(e: CompletionEvidence): string[] {
198
+ const reasons: string[] = [];
199
+ if (!e) return ["completion-missing-evidence"];
200
+ if (!e.finalCommit) reasons.push("completion-missing-commit");
201
+ if (!e.prUrl && !e.issueArtifact) reasons.push("completion-missing-pr-or-issue");
202
+ if (!Array.isArray(e.requiredValidationReceiptIds) || e.requiredValidationReceiptIds.length === 0) {
203
+ reasons.push("completion-missing-validation-receipts");
204
+ }
205
+ if (Array.isArray(e.blockers) && e.blockers.length > 0) reasons.push("completion-has-blockers");
206
+ return reasons;
207
+ }
208
+
209
+ /** Classifications that MUST have a valid `vanish` receipt before the action proceeds. */
210
+ export function requiresVanishBeforeAction(classification: RecoveryClassification): boolean {
211
+ return (
212
+ classification === "restart-clean" ||
213
+ classification === "restart-preserve-delta" ||
214
+ classification === "fallback-codex-exec"
215
+ );
216
+ }
@@ -0,0 +1,276 @@
1
+ /**
2
+ * gajae-code RPC adapter + single-flight acceptance.
3
+ *
4
+ * The gajae-code harness is driven via `gjc --mode rpc` (see docs/rpc.md). Acceptance
5
+ * is a PROTOCOL FACT, not an echo: a prompt is `accepted` only when the RPC command is
6
+ * acked AND the next `agent_start` event arrives after the pre-submit cursor within the
7
+ * timeout, with an idle + empty-queue pre-state. Ack alone never means accepted.
8
+ *
9
+ * The acceptance logic ({@link singleFlightAccept}) is decoupled from the transport via
10
+ * the {@link HarnessRpc} interface so it is unit-testable with a fake, while
11
+ * {@link GajaeCodeRpc} provides the real `gjc --mode rpc` subprocess implementation
12
+ * (exercised by the M10 e2e suite).
13
+ */
14
+ import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
15
+ import { randomUUID } from "node:crypto";
16
+
17
+ export interface RpcStateSnapshot {
18
+ isStreaming: boolean;
19
+ steeringQueueDepth: number;
20
+ followupQueueDepth: number;
21
+ }
22
+
23
+ /** Abstract handle to a live gajae-code RPC session. */
24
+ export interface HarnessRpc {
25
+ getState(): Promise<RpcStateSnapshot>;
26
+ /** Send a prompt; resolves with the RPC command id and whether it was acked. Does NOT await agent_start. */
27
+ sendPrompt(prompt: string): Promise<{ commandId: string; ack: boolean }>;
28
+ /** Monotonic count of events observed so far (the acceptance cursor). */
29
+ eventCursor(): number;
30
+ /** Resolve when an `agent_start` event arrives strictly after `afterCursor`, else null on timeout. */
31
+ waitForAgentStart(afterCursor: number, timeoutMs: number): Promise<{ cursor: number } | null>;
32
+ close(): Promise<void>;
33
+ /** Subscribe to parsed event frames (non-ready, non-response), fired AFTER the cursor advances. Returns unsubscribe. */
34
+ onEventFrame?(listener: (frame: Record<string, unknown>) => void): () => void;
35
+ /** Whether the underlying RPC subprocess is still alive. */
36
+ isLive?(): boolean;
37
+ /** ISO timestamp of the last observed event frame, or null. */
38
+ lastFrameAt?(): string | null;
39
+ }
40
+
41
+ export interface AcceptanceResult {
42
+ accepted: boolean;
43
+ reason: string;
44
+ commandId: string | null;
45
+ preSubmitCursor: number;
46
+ agentStartCursor: number | null;
47
+ preSubmitState: RpcStateSnapshot;
48
+ }
49
+
50
+ /**
51
+ * Single-flight acceptance: idle + empty-queue pre-state, ack, then the NEXT
52
+ * `agent_start` after the pre-submit cursor within `timeoutMs`.
53
+ */
54
+ export async function singleFlightAccept(
55
+ rpc: HarnessRpc,
56
+ prompt: string,
57
+ timeoutMs: number,
58
+ ): Promise<AcceptanceResult> {
59
+ const pre = await rpc.getState();
60
+ const preSubmitCursor = rpc.eventCursor();
61
+ if (pre.isStreaming || pre.steeringQueueDepth > 0 || pre.followupQueueDepth > 0) {
62
+ return {
63
+ accepted: false,
64
+ reason: "pre-state-not-idle",
65
+ commandId: null,
66
+ preSubmitCursor,
67
+ agentStartCursor: null,
68
+ preSubmitState: pre,
69
+ };
70
+ }
71
+ const { commandId, ack } = await rpc.sendPrompt(prompt);
72
+ if (!ack) {
73
+ return {
74
+ accepted: false,
75
+ reason: "no-ack",
76
+ commandId,
77
+ preSubmitCursor,
78
+ agentStartCursor: null,
79
+ preSubmitState: pre,
80
+ };
81
+ }
82
+ const started = await rpc.waitForAgentStart(preSubmitCursor, timeoutMs);
83
+ if (!started) {
84
+ return {
85
+ accepted: false,
86
+ reason: "no-agent-start-within-timeout",
87
+ commandId,
88
+ preSubmitCursor,
89
+ agentStartCursor: null,
90
+ preSubmitState: pre,
91
+ };
92
+ }
93
+ return {
94
+ accepted: true,
95
+ reason: "protocol-ack-single-flight",
96
+ commandId,
97
+ preSubmitCursor,
98
+ agentStartCursor: started.cursor,
99
+ preSubmitState: pre,
100
+ };
101
+ }
102
+
103
+ interface PendingResponse {
104
+ resolve: (value: Record<string, unknown>) => void;
105
+ reject: (error: Error) => void;
106
+ }
107
+
108
+ /**
109
+ * Real adapter: spawns `gjc --mode rpc --session-dir <dir>` and speaks the JSONL
110
+ * protocol from docs/rpc.md. Verified end-to-end in the M10 suite.
111
+ */
112
+ export class GajaeCodeRpc implements HarnessRpc {
113
+ #proc: ChildProcessWithoutNullStreams;
114
+ #buffer = "";
115
+ #cursor = 0;
116
+ #pending = new Map<string, PendingResponse>();
117
+ #agentStartCursors: number[] = [];
118
+ #waiters: {
119
+ afterCursor: number;
120
+ resolve: (v: { cursor: number } | null) => void;
121
+ timer: ReturnType<typeof setTimeout>;
122
+ }[] = [];
123
+ #frameListeners: ((frame: Record<string, unknown>) => void)[] = [];
124
+ #lastFrameAt: string | null = null;
125
+ #alive = true;
126
+
127
+ constructor(opts: { sessionDir: string; command?: string[]; cwd?: string; env?: NodeJS.ProcessEnv }) {
128
+ const base = opts.command ?? ["gjc", "--mode", "rpc"];
129
+ const args = [...base.slice(1), "--session-dir", opts.sessionDir];
130
+ this.#proc = spawn(base[0], args, {
131
+ cwd: opts.cwd,
132
+ env: opts.env ?? process.env,
133
+ stdio: ["pipe", "pipe", "pipe"],
134
+ }) as ChildProcessWithoutNullStreams;
135
+ this.#proc.stdout.setEncoding("utf8");
136
+ this.#proc.stdout.on("data", chunk => this.#onData(chunk as string));
137
+ this.#proc.on("exit", () => {
138
+ this.#alive = false;
139
+ });
140
+ this.#proc.on("error", () => {
141
+ this.#alive = false;
142
+ });
143
+ }
144
+
145
+ #onData(chunk: string): void {
146
+ this.#buffer += chunk;
147
+ let idx = this.#buffer.indexOf("\n");
148
+ while (idx >= 0) {
149
+ const line = this.#buffer.slice(0, idx).trim();
150
+ this.#buffer = this.#buffer.slice(idx + 1);
151
+ if (line) this.#onFrame(line);
152
+ idx = this.#buffer.indexOf("\n");
153
+ }
154
+ }
155
+
156
+ #onFrame(line: string): void {
157
+ let frame: Record<string, unknown>;
158
+ try {
159
+ frame = JSON.parse(line) as Record<string, unknown>;
160
+ } catch {
161
+ return;
162
+ }
163
+ const type = frame.type;
164
+ if (type === "response") {
165
+ const id = typeof frame.id === "string" ? frame.id : undefined;
166
+ if (id && this.#pending.has(id)) {
167
+ const pending = this.#pending.get(id);
168
+ this.#pending.delete(id);
169
+ pending?.resolve(frame);
170
+ }
171
+ return;
172
+ }
173
+ if (type === "ready") return;
174
+ // Any other frame is a session/agent event: advance the cursor.
175
+ this.#cursor += 1;
176
+ this.#lastFrameAt = new Date().toISOString();
177
+ if (type === "agent_start") {
178
+ const cursor = this.#cursor;
179
+ this.#agentStartCursors.push(cursor);
180
+ this.#waiters = this.#waiters.filter(w => {
181
+ if (cursor > w.afterCursor) {
182
+ clearTimeout(w.timer);
183
+ w.resolve({ cursor });
184
+ return false;
185
+ }
186
+ return true;
187
+ });
188
+ }
189
+ // Fire-and-forget frame listeners (owner maps + emits). Never await; never let a listener kill the reader.
190
+ for (const listener of this.#frameListeners) {
191
+ try {
192
+ listener(frame);
193
+ } catch {
194
+ // swallow listener errors
195
+ }
196
+ }
197
+ }
198
+
199
+ #send(command: Record<string, unknown>): Promise<Record<string, unknown>> {
200
+ const id = randomUUID();
201
+ return new Promise((resolve, reject) => {
202
+ this.#pending.set(id, { resolve, reject });
203
+ this.#proc.stdin.write(`${JSON.stringify({ id, ...command })}\n`, err => {
204
+ if (err) {
205
+ this.#pending.delete(id);
206
+ reject(err);
207
+ }
208
+ });
209
+ });
210
+ }
211
+
212
+ onEventFrame(listener: (frame: Record<string, unknown>) => void): () => void {
213
+ this.#frameListeners.push(listener);
214
+ return () => {
215
+ this.#frameListeners = this.#frameListeners.filter(l => l !== listener);
216
+ };
217
+ }
218
+
219
+ isLive(): boolean {
220
+ return this.#alive;
221
+ }
222
+
223
+ lastFrameAt(): string | null {
224
+ return this.#lastFrameAt;
225
+ }
226
+
227
+ async getState(): Promise<RpcStateSnapshot> {
228
+ const res = await this.#send({ type: "get_state" });
229
+ const data = (res.data ?? {}) as Record<string, unknown>;
230
+ return {
231
+ isStreaming: Boolean(data.isStreaming),
232
+ steeringQueueDepth: typeof data.queuedMessageCount === "number" ? data.queuedMessageCount : 0,
233
+ followupQueueDepth: 0,
234
+ };
235
+ }
236
+
237
+ async sendPrompt(prompt: string): Promise<{ commandId: string; ack: boolean }> {
238
+ const id = randomUUID();
239
+ const ackPromise = new Promise<Record<string, unknown>>((resolve, reject) => {
240
+ this.#pending.set(id, { resolve, reject });
241
+ this.#proc.stdin.write(`${JSON.stringify({ id, type: "prompt", message: prompt })}\n`, err => {
242
+ if (err) {
243
+ this.#pending.delete(id);
244
+ reject(err);
245
+ }
246
+ });
247
+ });
248
+ const res = await ackPromise;
249
+ return { commandId: id, ack: res.success === true };
250
+ }
251
+
252
+ eventCursor(): number {
253
+ return this.#cursor;
254
+ }
255
+
256
+ waitForAgentStart(afterCursor: number, timeoutMs: number): Promise<{ cursor: number } | null> {
257
+ const existing = this.#agentStartCursors.find(c => c > afterCursor);
258
+ if (existing !== undefined) return Promise.resolve({ cursor: existing });
259
+ return new Promise(resolve => {
260
+ const timer = setTimeout(() => {
261
+ this.#waiters = this.#waiters.filter(w => w.timer !== timer);
262
+ resolve(null);
263
+ }, timeoutMs);
264
+ this.#waiters.push({ afterCursor, resolve, timer });
265
+ });
266
+ }
267
+
268
+ async close(): Promise<void> {
269
+ try {
270
+ this.#proc.stdin.end();
271
+ } catch {
272
+ // ignore
273
+ }
274
+ this.#proc.kill();
275
+ }
276
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * v1 seams (M11). The control plane is harness-agnostic by design, but v1 ships ONLY the
3
+ * gajae-code adapter. Other harnesses and transports are explicit, designed-not-built seams
4
+ * that fail closed with a clear `seam_unsupported_in_v1` signal rather than silently degrading.
5
+ */
6
+ import type { Harness } from "./types";
7
+
8
+ export const SUPPORTED_HARNESSES: readonly Harness[] = ["gajae-code"];
9
+
10
+ export const DEFERRED_SEAMS = [
11
+ "codex-adapter",
12
+ "omx-adapter",
13
+ "remote-transport",
14
+ "global-daemon",
15
+ "capability-token-auth",
16
+ "web-viewer",
17
+ "fleet-control-plane",
18
+ "rich-tui-coview",
19
+ ] as const;
20
+
21
+ export type DeferredSeam = (typeof DEFERRED_SEAMS)[number];
22
+
23
+ export function isHarnessSupported(harness: string): harness is Harness {
24
+ return (SUPPORTED_HARNESSES as readonly string[]).includes(harness);
25
+ }
26
+
27
+ export interface UnsupportedSeamResult {
28
+ ok: false;
29
+ error: string;
30
+ evidence: { seam: true; name: string; supported: readonly Harness[]; deferred: readonly string[] };
31
+ }
32
+
33
+ export function unsupportedSeam(name: string): UnsupportedSeamResult {
34
+ return {
35
+ ok: false,
36
+ error: `seam_unsupported_in_v1:${name}`,
37
+ evidence: { seam: true, name, supported: SUPPORTED_HARNESSES, deferred: DEFERRED_SEAMS },
38
+ };
39
+ }