@gajae-code/coding-agent 0.11.4 → 0.11.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/types/cli/notify-cli.d.ts +5 -1
  3. package/dist/types/config/settings-schema.d.ts +20 -0
  4. package/dist/types/export/html/template.generated.d.ts +1 -1
  5. package/dist/types/gjc-runtime/deep-interview-state.d.ts +28 -0
  6. package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -0
  7. package/dist/types/gjc-runtime/managed-owner-admission.d.ts +42 -0
  8. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +41 -0
  9. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +9 -0
  10. package/dist/types/gjc-runtime/ultragoal-owner-loss-recovery.d.ts +94 -0
  11. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
  12. package/dist/types/harness-control-plane/owner.d.ts +3 -1
  13. package/dist/types/internal-urls/local-protocol.d.ts +18 -0
  14. package/dist/types/main.d.ts +2 -2
  15. package/dist/types/modes/components/ansi-display-validator.d.ts +2 -0
  16. package/dist/types/modes/components/notifications-settings-editor.d.ts +4 -0
  17. package/dist/types/modes/components/session-observer-overlay.d.ts +3 -1
  18. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  19. package/dist/types/modes/components/tool-transcript-format.d.ts +58 -0
  20. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +3 -0
  21. package/dist/types/modes/controllers/goal-mode-controller.d.ts +8 -1
  22. package/dist/types/modes/controllers/selector-controller.d.ts +15 -0
  23. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +3 -0
  24. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +16 -0
  25. package/dist/types/plan-mode/approved-plan.d.ts +1 -0
  26. package/dist/types/runtime-mcp/manager.d.ts +2 -0
  27. package/dist/types/runtime-mcp/oauth-discovery.d.ts +12 -1
  28. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -1
  29. package/dist/types/sdk/bus/chat-daemon-control.d.ts +39 -4
  30. package/dist/types/sdk/bus/config-commands.d.ts +8 -0
  31. package/dist/types/sdk/bus/config.d.ts +22 -0
  32. package/dist/types/sdk/bus/index.d.ts +162 -2
  33. package/dist/types/sdk/bus/notification-service.d.ts +71 -8
  34. package/dist/types/sdk/bus/session-control.d.ts +11 -8
  35. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +5 -0
  36. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -3
  37. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +23 -4
  38. package/dist/types/sdk/bus/telegram-daemon.d.ts +214 -20
  39. package/dist/types/sdk/session.d.ts +16 -1
  40. package/dist/types/secrets/index.d.ts +4 -2
  41. package/dist/types/secrets/obfuscator.d.ts +1 -1
  42. package/dist/types/session/agent-session.d.ts +31 -2
  43. package/dist/types/session/artifacts.d.ts +30 -8
  44. package/dist/types/session/internal/managed-session-scope.d.ts +7 -0
  45. package/dist/types/session/internal/managed-session-storage.d.ts +63 -6
  46. package/dist/types/session/session-manager.d.ts +88 -18
  47. package/dist/types/session/session-storage.d.ts +27 -0
  48. package/dist/types/session/streaming-output.d.ts +4 -0
  49. package/dist/types/task/executor.d.ts +9 -0
  50. package/dist/types/tool-discovery/tool-index.d.ts +2 -0
  51. package/dist/types/tools/ask.d.ts +48 -0
  52. package/dist/types/tools/bash-skill-urls.d.ts +2 -2
  53. package/dist/types/tools/index.d.ts +4 -0
  54. package/package.json +9 -9
  55. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  56. package/src/cli/notify-cli.ts +40 -18
  57. package/src/commands/sdk.ts +7 -2
  58. package/src/config/settings-schema.ts +17 -0
  59. package/src/daemon/runtime.ts +2 -2
  60. package/src/defaults/gjc/skills/deep-interview/SKILL.md +70 -49
  61. package/src/defaults/gjc/skills/ralplan/SKILL.md +0 -6
  62. package/src/defaults/gjc/skills/team/SKILL.md +7 -9
  63. package/src/defaults/gjc/skills/ultragoal/SKILL.md +0 -2
  64. package/src/eval/py/index.ts +2 -2
  65. package/src/export/html/template.generated.ts +1 -1
  66. package/src/extensibility/plugins/marketplace/manager.ts +94 -10
  67. package/src/gjc-runtime/deep-interview-recorder.ts +7 -0
  68. package/src/gjc-runtime/deep-interview-runtime.ts +10 -0
  69. package/src/gjc-runtime/deep-interview-state.ts +127 -3
  70. package/src/gjc-runtime/launch-tmux.ts +61 -6
  71. package/src/gjc-runtime/managed-owner-admission.ts +352 -0
  72. package/src/gjc-runtime/managed-owner-supervisor.ts +162 -0
  73. package/src/gjc-runtime/state-runtime.ts +38 -12
  74. package/src/gjc-runtime/team-runtime.ts +1 -1
  75. package/src/gjc-runtime/tmux-owner-isolation.ts +117 -0
  76. package/src/gjc-runtime/ultragoal-owner-loss-recovery.ts +328 -0
  77. package/src/gjc-runtime/ultragoal-runtime.ts +14 -0
  78. package/src/harness-control-plane/owner.ts +59 -3
  79. package/src/hindsight/client.ts +65 -2
  80. package/src/internal-urls/agent-protocol.ts +76 -5
  81. package/src/internal-urls/docs-index.generated.ts +7 -6
  82. package/src/internal-urls/local-protocol.ts +416 -18
  83. package/src/lsp/index.ts +4 -88
  84. package/src/main.ts +37 -13
  85. package/src/modes/acp/acp-agent.ts +8 -0
  86. package/src/modes/components/ansi-display-validator.ts +54 -0
  87. package/src/modes/components/notifications-settings-editor.ts +26 -2
  88. package/src/modes/components/session-observer-overlay.ts +40 -24
  89. package/src/modes/components/settings-selector.ts +3 -1
  90. package/src/modes/components/tool-transcript-format.ts +321 -0
  91. package/src/modes/components/transcript-viewer-overlay.ts +39 -17
  92. package/src/modes/controllers/goal-mode-controller.ts +20 -1
  93. package/src/modes/controllers/plan-mode-controller.ts +2 -0
  94. package/src/modes/controllers/runtime-mcp-command-controller.ts +3 -0
  95. package/src/modes/controllers/selector-controller.ts +70 -8
  96. package/src/modes/interactive-mode.ts +1 -0
  97. package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -1
  98. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +17 -1
  99. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +4 -2
  100. package/src/modes/shared/agent-wire/workflow-gate-types.ts +120 -3
  101. package/src/plan-mode/approved-plan.ts +3 -1
  102. package/src/prompts/agents/architect.md +1 -0
  103. package/src/prompts/agents/critic.md +1 -0
  104. package/src/prompts/system/custom-system-prompt.md +1 -1
  105. package/src/prompts/system/system-prompt.md +2 -1
  106. package/src/prompts/tools/lsp.md +3 -3
  107. package/src/runtime-mcp/manager.ts +16 -1
  108. package/src/runtime-mcp/oauth-discovery.ts +116 -26
  109. package/src/sdk/broker/session-index.ts +122 -64
  110. package/src/sdk/bus/chat-daemon-cli.ts +72 -10
  111. package/src/sdk/bus/chat-daemon-control.ts +526 -130
  112. package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
  113. package/src/sdk/bus/config-commands.ts +22 -0
  114. package/src/sdk/bus/config.ts +37 -0
  115. package/src/sdk/bus/index.ts +384 -92
  116. package/src/sdk/bus/lifecycle-control-runtime.ts +53 -3
  117. package/src/sdk/bus/notification-service.ts +284 -62
  118. package/src/sdk/bus/recent-activity.ts +14 -5
  119. package/src/sdk/bus/session-control.ts +150 -44
  120. package/src/sdk/bus/telegram-daemon-cli.ts +20 -14
  121. package/src/sdk/bus/telegram-daemon-contract.ts +6 -3
  122. package/src/sdk/bus/telegram-daemon-control.ts +294 -76
  123. package/src/sdk/bus/telegram-daemon.ts +1788 -267
  124. package/src/sdk/protocol/operation-inventory.generated.json +11 -0
  125. package/src/sdk/session.ts +155 -27
  126. package/src/secrets/index.ts +53 -10
  127. package/src/secrets/obfuscator.ts +21 -18
  128. package/src/secrets/regex.ts +2 -0
  129. package/src/session/agent-session.ts +495 -253
  130. package/src/session/artifacts.ts +188 -23
  131. package/src/session/context-estimation.ts +7 -2
  132. package/src/session/internal/managed-session-scope.ts +200 -38
  133. package/src/session/internal/managed-session-storage.ts +1152 -46
  134. package/src/session/session-manager.ts +1055 -148
  135. package/src/session/session-storage.ts +109 -11
  136. package/src/session/streaming-output.ts +6 -0
  137. package/src/task/executor.ts +71 -26
  138. package/src/task/index.ts +63 -20
  139. package/src/tool-discovery/tool-index.ts +18 -0
  140. package/src/tools/ask.ts +129 -32
  141. package/src/tools/bash-skill-urls.ts +14 -3
  142. package/src/tools/bash.ts +2 -2
  143. package/src/tools/index.ts +4 -0
  144. package/src/tools/plan-mode-guard.ts +1 -0
  145. package/src/tools/sqlite-reader.ts +3 -2
  146. package/src/tools/subagent.ts +2 -0
@@ -0,0 +1,352 @@
1
+ import { Buffer } from "node:buffer";
2
+ import * as crypto from "node:crypto";
3
+ import * as fs from "node:fs/promises";
4
+ import * as path from "node:path";
5
+ import { openRecoveryFsRoot } from "@gajae-code/natives";
6
+ import {
7
+ MANAGED_OWNER_CHILD_TOKEN_ENV,
8
+ MANAGED_OWNER_GENERATION_ENV,
9
+ MANAGED_OWNER_INCARNATION_ENV,
10
+ MANAGED_OWNER_RUN_ID_ENV,
11
+ MANAGED_OWNER_SESSION_ID_ENV,
12
+ MANAGED_OWNER_STATE_DIR_ENV,
13
+ type ManagedOwnerBinding,
14
+ type ManagedOwnerSigabrtReceipt,
15
+ } from "./managed-owner-supervisor";
16
+ import { assertSafePathComponent } from "./session-layout";
17
+ import { lifecyclePaths } from "./tmux-owner-isolation";
18
+ import {
19
+ persistUltragoalRecoveryDecision,
20
+ planUltragoalOwnerLossRecovery,
21
+ type UltragoalRecoveryDecision,
22
+ } from "./ultragoal-owner-loss-recovery";
23
+
24
+ export const MANAGED_OWNER_PREDECESSOR_TOKEN_ENV = "GJC_MANAGED_OWNER_PREDECESSOR_TOKEN";
25
+ export const MANAGED_OWNER_PREDECESSOR_GENERATION_ENV = "GJC_MANAGED_OWNER_PREDECESSOR_GENERATION";
26
+ export const MANAGED_OWNER_PREDECESSOR_RUN_ID_ENV = "GJC_MANAGED_OWNER_PREDECESSOR_RUN_ID";
27
+ export const MANAGED_OWNER_PREDECESSOR_INCARNATION_ENV = "GJC_MANAGED_OWNER_PREDECESSOR_INCARNATION";
28
+ export const MANAGED_OWNER_TRANSCRIPT_PATH_ENV = "GJC_MANAGED_OWNER_TRANSCRIPT_PATH";
29
+
30
+ export interface ManagedOwnerRecoveryContext {
31
+ root: string;
32
+ binding: ManagedOwnerBinding;
33
+ receipt: ManagedOwnerSigabrtReceipt;
34
+ admission: { session_id: string; endpoint_incarnation: string; owner_generation: string; admitted: true };
35
+ decision: UltragoalRecoveryDecision;
36
+ }
37
+ export type ManagedOwnerAdmission =
38
+ | { kind: "fresh" | "supervised" }
39
+ | { kind: "recovery"; context: ManagedOwnerRecoveryContext }
40
+ | { kind: "blocked" };
41
+
42
+ function ownerEnvironment(): {
43
+ root: string;
44
+ generation: string;
45
+ sessionId: string;
46
+ runId: string;
47
+ incarnation: string;
48
+ } | null {
49
+ const stateDir = process.env[MANAGED_OWNER_STATE_DIR_ENV]?.trim();
50
+ const sessionId = process.env[MANAGED_OWNER_SESSION_ID_ENV]?.trim();
51
+ const generation = process.env[MANAGED_OWNER_GENERATION_ENV]?.trim();
52
+ const runId = process.env[MANAGED_OWNER_RUN_ID_ENV]?.trim();
53
+ const incarnation = process.env[MANAGED_OWNER_INCARNATION_ENV]?.trim();
54
+ if (!stateDir && !sessionId && !generation && !runId && !incarnation) return null;
55
+ if (!stateDir || !sessionId || !generation || !runId || !incarnation || !path.isAbsolute(stateDir))
56
+ throw new Error("managed_owner_admission_metadata_invalid");
57
+ for (const [value, label] of [
58
+ [sessionId, "managed owner session id"],
59
+ [generation, "managed owner generation"],
60
+ [runId, "managed owner run id"],
61
+ [incarnation, "managed owner incarnation"],
62
+ ] as const)
63
+ assertSafePathComponent(value, label);
64
+ const root = lifecyclePaths(stateDir, sessionId, generation).root;
65
+ if (!root.startsWith(`${path.resolve(stateDir)}${path.sep}`)) throw new Error("managed_owner_admission_path_unsafe");
66
+ return { root, generation, sessionId, runId, incarnation };
67
+ }
68
+
69
+ function isCommand(command: unknown): command is string[] {
70
+ return (
71
+ Array.isArray(command) &&
72
+ command.length > 0 &&
73
+ command.every(value => typeof value === "string" && value.length > 0)
74
+ );
75
+ }
76
+
77
+ function isBinding(
78
+ value: unknown,
79
+ expected: { generation: string; sessionId: string; runId: string; incarnation: string; token: string },
80
+ ): value is ManagedOwnerBinding {
81
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
82
+ const binding = value as Partial<ManagedOwnerBinding>;
83
+ return (
84
+ binding.schema_version === 2 &&
85
+ binding.generation === expected.generation &&
86
+ binding.session_id === expected.sessionId &&
87
+ binding.run_id === expected.runId &&
88
+ binding.endpoint_incarnation === expected.incarnation &&
89
+ binding.child_token === expected.token &&
90
+ isCommand(binding.command) &&
91
+ typeof binding.command_sha256 === "string" &&
92
+ binding.command_sha256 === crypto.createHash("sha256").update(JSON.stringify(binding.command)).digest("hex") &&
93
+ typeof binding.supervisor_pid === "number" &&
94
+ Number.isSafeInteger(binding.supervisor_pid) &&
95
+ binding.supervisor_pid > 0 &&
96
+ typeof binding.supervisor_start_time === "string" &&
97
+ typeof binding.created_at === "string"
98
+ );
99
+ }
100
+
101
+ function isReceipt(value: unknown, binding: ManagedOwnerBinding): value is ManagedOwnerSigabrtReceipt {
102
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
103
+ const receipt = value as Partial<ManagedOwnerSigabrtReceipt>;
104
+ return (
105
+ receipt.schema_version === 2 &&
106
+ receipt.generation === binding.generation &&
107
+ receipt.session_id === binding.session_id &&
108
+ receipt.run_id === binding.run_id &&
109
+ receipt.endpoint_incarnation === binding.endpoint_incarnation &&
110
+ receipt.child_token === binding.child_token &&
111
+ receipt.command_sha256 === binding.command_sha256 &&
112
+ receipt.supervisor_pid === binding.supervisor_pid &&
113
+ receipt.supervisor_start_time === binding.supervisor_start_time &&
114
+ typeof receipt.child_pid === "number" &&
115
+ Number.isSafeInteger(receipt.child_pid) &&
116
+ receipt.child_pid > 0 &&
117
+ typeof receipt.child_start_time === "string" &&
118
+ receipt.signal === "SIGABRT" &&
119
+ receipt.signal_number === 6 &&
120
+ (receipt.exit_code === null || Number.isSafeInteger(receipt.exit_code)) &&
121
+ typeof receipt.received_at === "string"
122
+ );
123
+ }
124
+
125
+ function safeChildToken(value: string): boolean {
126
+ try {
127
+ assertSafePathComponent(value, "managed owner child token");
128
+ return true;
129
+ } catch {
130
+ return false;
131
+ }
132
+ }
133
+
134
+ async function readExactJsons(root: string, files: readonly string[]): Promise<unknown[] | null> {
135
+ if (process.platform !== "linux") return null;
136
+ try {
137
+ const authority = openRecoveryFsRoot(root);
138
+ try {
139
+ const values: unknown[] = [];
140
+ for (const file of files) {
141
+ const result = authority.read(file, 64 * 1024);
142
+ if (!result.ok || !result.data) return null;
143
+ const content = Buffer.from(result.data).toString("utf8");
144
+ if (!content.endsWith("\n") || content.indexOf("\n") !== content.length - 1) return null;
145
+ values.push(JSON.parse(content));
146
+ }
147
+ return values;
148
+ } finally {
149
+ authority.close();
150
+ }
151
+ } catch {
152
+ return null;
153
+ }
154
+ }
155
+
156
+ async function durableHandoff(
157
+ root: string,
158
+ generation: string,
159
+ sessionId: string,
160
+ reason: string,
161
+ details: Record<string, unknown> = {},
162
+ ): Promise<void> {
163
+ await fs.mkdir(root, { recursive: true, mode: 0o700 });
164
+ const file = path.join(root, `admission-handoff-${crypto.randomUUID()}.json`);
165
+ const record = {
166
+ schema_version: 2,
167
+ generation,
168
+ session_id: sessionId,
169
+ state: "fail_closed_handoff",
170
+ reason,
171
+ ...details,
172
+ created_at: new Date().toISOString(),
173
+ };
174
+ const handle = await fs.open(file, "wx", 0o600);
175
+ try {
176
+ await handle.writeFile(`${JSON.stringify(record)}\n`);
177
+ await handle.sync();
178
+ } finally {
179
+ await handle.close();
180
+ }
181
+ const directory = await fs.open(root, "r");
182
+ try {
183
+ await directory.sync();
184
+ } finally {
185
+ await directory.close();
186
+ }
187
+ }
188
+
189
+ /**
190
+ * This is the pre-CLI barrier. A replacement is identified only by the exact
191
+ * predecessor token supplied by its launch binding; directory enumeration is
192
+ * deliberately never an authority source.
193
+ */
194
+ export async function admitManagedOwnerBeforeCli(): Promise<ManagedOwnerAdmission> {
195
+ const owner = ownerEnvironment();
196
+ if (!owner) return { kind: "fresh" };
197
+ const childToken = process.env[MANAGED_OWNER_CHILD_TOKEN_ENV]?.trim();
198
+ const predecessorToken = process.env[MANAGED_OWNER_PREDECESSOR_TOKEN_ENV]?.trim();
199
+ if (childToken && !predecessorToken) {
200
+ if (!safeChildToken(childToken)) {
201
+ await durableHandoff(owner.root, owner.generation, owner.sessionId, "exact_child_binding_unavailable");
202
+ process.exitCode = 75;
203
+ return { kind: "blocked" };
204
+ }
205
+ const [binding] = (await readExactJsons(owner.root, [`child-${childToken}.binding.json`])) ?? [];
206
+ if (
207
+ isBinding(binding, {
208
+ generation: owner.generation,
209
+ sessionId: owner.sessionId,
210
+ runId: owner.runId,
211
+ incarnation: owner.incarnation,
212
+ token: childToken,
213
+ })
214
+ )
215
+ return { kind: "supervised" };
216
+ await durableHandoff(owner.root, owner.generation, owner.sessionId, "exact_child_binding_unavailable");
217
+ process.exitCode = 75;
218
+ return { kind: "blocked" };
219
+ }
220
+ const predecessorGeneration = process.env[MANAGED_OWNER_PREDECESSOR_GENERATION_ENV]?.trim();
221
+ const predecessorRunId = process.env[MANAGED_OWNER_PREDECESSOR_RUN_ID_ENV]?.trim();
222
+ const predecessorIncarnation = process.env[MANAGED_OWNER_PREDECESSOR_INCARNATION_ENV]?.trim();
223
+ if (!predecessorGeneration || !predecessorRunId || !predecessorIncarnation) {
224
+ await durableHandoff(owner.root, owner.generation, owner.sessionId, "replacement_predecessor_identity_missing");
225
+ process.exitCode = 75;
226
+ return { kind: "blocked" };
227
+ }
228
+ if (!predecessorToken) {
229
+ await durableHandoff(owner.root, owner.generation, owner.sessionId, "replacement_predecessor_binding_missing");
230
+ process.exitCode = 75;
231
+ return { kind: "blocked" };
232
+ }
233
+ if (!safeChildToken(predecessorToken)) {
234
+ await durableHandoff(owner.root, owner.generation, owner.sessionId, "replacement_predecessor_binding_untrusted");
235
+ process.exitCode = 75;
236
+ return { kind: "blocked" };
237
+ }
238
+ const [binding, receipt] =
239
+ (await readExactJsons(owner.root, [
240
+ `child-${predecessorToken}.binding.json`,
241
+ `sigabrt-${predecessorToken}.receipt.json`,
242
+ ])) ?? [];
243
+ if (
244
+ !isBinding(binding, {
245
+ generation: predecessorGeneration,
246
+ sessionId: owner.sessionId,
247
+ runId: predecessorRunId,
248
+ incarnation: predecessorIncarnation,
249
+ token: predecessorToken,
250
+ })
251
+ ) {
252
+ await durableHandoff(owner.root, owner.generation, owner.sessionId, "replacement_predecessor_binding_untrusted");
253
+ process.exitCode = 75;
254
+ return { kind: "blocked" };
255
+ }
256
+ if (!isReceipt(receipt, binding)) {
257
+ await durableHandoff(owner.root, owner.generation, owner.sessionId, "exact_sigabrt_receipt_untrusted");
258
+ process.exitCode = 75;
259
+ return { kind: "blocked" };
260
+ }
261
+ const admission = {
262
+ session_id: owner.sessionId,
263
+ endpoint_incarnation: predecessorIncarnation,
264
+ owner_generation: predecessorGeneration,
265
+ admitted: true,
266
+ } as const;
267
+ const decision = await planUltragoalOwnerLossRecovery({
268
+ binding: {
269
+ sessionId: owner.sessionId,
270
+ endpointIncarnation: predecessorIncarnation,
271
+ ownerGeneration: predecessorGeneration,
272
+ cwd: process.cwd(),
273
+ },
274
+ receipt,
275
+ admission,
276
+ transcriptPath: process.env[MANAGED_OWNER_TRANSCRIPT_PATH_ENV] ?? "",
277
+ });
278
+ await persistUltragoalRecoveryDecision({
279
+ cwd: process.cwd(),
280
+ sessionId: owner.sessionId,
281
+ binding: {
282
+ sessionId: owner.sessionId,
283
+ endpointIncarnation: predecessorIncarnation,
284
+ ownerGeneration: predecessorGeneration,
285
+ cwd: process.cwd(),
286
+ },
287
+ decision,
288
+ });
289
+ if (decision.disposition !== "resume") {
290
+ await durableHandoff(owner.root, owner.generation, owner.sessionId, decision.reason);
291
+ process.exitCode = 75;
292
+ return { kind: "blocked" };
293
+ }
294
+ return { kind: "recovery", context: { root: owner.root, binding, receipt, admission, decision } };
295
+ }
296
+
297
+ /**
298
+ * The ordinary CLI factory has no recovery-owned writer-lease or exact-child
299
+ * reconciliation capability. Do not route a recovered owner through it: first
300
+ * revalidate the immutable evidence and B0, then publish a terminal handoff.
301
+ */
302
+ export async function completeManagedOwnerRecovery(
303
+ context: ManagedOwnerRecoveryContext,
304
+ ): Promise<{ kind: "handoff"; exitCode: 75 }> {
305
+ const recoveryBinding = {
306
+ sessionId: context.binding.session_id,
307
+ endpointIncarnation: context.binding.endpoint_incarnation,
308
+ ownerGeneration: context.binding.generation,
309
+ cwd: process.cwd(),
310
+ };
311
+ const revalidated = await planUltragoalOwnerLossRecovery({
312
+ binding: recoveryBinding,
313
+ receipt: context.receipt,
314
+ admission: context.admission,
315
+ transcriptPath: process.env[MANAGED_OWNER_TRANSCRIPT_PATH_ENV] ?? "",
316
+ protectedPaths: context.decision.snapshot?.protectedPaths,
317
+ sanctionedDeltas: context.decision.snapshot?.sanctionedDeltas,
318
+ absentArtifacts: context.decision.snapshot?.absentArtifacts,
319
+ transientHistory: context.decision.snapshot?.transientHistory,
320
+ });
321
+ const b0Unchanged =
322
+ context.decision.snapshot !== undefined &&
323
+ revalidated.snapshot !== undefined &&
324
+ context.decision.snapshot.b0.planSha256 === revalidated.snapshot.b0.planSha256 &&
325
+ context.decision.snapshot.b0.ledgerSha256 === revalidated.snapshot.b0.ledgerSha256;
326
+ const transcriptUnchanged =
327
+ context.decision.terminal?.yieldId !== undefined &&
328
+ revalidated.terminal?.yieldId === context.decision.terminal.yieldId;
329
+ const reason =
330
+ revalidated.disposition !== "resume"
331
+ ? `recovery_authority_changed:${revalidated.reason}`
332
+ : !b0Unchanged
333
+ ? "recovery_b0_changed"
334
+ : !transcriptUnchanged
335
+ ? "recovery_transcript_changed"
336
+ : "safe_session_resume_seam_unavailable";
337
+ const decision: UltragoalRecoveryDecision = { disposition: "handoff", reason };
338
+ await persistUltragoalRecoveryDecision({
339
+ cwd: recoveryBinding.cwd,
340
+ sessionId: recoveryBinding.sessionId,
341
+ binding: recoveryBinding,
342
+ decision,
343
+ });
344
+ await durableHandoff(context.root, context.binding.generation, context.binding.session_id, reason, {
345
+ predecessor_child_token: context.binding.child_token,
346
+ predecessor_run_id: context.binding.run_id,
347
+ terminal_reconciliation: "unavailable_without_owning_store_cas",
348
+ b0_preserved: b0Unchanged,
349
+ });
350
+ process.exitCode = 75;
351
+ return { kind: "handoff", exitCode: 75 };
352
+ }
@@ -0,0 +1,162 @@
1
+ import * as crypto from "node:crypto";
2
+ import * as fs from "node:fs/promises";
3
+ import * as path from "node:path";
4
+ import { readLinuxProcStartTime } from "./linux-proc";
5
+ import { assertSafePathComponent } from "./session-layout";
6
+ import { lifecyclePaths } from "./tmux-owner-isolation";
7
+
8
+ export const MANAGED_OWNER_SUPERVISOR_ARG = "--internal-managed-owner-supervisor";
9
+ export const MANAGED_OWNER_CHILD_TOKEN_ENV = "GJC_MANAGED_OWNER_CHILD_TOKEN";
10
+ export const MANAGED_OWNER_COMMAND_ENV = "GJC_MANAGED_OWNER_COMMAND_JSON";
11
+ export const MANAGED_OWNER_SESSION_ID_ENV = "GJC_COORDINATOR_SESSION_ID";
12
+ export const MANAGED_OWNER_GENERATION_ENV = "GJC_TMUX_OWNER_GENERATION";
13
+ export const MANAGED_OWNER_STATE_DIR_ENV = "GJC_TMUX_OWNER_STATE_DIR";
14
+ export const MANAGED_OWNER_RUN_ID_ENV = "GJC_MANAGED_OWNER_RUN_ID";
15
+ export const MANAGED_OWNER_INCARNATION_ENV = "GJC_MANAGED_OWNER_INCARNATION";
16
+
17
+ export interface ManagedOwnerBinding {
18
+ schema_version: 2;
19
+ generation: string;
20
+ session_id: string;
21
+ run_id: string;
22
+ endpoint_incarnation: string;
23
+ child_token: string;
24
+ command: string[];
25
+ command_sha256: string;
26
+ supervisor_pid: number;
27
+ supervisor_start_time: string;
28
+ created_at: string;
29
+ }
30
+
31
+ export interface ManagedOwnerSigabrtReceipt {
32
+ schema_version: 2;
33
+ generation: string;
34
+ session_id: string;
35
+ run_id: string;
36
+ endpoint_incarnation: string;
37
+ child_token: string;
38
+ command_sha256: string;
39
+ supervisor_pid: number;
40
+ supervisor_start_time: string;
41
+ child_pid: number;
42
+ child_start_time: string;
43
+ signal: "SIGABRT";
44
+ signal_number: 6;
45
+ exit_code: number | null;
46
+ received_at: string;
47
+ }
48
+
49
+ function requiredEnvironment(name: string): string {
50
+ const value = process.env[name]?.trim();
51
+ if (!value) throw new Error(`managed_owner_${name.toLowerCase()}_missing`);
52
+ return value;
53
+ }
54
+
55
+ function lifecycleRoot(): { root: string; generation: string; sessionId: string; runId: string; incarnation: string } {
56
+ const stateDir = requiredEnvironment(MANAGED_OWNER_STATE_DIR_ENV);
57
+ const sessionId = requiredEnvironment(MANAGED_OWNER_SESSION_ID_ENV);
58
+ const generation = requiredEnvironment(MANAGED_OWNER_GENERATION_ENV);
59
+ const runId = requiredEnvironment(MANAGED_OWNER_RUN_ID_ENV);
60
+ const incarnation = requiredEnvironment(MANAGED_OWNER_INCARNATION_ENV);
61
+ for (const [value, label] of [
62
+ [sessionId, "managed owner session id"],
63
+ [generation, "managed owner generation"],
64
+ [runId, "managed owner run id"],
65
+ [incarnation, "managed owner incarnation"],
66
+ ] as const)
67
+ assertSafePathComponent(value, label);
68
+ if (!path.isAbsolute(stateDir)) throw new Error("managed_owner_lifecycle_path_unsafe");
69
+ const root = lifecyclePaths(stateDir, sessionId, generation).root;
70
+ if (!root.startsWith(`${path.resolve(stateDir)}${path.sep}`)) throw new Error("managed_owner_lifecycle_path_unsafe");
71
+ return { root, generation, sessionId, runId, incarnation };
72
+ }
73
+
74
+ function commandDigest(command: readonly string[]): string {
75
+ return crypto.createHash("sha256").update(JSON.stringify(command)).digest("hex");
76
+ }
77
+
78
+ async function writeDurableExclusive(file: string, value: object): Promise<void> {
79
+ const handle = await fs.open(file, "wx", 0o600);
80
+ try {
81
+ await handle.writeFile(`${JSON.stringify(value)}\n`);
82
+ await handle.sync();
83
+ } finally {
84
+ await handle.close();
85
+ }
86
+ const directory = await fs.open(path.dirname(file), "r");
87
+ try {
88
+ await directory.sync();
89
+ } finally {
90
+ await directory.close();
91
+ }
92
+ const persisted = JSON.parse(await fs.readFile(file, "utf8")) as unknown;
93
+ if (JSON.stringify(persisted) !== JSON.stringify(value)) throw new Error("managed_owner_durable_reread_failed");
94
+ }
95
+
96
+ function childCommand(): string[] {
97
+ const command = JSON.parse(requiredEnvironment(MANAGED_OWNER_COMMAND_ENV)) as unknown;
98
+ if (!Array.isArray(command) || command.length === 0 || command.some(value => typeof value !== "string" || !value))
99
+ throw new Error("managed_owner_command_invalid");
100
+ return command;
101
+ }
102
+
103
+ export function isManagedOwnerSupervisorArgv(args: readonly string[]): boolean {
104
+ return args.length === 1 && args[0] === MANAGED_OWNER_SUPERVISOR_ARG;
105
+ }
106
+
107
+ /** Runs one exact child and publishes authority only for a directly observed Linux signal 6. */
108
+ export async function runManagedOwnerSupervisor(): Promise<void> {
109
+ const { root, generation, sessionId, runId, incarnation } = lifecycleRoot();
110
+ const command = childCommand();
111
+ const supervisorStartTime = await readLinuxProcStartTime(process.pid);
112
+ if (!supervisorStartTime) throw new Error("managed_owner_supervisor_start_time_unavailable");
113
+ await fs.mkdir(root, { recursive: true, mode: 0o700 });
114
+ const childToken = crypto.randomUUID();
115
+ const binding: ManagedOwnerBinding = {
116
+ schema_version: 2,
117
+ generation,
118
+ session_id: sessionId,
119
+ run_id: runId,
120
+ endpoint_incarnation: incarnation,
121
+ child_token: childToken,
122
+ command,
123
+ command_sha256: commandDigest(command),
124
+ supervisor_pid: process.pid,
125
+ supervisor_start_time: supervisorStartTime,
126
+ created_at: new Date().toISOString(),
127
+ };
128
+ await writeDurableExclusive(path.join(root, `child-${childToken}.binding.json`), binding);
129
+ const child = Bun.spawn({
130
+ cmd: command,
131
+ stdin: "inherit",
132
+ stdout: "inherit",
133
+ stderr: "inherit",
134
+ env: { ...process.env, [MANAGED_OWNER_CHILD_TOKEN_ENV]: childToken },
135
+ });
136
+ const childStartTime = await readLinuxProcStartTime(child.pid);
137
+ if (!childStartTime) throw new Error("managed_owner_child_start_time_unavailable");
138
+ const exitCode = await child.exited;
139
+ if (child.signalCode === "SIGABRT") {
140
+ const receipt: ManagedOwnerSigabrtReceipt = {
141
+ schema_version: 2,
142
+ generation,
143
+ session_id: sessionId,
144
+ run_id: runId,
145
+ endpoint_incarnation: incarnation,
146
+ child_token: childToken,
147
+ command_sha256: binding.command_sha256,
148
+ supervisor_pid: process.pid,
149
+ supervisor_start_time: supervisorStartTime,
150
+ child_pid: child.pid,
151
+ child_start_time: childStartTime,
152
+ signal: "SIGABRT",
153
+ signal_number: 6,
154
+ exit_code: exitCode,
155
+ received_at: new Date().toISOString(),
156
+ };
157
+ await writeDurableExclusive(path.join(root, `sigabrt-${childToken}.receipt.json`), receipt);
158
+ process.exitCode = 134;
159
+ return;
160
+ }
161
+ process.exitCode = exitCode;
162
+ }
@@ -29,9 +29,13 @@ import {
29
29
  import { renderCliWriteReceipt } from "./cli-write-receipt";
30
30
  import { applyAmbiguityFloorToEnvelope } from "./deep-interview-ambiguity";
31
31
  import {
32
+ assertDeepInterviewEnvelopeInputLimits,
33
+ assertDeepInterviewInputWithinLimit,
32
34
  assertDeepInterviewIntentManifest,
33
35
  assertDeepInterviewIntentReview,
36
+ assertDeepInterviewStructuredResponseWithinLimit,
34
37
  type DeepInterviewIntentManifest,
38
+ MAX_DEEP_INTERVIEW_STRUCTURED_RESPONSE_LENGTH,
35
39
  mergeDeepInterviewEnvelope,
36
40
  normalizeDeepInterviewEnvelope,
37
41
  } from "./deep-interview-state";
@@ -992,6 +996,7 @@ async function reconcileWorkflowSkillStateUnlocked(
992
996
  ): Promise<{ stateFile: string }> {
993
997
  const { cwd, mode, threadId, turnId, active, payload } = options;
994
998
  const filePath = modeStateFile(cwd, mode, sessionId);
999
+ if (mode === "deep-interview") assertDeepInterviewStructuredResponseWithinLimit(payload);
995
1000
  const existingRead = await readExistingStateForMutation(filePath);
996
1001
  const existingPayload = existingRead.kind === "valid" ? existingRead.value : {};
997
1002
  const nowIsoStr = nowIso();
@@ -1028,6 +1033,7 @@ async function reconcileWorkflowSkillStateUnlocked(
1028
1033
  unknown
1029
1034
  >)
1030
1035
  : mergeWithNullDelete(existingPayload, payload);
1036
+ if (mode === "deep-interview") assertDeepInterviewEnvelopeInputLimits(merged);
1031
1037
  merged.skill = mode;
1032
1038
  merged.current_phase = trimmedPhase;
1033
1039
  merged.active = active;
@@ -1177,6 +1183,13 @@ async function handleWrite(args: readonly string[], cwd: string): Promise<StateC
1177
1183
  "gjc state write requires --mode <skill>, positional <skill>, input.skill, or an active workflow in the current session active state",
1178
1184
  );
1179
1185
 
1186
+ if (mode === "deep-interview") {
1187
+ try {
1188
+ assertDeepInterviewStructuredResponseWithinLimit(payload);
1189
+ } catch (error) {
1190
+ throw new StateCommandError(2, error instanceof Error ? error.message : String(error));
1191
+ }
1192
+ }
1180
1193
  const filePath = modeStateFile(cwd, mode, sessionId);
1181
1194
  const forced = hasFlag(args, "--force");
1182
1195
  return await withWorkflowStateLock(
@@ -1219,6 +1232,11 @@ async function handleWrite(args: readonly string[], cwd: string): Promise<StateC
1219
1232
  merged = applyAmbiguityFloorToEnvelope(
1220
1233
  mergeDeepInterviewEnvelope(existingPayload, payload, { replace: hasFlag(args, "--replace") }),
1221
1234
  ).envelope;
1235
+ try {
1236
+ assertDeepInterviewEnvelopeInputLimits(merged);
1237
+ } catch (error) {
1238
+ throw new StateCommandError(2, error instanceof Error ? error.message : String(error));
1239
+ }
1222
1240
  } else if (hasFlag(args, "--replace")) {
1223
1241
  merged = { ...payload };
1224
1242
  } else {
@@ -1419,6 +1437,25 @@ async function handleClear(args: readonly string[], cwd: string): Promise<StateC
1419
1437
  const DEEP_INTERVIEW_INTENT_ID_RE = /(?:artifact|surface|integration|constraint):[a-z0-9][a-z0-9._/-]{0,127}/g;
1420
1438
 
1421
1439
  async function assertDeepInterviewHandoffReady(state: Record<string, unknown>): Promise<void> {
1440
+ const specPath = typeof state.spec_path === "string" ? state.spec_path : undefined;
1441
+ const expectedSha = typeof state.spec_sha256 === "string" ? state.spec_sha256 : undefined;
1442
+ let content: string | undefined;
1443
+ if (specPath) {
1444
+ try {
1445
+ content = await fs.readFile(specPath, "utf-8");
1446
+ } catch (error) {
1447
+ throw new StateCommandError(
1448
+ 2,
1449
+ `deep-interview handoff cannot read persisted spec: ${error instanceof Error ? error.message : String(error)}`,
1450
+ );
1451
+ }
1452
+ const boundedContent = content.endsWith("\n") ? content.slice(0, -1) : content;
1453
+ assertDeepInterviewInputWithinLimit(
1454
+ boundedContent,
1455
+ MAX_DEEP_INTERVIEW_STRUCTURED_RESPONSE_LENGTH,
1456
+ "persisted deep-interview spec",
1457
+ );
1458
+ }
1422
1459
  const envelope = normalizeDeepInterviewEnvelope(state);
1423
1460
  const inner = envelope.state;
1424
1461
  if (!inner) return;
@@ -1428,19 +1465,8 @@ async function assertDeepInterviewHandoffReady(state: Record<string, unknown>):
1428
1465
  return;
1429
1466
  }
1430
1467
  assertDeepInterviewIntentManifest(inner.intent_contract);
1431
- const specPath = typeof state.spec_path === "string" ? state.spec_path : undefined;
1432
- const expectedSha = typeof state.spec_sha256 === "string" ? state.spec_sha256 : undefined;
1433
- if (!specPath || !expectedSha)
1468
+ if (!specPath || !expectedSha || content === undefined)
1434
1469
  throw new StateCommandError(2, "deep-interview handoff requires a persisted intent-validated spec");
1435
- let content: string;
1436
- try {
1437
- content = await fs.readFile(specPath, "utf-8");
1438
- } catch (error) {
1439
- throw new StateCommandError(
1440
- 2,
1441
- `deep-interview handoff cannot read persisted spec: ${error instanceof Error ? error.message : String(error)}`,
1442
- );
1443
- }
1444
1470
  if (createHash("sha256").update(content).digest("hex") !== expectedSha)
1445
1471
  throw new StateCommandError(2, "deep-interview handoff spec hash mismatch");
1446
1472
  const observedIds = [...new Set(content.match(DEEP_INTERVIEW_INTENT_ID_RE) ?? [])].sort();
@@ -1885,7 +1885,7 @@ async function startTmuxSession(
1885
1885
  for (const worker of config.workers) {
1886
1886
  const splitDirection: string = worker.index === 1 ? "-h" : "-v";
1887
1887
  const splitTarget: string =
1888
- worker.index === 1 ? config.leader.pane_id : (rightStackRootPaneId ?? config.leader.pane_id);
1888
+ worker.index === 1 ? config.tmux_target : (rightStackRootPaneId ?? config.tmux_target);
1889
1889
  const workerCommand = buildWorkerCommand(config, worker);
1890
1890
  const workerCwd = worker.worktree_path ?? config.leader.cwd;
1891
1891
  const useSendKeysFallback = shouldDispatchWorkerWithSendKeys(config.tmux_command);