@gajae-code/coding-agent 0.11.4 → 0.11.6
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.
- package/CHANGELOG.md +23 -0
- package/dist/types/cli/notify-cli.d.ts +5 -1
- package/dist/types/config/settings-schema.d.ts +31 -1
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +28 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -0
- package/dist/types/gjc-runtime/managed-owner-admission.d.ts +42 -0
- package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +41 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +9 -0
- package/dist/types/gjc-runtime/ultragoal-owner-loss-recovery.d.ts +94 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
- package/dist/types/harness-control-plane/owner.d.ts +3 -1
- package/dist/types/internal-urls/local-protocol.d.ts +18 -0
- package/dist/types/main.d.ts +2 -2
- package/dist/types/modes/components/ansi-display-validator.d.ts +2 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +4 -0
- package/dist/types/modes/components/session-observer-overlay.d.ts +3 -1
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-transcript-format.d.ts +58 -0
- package/dist/types/modes/components/transcript-viewer-overlay.d.ts +3 -0
- package/dist/types/modes/controllers/goal-mode-controller.d.ts +8 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +15 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +3 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +16 -0
- package/dist/types/plan-mode/approved-plan.d.ts +1 -0
- package/dist/types/runtime-mcp/manager.d.ts +2 -0
- package/dist/types/runtime-mcp/oauth-discovery.d.ts +12 -1
- package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +42 -4
- package/dist/types/sdk/bus/config-commands.d.ts +8 -0
- package/dist/types/sdk/bus/config.d.ts +22 -0
- package/dist/types/sdk/bus/index.d.ts +162 -2
- package/dist/types/sdk/bus/notification-service.d.ts +71 -8
- package/dist/types/sdk/bus/session-control.d.ts +11 -8
- package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +5 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -3
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +23 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +214 -20
- package/dist/types/sdk/session.d.ts +16 -1
- package/dist/types/secrets/index.d.ts +4 -2
- package/dist/types/secrets/obfuscator.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +31 -2
- package/dist/types/session/artifacts.d.ts +30 -8
- package/dist/types/session/internal/managed-session-scope.d.ts +7 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +63 -6
- package/dist/types/session/session-manager.d.ts +98 -18
- package/dist/types/session/session-storage.d.ts +27 -0
- package/dist/types/session/streaming-output.d.ts +4 -0
- package/dist/types/session/yield-queue.d.ts +8 -0
- package/dist/types/task/executor.d.ts +9 -0
- package/dist/types/tool-discovery/tool-index.d.ts +2 -0
- package/dist/types/tools/ask.d.ts +48 -0
- package/dist/types/tools/bash-skill-urls.d.ts +2 -2
- package/dist/types/tools/index.d.ts +4 -0
- package/package.json +9 -9
- package/scripts/generate-sdk-operation-inventory.ts +3 -0
- package/src/cli/notify-cli.ts +40 -18
- package/src/commands/sdk.ts +7 -2
- package/src/config/settings-schema.ts +31 -1
- package/src/daemon/runtime.ts +2 -2
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +70 -49
- package/src/defaults/gjc/skills/ralplan/SKILL.md +0 -6
- package/src/defaults/gjc/skills/team/SKILL.md +7 -9
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +0 -2
- package/src/eval/py/index.ts +2 -2
- package/src/export/html/template.generated.ts +1 -1
- package/src/extensibility/plugins/marketplace/manager.ts +94 -10
- package/src/gjc-runtime/deep-interview-recorder.ts +7 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +10 -0
- package/src/gjc-runtime/deep-interview-state.ts +127 -3
- package/src/gjc-runtime/launch-tmux.ts +61 -6
- package/src/gjc-runtime/managed-owner-admission.ts +352 -0
- package/src/gjc-runtime/managed-owner-supervisor.ts +162 -0
- package/src/gjc-runtime/state-runtime.ts +38 -12
- package/src/gjc-runtime/team-runtime.ts +1 -1
- package/src/gjc-runtime/tmux-owner-isolation.ts +117 -0
- package/src/gjc-runtime/ultragoal-owner-loss-recovery.ts +328 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +14 -0
- package/src/harness-control-plane/owner.ts +59 -3
- package/src/hindsight/client.ts +65 -2
- package/src/internal-urls/agent-protocol.ts +76 -5
- package/src/internal-urls/docs-index.generated.ts +8 -7
- package/src/internal-urls/local-protocol.ts +416 -18
- package/src/lsp/index.ts +4 -88
- package/src/main.ts +36 -12
- package/src/modes/acp/acp-agent.ts +8 -0
- package/src/modes/components/ansi-display-validator.ts +54 -0
- package/src/modes/components/notifications-settings-editor.ts +26 -2
- package/src/modes/components/session-observer-overlay.ts +40 -24
- package/src/modes/components/settings-selector.ts +3 -1
- package/src/modes/components/tool-transcript-format.ts +321 -0
- package/src/modes/components/transcript-viewer-overlay.ts +39 -17
- package/src/modes/controllers/command-controller.ts +15 -0
- package/src/modes/controllers/extension-ui-controller.ts +14 -2
- package/src/modes/controllers/goal-mode-controller.ts +20 -1
- package/src/modes/controllers/plan-mode-controller.ts +2 -0
- package/src/modes/controllers/runtime-mcp-command-controller.ts +3 -0
- package/src/modes/controllers/selector-controller.ts +70 -8
- package/src/modes/interactive-mode.ts +1 -0
- package/src/modes/runtime-init.ts +16 -2
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -1
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +17 -1
- package/src/modes/shared/agent-wire/workflow-gate-schema.ts +4 -2
- package/src/modes/shared/agent-wire/workflow-gate-types.ts +120 -3
- package/src/plan-mode/approved-plan.ts +3 -1
- package/src/prompts/agents/architect.md +1 -0
- package/src/prompts/agents/critic.md +1 -0
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/system-prompt.md +2 -1
- package/src/prompts/tools/lsp.md +3 -3
- package/src/runtime-mcp/manager.ts +16 -1
- package/src/runtime-mcp/oauth-discovery.ts +116 -26
- package/src/sdk/broker/session-index.ts +122 -64
- package/src/sdk/bus/chat-daemon-cli.ts +72 -10
- package/src/sdk/bus/chat-daemon-control.ts +538 -130
- package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
- package/src/sdk/bus/config-commands.ts +22 -0
- package/src/sdk/bus/config.ts +37 -0
- package/src/sdk/bus/index.ts +388 -94
- package/src/sdk/bus/lifecycle-control-runtime.ts +53 -3
- package/src/sdk/bus/notification-service.ts +284 -62
- package/src/sdk/bus/recent-activity.ts +14 -5
- package/src/sdk/bus/session-control.ts +150 -44
- package/src/sdk/bus/telegram-daemon-cli.ts +20 -14
- package/src/sdk/bus/telegram-daemon-contract.ts +11 -3
- package/src/sdk/bus/telegram-daemon-control.ts +312 -76
- package/src/sdk/bus/telegram-daemon.ts +1916 -285
- package/src/sdk/host/control/dispatch.ts +14 -5
- package/src/sdk/protocol/operation-inventory.generated.json +29 -0
- package/src/sdk/session.ts +155 -27
- package/src/secrets/index.ts +53 -10
- package/src/secrets/obfuscator.ts +21 -18
- package/src/secrets/regex.ts +2 -0
- package/src/session/agent-session.ts +1447 -852
- package/src/session/artifacts.ts +188 -23
- package/src/session/context-estimation.ts +7 -2
- package/src/session/internal/managed-session-scope.ts +200 -38
- package/src/session/internal/managed-session-storage.ts +1152 -46
- package/src/session/session-manager.ts +1121 -148
- package/src/session/session-storage.ts +109 -11
- package/src/session/streaming-output.ts +6 -0
- package/src/session/yield-queue.ts +20 -0
- package/src/slash-commands/builtin-registry.ts +36 -0
- package/src/task/executor.ts +71 -26
- package/src/task/index.ts +63 -20
- package/src/tool-discovery/tool-index.ts +18 -0
- package/src/tools/ask-answer-registry.ts +15 -1
- package/src/tools/ask.ts +129 -32
- package/src/tools/bash-skill-urls.ts +14 -3
- package/src/tools/bash.ts +2 -2
- package/src/tools/index.ts +4 -0
- package/src/tools/plan-mode-guard.ts +1 -0
- package/src/tools/sqlite-reader.ts +3 -2
- 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
|
-
|
|
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.
|
|
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);
|