@gajae-code/coding-agent 0.7.10 → 0.8.0
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 +58 -0
- package/README.md +2 -2
- package/dist/types/capability/mcp.d.ts +2 -0
- package/dist/types/config/keybindings.d.ts +11 -5
- package/dist/types/config/settings-schema.d.ts +25 -2
- package/dist/types/defaults/gjc-defaults.d.ts +7 -0
- package/dist/types/discovery/cursor.d.ts +4 -1
- package/dist/types/discovery/index.d.ts +0 -1
- package/dist/types/discovery/windsurf.d.ts +3 -1
- package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +3 -2
- package/dist/types/gjc-runtime/restricted-role-agent-bash.d.ts +1 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +5 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +1 -0
- package/dist/types/modes/components/custom-editor.d.ts +6 -1
- package/dist/types/modes/components/settings-selector.d.ts +1 -0
- package/dist/types/modes/components/skill-message.d.ts +1 -0
- package/dist/types/modes/components/status-line.d.ts +8 -0
- package/dist/types/modes/components/two-column-body.d.ts +9 -0
- package/dist/types/modes/components/welcome.d.ts +9 -1
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/input-controller.d.ts +5 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/prompt-action-autocomplete.d.ts +2 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +1 -0
- package/dist/types/modes/slash-command-visibility.d.ts +1 -0
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/runtime-mcp/config-writer.d.ts +9 -0
- package/dist/types/runtime-mcp/config.d.ts +2 -0
- package/dist/types/runtime-mcp/loader.d.ts +2 -0
- package/dist/types/runtime-mcp/manager.d.ts +2 -0
- package/dist/types/runtime-mcp/types.d.ts +6 -0
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +8 -5
- package/dist/types/slash-commands/types.d.ts +2 -0
- package/dist/types/system-prompt.d.ts +7 -0
- package/dist/types/utils/cmux-workspace.d.ts +30 -1
- package/dist/types/web/search/index.d.ts +3 -1
- package/dist/types/web/search/provider.d.ts +9 -0
- package/dist/types/web/search/providers/utils.d.ts +42 -13
- package/dist/types/workflow/workflow-intent-diff.d.ts +19 -0
- package/package.json +10 -10
- package/src/capability/mcp.ts +2 -0
- package/src/cli/update-cli.ts +24 -0
- package/src/cli/web-search-cli.ts +2 -5
- package/src/cli.ts +1 -1
- package/src/commands/launch.ts +1 -1
- package/src/commands/ralplan.ts +1 -0
- package/src/config/keybindings.ts +14 -5
- package/src/config/mcp-schema.json +5 -1
- package/src/config/model-profiles.ts +16 -0
- package/src/config/settings-schema.ts +18 -2
- package/src/coordinator-mcp/server.ts +15 -3
- package/src/defaults/gjc/skills/ralplan/SKILL.md +16 -14
- package/src/defaults/gjc-defaults.ts +16 -0
- package/src/discovery/builtin.ts +12 -0
- package/src/discovery/cursor.ts +5 -84
- package/src/discovery/gemini.ts +3 -89
- package/src/discovery/index.ts +4 -1
- package/src/discovery/mcp-json.ts +11 -0
- package/src/discovery/opencode.ts +5 -122
- package/src/discovery/windsurf.ts +4 -82
- package/src/edit/read-file.ts +9 -1
- package/src/extensibility/extensions/runner.ts +2 -1
- package/src/gjc-runtime/launch-tmux.ts +164 -42
- package/src/gjc-runtime/ralplan-runtime.ts +22 -6
- package/src/gjc-runtime/restricted-role-agent-bash.ts +1 -0
- package/src/gjc-runtime/session-state-sidecar.ts +167 -24
- package/src/gjc-runtime/state-runtime.ts +8 -5
- package/src/gjc-runtime/state-writer.ts +5 -5
- package/src/gjc-runtime/team-runtime.ts +56 -51
- package/src/internal-urls/docs-index.generated.ts +8 -7
- package/src/main.ts +2 -2
- package/src/migrate/executor.ts +23 -6
- package/src/modes/components/agent-dashboard.ts +1 -34
- package/src/modes/components/custom-editor.ts +26 -0
- package/src/modes/components/extensions/extension-dashboard.ts +2 -48
- package/src/modes/components/model-selector.ts +64 -4
- package/src/modes/components/session-selector.ts +19 -9
- package/src/modes/components/settings-selector.ts +13 -2
- package/src/modes/components/skill-message.ts +18 -10
- package/src/modes/components/status-line.ts +215 -86
- package/src/modes/components/tree-selector.ts +28 -8
- package/src/modes/components/two-column-body.ts +36 -0
- package/src/modes/components/welcome.ts +171 -35
- package/src/modes/controllers/command-controller.ts +23 -0
- package/src/modes/controllers/event-controller.ts +4 -0
- package/src/modes/controllers/input-controller.ts +192 -39
- package/src/modes/controllers/selector-controller.ts +25 -28
- package/src/modes/interactive-mode.ts +36 -30
- package/src/modes/prompt-action-autocomplete.ts +23 -1
- package/src/modes/rpc/rpc-types.ts +1 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +9 -3
- package/src/modes/shared/agent-wire/workflow-gate-schema.ts +20 -0
- package/src/modes/slash-command-visibility.ts +9 -0
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/hotkeys-markdown.ts +5 -2
- package/src/modes/utils/ui-helpers.ts +76 -3
- package/src/notifications/index.ts +20 -1
- package/src/notifications/lifecycle-control-runtime.ts +12 -3
- package/src/notifications/telegram-daemon.ts +4 -4
- package/src/prompts/agents/architect.md +7 -4
- package/src/prompts/agents/critic.md +15 -6
- package/src/prompts/agents/planner.md +8 -3
- package/src/prompts/system/system-prompt.md +15 -1
- package/src/prompts/tools/bash.md +1 -1
- package/src/runtime-mcp/config-writer.ts +28 -0
- package/src/runtime-mcp/config.ts +7 -0
- package/src/runtime-mcp/loader.ts +3 -0
- package/src/runtime-mcp/manager.ts +3 -0
- package/src/runtime-mcp/types.ts +6 -0
- package/src/sdk.ts +7 -6
- package/src/session/agent-session.ts +81 -18
- package/src/session/session-manager.ts +58 -18
- package/src/slash-commands/builtin-registry.ts +30 -10
- package/src/slash-commands/types.ts +2 -0
- package/src/system-prompt.ts +8 -0
- package/src/task/executor.ts +5 -0
- package/src/tools/ast-grep.ts +10 -7
- package/src/tools/bash.ts +39 -14
- package/src/tools/index.ts +8 -1
- package/src/utils/cmux-workspace.ts +134 -7
- package/src/utils/title-generator.ts +1 -1
- package/src/web/kagi.ts +1 -1
- package/src/web/parallel.ts +2 -2
- package/src/web/search/index.ts +118 -45
- package/src/web/search/provider.ts +65 -0
- package/src/web/search/providers/anthropic.ts +1 -1
- package/src/web/search/providers/brave.ts +1 -1
- package/src/web/search/providers/codex.ts +1 -1
- package/src/web/search/providers/duckduckgo.ts +1 -1
- package/src/web/search/providers/exa.ts +1 -1
- package/src/web/search/providers/gemini.ts +7 -3
- package/src/web/search/providers/jina.ts +1 -1
- package/src/web/search/providers/kimi.ts +6 -1
- package/src/web/search/providers/openai-compatible.ts +1 -1
- package/src/web/search/providers/parallel.ts +1 -1
- package/src/web/search/providers/perplexity.ts +2 -2
- package/src/web/search/providers/searxng.ts +1 -1
- package/src/web/search/providers/synthetic.ts +1 -1
- package/src/web/search/providers/tavily.ts +1 -1
- package/src/web/search/providers/utils.ts +67 -17
- package/src/web/search/providers/xai.ts +1 -1
- package/src/web/search/providers/zai.ts +1 -1
- package/src/workflow/workflow-intent-diff.ts +199 -0
- package/dist/types/discovery/vscode.d.ts +0 -1
- package/src/discovery/vscode.ts +0 -105
|
@@ -1,13 +1,18 @@
|
|
|
1
|
+
import * as fsSync from "node:fs";
|
|
1
2
|
import * as fs from "node:fs/promises";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import type { AssistantMessage } from "@gajae-code/ai";
|
|
4
|
-
import { logger } from "@gajae-code/utils";
|
|
5
|
+
import { logger, postmortem } from "@gajae-code/utils";
|
|
5
6
|
|
|
6
7
|
export const GJC_COORDINATOR_SESSION_STATE_FILE_ENV = "GJC_COORDINATOR_SESSION_STATE_FILE";
|
|
7
8
|
export const GJC_COORDINATOR_SESSION_ID_ENV = "GJC_COORDINATOR_SESSION_ID";
|
|
9
|
+
export const GJC_COORDINATOR_SESSION_BRANCH_ENV = "GJC_COORDINATOR_SESSION_BRANCH";
|
|
8
10
|
|
|
9
11
|
export type RuntimeState = "ready_for_input" | "running" | "needs_user_input" | "completed" | "errored";
|
|
10
12
|
|
|
13
|
+
type FinalResponseSource = "agent_end" | "launch_error";
|
|
14
|
+
const MAX_PUBLIC_ERROR_MESSAGE_LENGTH = 2000;
|
|
15
|
+
|
|
11
16
|
interface RuntimeStateEvent {
|
|
12
17
|
type: string;
|
|
13
18
|
messages?: unknown[];
|
|
@@ -17,6 +22,7 @@ interface RuntimeStateContext {
|
|
|
17
22
|
sessionId: string;
|
|
18
23
|
cwd: string;
|
|
19
24
|
sessionFile?: string | null;
|
|
25
|
+
branch?: string | null;
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
interface RuntimeStateSidecarPayload {
|
|
@@ -26,6 +32,7 @@ interface RuntimeStateSidecarPayload {
|
|
|
26
32
|
ready_for_input?: unknown;
|
|
27
33
|
cwd?: unknown;
|
|
28
34
|
session_file?: unknown;
|
|
35
|
+
final_response?: { source?: unknown };
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
export type TerminalRuntimeStateStatus =
|
|
@@ -103,7 +110,7 @@ function assistantText(assistant: AssistantMessage | undefined): string | null {
|
|
|
103
110
|
function finalResponseForEvent(event: RuntimeStateEvent): {
|
|
104
111
|
text: string | null;
|
|
105
112
|
format: "markdown";
|
|
106
|
-
source:
|
|
113
|
+
source: FinalResponseSource;
|
|
107
114
|
artifact_path: null;
|
|
108
115
|
truncated: false;
|
|
109
116
|
} | null {
|
|
@@ -127,6 +134,118 @@ function stateForEvent(event: RuntimeStateEvent): RuntimeState | null {
|
|
|
127
134
|
return null;
|
|
128
135
|
}
|
|
129
136
|
|
|
137
|
+
function readPreviousPayload(stateFile: string): Record<string, unknown> {
|
|
138
|
+
try {
|
|
139
|
+
return JSON.parse(fsSync.readFileSync(stateFile, "utf8")) as Record<string, unknown>;
|
|
140
|
+
} catch {
|
|
141
|
+
return {};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function shouldPreserveTerminalPayload(previous: RuntimeStateSidecarPayload): boolean {
|
|
146
|
+
if (previous.state !== "completed" && previous.state !== "errored") return false;
|
|
147
|
+
const source = previous.final_response?.source;
|
|
148
|
+
return source === "agent_end" || source === "launch_error";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function branchForContext(context: RuntimeStateContext): string | null {
|
|
152
|
+
return context.branch ?? (process.env[GJC_COORDINATOR_SESSION_BRANCH_ENV]?.trim() || null);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function basePayload(input: {
|
|
156
|
+
context: RuntimeStateContext;
|
|
157
|
+
previous: Record<string, unknown>;
|
|
158
|
+
state: RuntimeState;
|
|
159
|
+
now: string;
|
|
160
|
+
source: string;
|
|
161
|
+
event: string;
|
|
162
|
+
reason: string | null;
|
|
163
|
+
}): Record<string, unknown> {
|
|
164
|
+
return {
|
|
165
|
+
schema_version: 1,
|
|
166
|
+
session_id: process.env[GJC_COORDINATOR_SESSION_ID_ENV]?.trim() || input.context.sessionId,
|
|
167
|
+
state: input.state,
|
|
168
|
+
ready_for_input: input.state === "completed" || input.state === "ready_for_input",
|
|
169
|
+
updated_at: input.now,
|
|
170
|
+
current_turn_id: typeof input.previous.current_turn_id === "string" ? input.previous.current_turn_id : null,
|
|
171
|
+
last_turn_id: typeof input.previous.last_turn_id === "string" ? input.previous.last_turn_id : null,
|
|
172
|
+
live: input.state === "running",
|
|
173
|
+
reason: input.reason,
|
|
174
|
+
source: input.source,
|
|
175
|
+
event: input.event,
|
|
176
|
+
cwd: input.context.cwd,
|
|
177
|
+
workdir: input.context.cwd,
|
|
178
|
+
branch: branchForContext(input.context),
|
|
179
|
+
session_file: input.context.sessionFile ?? null,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function publicSafeErrorMessage(message: string): string {
|
|
183
|
+
const normalized = message.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ").trim();
|
|
184
|
+
if (normalized.length <= MAX_PUBLIC_ERROR_MESSAGE_LENGTH) return normalized;
|
|
185
|
+
return `${normalized.slice(0, MAX_PUBLIC_ERROR_MESSAGE_LENGTH)}…`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function errorMessageForPostmortem(reason: postmortem.Reason): string {
|
|
189
|
+
return publicSafeErrorMessage(`GJC process cleanup ran for ${reason}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function numericProcessExitCode(defaultCode: number | null): number | null {
|
|
193
|
+
return typeof process.exitCode === "number" ? process.exitCode : defaultCode;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function postmortemExitDetails(reason: postmortem.Reason): {
|
|
197
|
+
state: RuntimeState;
|
|
198
|
+
reason: string;
|
|
199
|
+
exitKind: string;
|
|
200
|
+
exitCode: number | null;
|
|
201
|
+
signal: string | null;
|
|
202
|
+
error?: { code: string; message: string; recoverable: true };
|
|
203
|
+
} {
|
|
204
|
+
if (reason === postmortem.Reason.EXIT || reason === postmortem.Reason.MANUAL) {
|
|
205
|
+
const exitCode = numericProcessExitCode(0) ?? 0;
|
|
206
|
+
const state: RuntimeState = exitCode === 0 ? "completed" : "errored";
|
|
207
|
+
return {
|
|
208
|
+
state,
|
|
209
|
+
reason: reason === postmortem.Reason.EXIT ? "process_exit" : "manual_cleanup",
|
|
210
|
+
exitKind: reason,
|
|
211
|
+
exitCode,
|
|
212
|
+
signal: null,
|
|
213
|
+
...(state === "errored"
|
|
214
|
+
? {
|
|
215
|
+
error: {
|
|
216
|
+
code: "process_exit",
|
|
217
|
+
message: publicSafeErrorMessage(`GJC process exited with code ${exitCode}`),
|
|
218
|
+
recoverable: true,
|
|
219
|
+
},
|
|
220
|
+
}
|
|
221
|
+
: {}),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
const signalByReason: Partial<Record<postmortem.Reason, string>> = {
|
|
225
|
+
[postmortem.Reason.SIGINT]: "SIGINT",
|
|
226
|
+
[postmortem.Reason.SIGTERM]: "SIGTERM",
|
|
227
|
+
[postmortem.Reason.SIGHUP]: "SIGHUP",
|
|
228
|
+
};
|
|
229
|
+
return {
|
|
230
|
+
state: "errored",
|
|
231
|
+
reason,
|
|
232
|
+
exitKind: reason,
|
|
233
|
+
exitCode: numericProcessExitCode(null),
|
|
234
|
+
signal: signalByReason[reason] ?? null,
|
|
235
|
+
error: { code: reason, message: errorMessageForPostmortem(reason), recoverable: true },
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function writeStateFileSync(stateFile: string, payload: Record<string, unknown>): void {
|
|
240
|
+
fsSync.mkdirSync(path.dirname(stateFile), { recursive: true });
|
|
241
|
+
fsSync.writeFileSync(stateFile, `${JSON.stringify(payload, null, 2)}\n`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function writeStateFile(stateFile: string, payload: Record<string, unknown>): Promise<void> {
|
|
245
|
+
await fs.mkdir(path.dirname(stateFile), { recursive: true });
|
|
246
|
+
await Bun.write(stateFile, `${JSON.stringify(payload, null, 2)}\n`);
|
|
247
|
+
}
|
|
248
|
+
|
|
130
249
|
export async function persistCoordinatorRuntimeStateFromEvent(
|
|
131
250
|
event: RuntimeStateEvent,
|
|
132
251
|
context: RuntimeStateContext,
|
|
@@ -136,42 +255,66 @@ export async function persistCoordinatorRuntimeStateFromEvent(
|
|
|
136
255
|
const state = stateForEvent(event);
|
|
137
256
|
if (!state) return;
|
|
138
257
|
const now = new Date().toISOString();
|
|
139
|
-
|
|
140
|
-
try {
|
|
141
|
-
previous = JSON.parse(await Bun.file(stateFile).text()) as Record<string, unknown>;
|
|
142
|
-
} catch {
|
|
143
|
-
previous = {};
|
|
144
|
-
}
|
|
258
|
+
const previous = readPreviousPayload(stateFile);
|
|
145
259
|
const finalResponse = finalResponseForEvent(event);
|
|
146
260
|
const payload = {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
state,
|
|
150
|
-
ready_for_input: state === "completed" || state === "ready_for_input",
|
|
151
|
-
updated_at: now,
|
|
152
|
-
current_turn_id: typeof previous.current_turn_id === "string" ? previous.current_turn_id : null,
|
|
153
|
-
last_turn_id: typeof previous.last_turn_id === "string" ? previous.last_turn_id : null,
|
|
154
|
-
live: typeof previous.live === "boolean" ? previous.live : null,
|
|
155
|
-
reason: null,
|
|
156
|
-
source: "agent_session_event",
|
|
157
|
-
event: event.type,
|
|
158
|
-
cwd: context.cwd,
|
|
159
|
-
session_file: context.sessionFile ?? null,
|
|
261
|
+
...basePayload({ context, previous, state, now, source: "agent_session_event", event: event.type, reason: null }),
|
|
262
|
+
...(state === "completed" || state === "errored" ? { ended_at: now } : {}),
|
|
160
263
|
...(finalResponse ? { final_response: finalResponse } : {}),
|
|
161
264
|
...(state === "errored"
|
|
162
265
|
? {
|
|
163
266
|
error: {
|
|
164
267
|
code: "agent_error",
|
|
165
|
-
message: lastAssistant(event.messages)?.errorMessage ?? "agent_error",
|
|
268
|
+
message: publicSafeErrorMessage(lastAssistant(event.messages)?.errorMessage ?? "agent_error"),
|
|
166
269
|
recoverable: true,
|
|
167
270
|
},
|
|
168
271
|
}
|
|
169
272
|
: {}),
|
|
170
273
|
};
|
|
171
274
|
try {
|
|
172
|
-
await
|
|
173
|
-
await Bun.write(stateFile, `${JSON.stringify(payload, null, 2)}\n`);
|
|
275
|
+
await writeStateFile(stateFile, payload);
|
|
174
276
|
} catch (error) {
|
|
175
277
|
logger.warn("Failed to persist coordinator runtime state", { error: String(error), stateFile });
|
|
176
278
|
}
|
|
177
279
|
}
|
|
280
|
+
|
|
281
|
+
export function persistCoordinatorRuntimeStateFromPostmortem(
|
|
282
|
+
reason: postmortem.Reason,
|
|
283
|
+
context: RuntimeStateContext,
|
|
284
|
+
): void {
|
|
285
|
+
const stateFile = process.env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim();
|
|
286
|
+
if (!stateFile) return;
|
|
287
|
+
const previous = readPreviousPayload(stateFile);
|
|
288
|
+
if (shouldPreserveTerminalPayload(previous as RuntimeStateSidecarPayload)) return;
|
|
289
|
+
const now = new Date().toISOString();
|
|
290
|
+
const details = postmortemExitDetails(reason);
|
|
291
|
+
const payload = {
|
|
292
|
+
...basePayload({
|
|
293
|
+
context,
|
|
294
|
+
previous,
|
|
295
|
+
state: details.state,
|
|
296
|
+
now,
|
|
297
|
+
source: "process_postmortem",
|
|
298
|
+
event: "process_exit",
|
|
299
|
+
reason: details.reason,
|
|
300
|
+
}),
|
|
301
|
+
ended_at: now,
|
|
302
|
+
detected_at: now,
|
|
303
|
+
exit_kind: details.exitKind,
|
|
304
|
+
exit_code: details.exitCode,
|
|
305
|
+
signal: details.signal,
|
|
306
|
+
...(details.error ? { error: details.error } : {}),
|
|
307
|
+
};
|
|
308
|
+
try {
|
|
309
|
+
writeStateFileSync(stateFile, payload);
|
|
310
|
+
} catch (error) {
|
|
311
|
+
logger.warn("Failed to persist coordinator runtime state during postmortem", { error: String(error), stateFile });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function registerCoordinatorRuntimeStateFinalizer(context: RuntimeStateContext): () => void {
|
|
316
|
+
if (!process.env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim()) return () => {};
|
|
317
|
+
return postmortem.register("coordinator-runtime-state", reason => {
|
|
318
|
+
persistCoordinatorRuntimeStateFromPostmortem(reason, context);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
@@ -847,11 +847,14 @@ async function writeJsonAtomic(
|
|
|
847
847
|
forced: options?.force ?? false,
|
|
848
848
|
},
|
|
849
849
|
});
|
|
850
|
-
// `writeResult.revision`
|
|
851
|
-
// write actually owns.
|
|
852
|
-
// could
|
|
853
|
-
//
|
|
854
|
-
|
|
850
|
+
// `writeResult.stamped` and `.revision` are computed inside the writer lock, so they are
|
|
851
|
+
// the envelope/revision this write actually owns. Never post-lock re-read here: a concurrent
|
|
852
|
+
// writer could advance the file before that read and make this payload publish another
|
|
853
|
+
// writer's newer revision into the derived active-state cache.
|
|
854
|
+
if (!writeResult.written || !isPlainObject(writeResult.stamped)) {
|
|
855
|
+
throw new Error(`state writer did not return a stamped workflow envelope for ${filePath}`);
|
|
856
|
+
}
|
|
857
|
+
return { warning, stamped: writeResult.stamped, revision: writeResult.revision };
|
|
855
858
|
}
|
|
856
859
|
|
|
857
860
|
function parseFieldsFlag(args: readonly string[]): StateProjectionField[] | undefined {
|
|
@@ -99,7 +99,7 @@ export interface GuardedStateWriterOptions extends StateWriterOptions {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
export type GuardedWriteResult =
|
|
102
|
-
| { path: string; written: true; revision: number }
|
|
102
|
+
| { path: string; written: true; revision: number; stamped: unknown }
|
|
103
103
|
| { path: string; written: false; reason: "stale-skip"; revision: number };
|
|
104
104
|
|
|
105
105
|
export interface StateWriterOptions {
|
|
@@ -511,7 +511,7 @@ async function writeGuardedResolvedJsonAtomic(
|
|
|
511
511
|
const next = stampStateRevision(withWorkflowReceipt(value, buildReceipt(options)), currentRevision + 1);
|
|
512
512
|
await atomicWrite(filePath, jsonText(next));
|
|
513
513
|
await maybeAudit(filePath, options);
|
|
514
|
-
return { path: filePath, written: true, revision: currentRevision + 1 };
|
|
514
|
+
return { path: filePath, written: true, revision: currentRevision + 1, stamped: next };
|
|
515
515
|
}
|
|
516
516
|
|
|
517
517
|
const incomingSourceRevision =
|
|
@@ -526,7 +526,7 @@ async function writeGuardedResolvedJsonAtomic(
|
|
|
526
526
|
);
|
|
527
527
|
await atomicWrite(filePath, jsonText(next));
|
|
528
528
|
await maybeAudit(filePath, options);
|
|
529
|
-
return { path: filePath, written: true, revision: currentRevision + 1 };
|
|
529
|
+
return { path: filePath, written: true, revision: currentRevision + 1, stamped: next };
|
|
530
530
|
},
|
|
531
531
|
options.lock,
|
|
532
532
|
);
|
|
@@ -574,7 +574,7 @@ export async function writeGuardedWorkflowEnvelopeAtomic(
|
|
|
574
574
|
}
|
|
575
575
|
await atomicWrite(filePath, jsonText(next));
|
|
576
576
|
await maybeAudit(filePath, options);
|
|
577
|
-
return { path: filePath, written: true, revision: currentRevision + 1 };
|
|
577
|
+
return { path: filePath, written: true, revision: currentRevision + 1, stamped: next };
|
|
578
578
|
}
|
|
579
579
|
|
|
580
580
|
const incomingSourceRevision =
|
|
@@ -599,7 +599,7 @@ export async function writeGuardedWorkflowEnvelopeAtomic(
|
|
|
599
599
|
}
|
|
600
600
|
await atomicWrite(filePath, jsonText(next));
|
|
601
601
|
await maybeAudit(filePath, options);
|
|
602
|
-
return { path: filePath, written: true, revision: currentRevision + 1 };
|
|
602
|
+
return { path: filePath, written: true, revision: currentRevision + 1, stamped: next };
|
|
603
603
|
},
|
|
604
604
|
options.lock,
|
|
605
605
|
);
|
|
@@ -1931,16 +1931,14 @@ export function buildWorkerCommand(
|
|
|
1931
1931
|
const workspace = worker.worktree_path
|
|
1932
1932
|
? `Worker worktree: ${worker.worktree_path}.`
|
|
1933
1933
|
: `Worker cwd: ${config.leader.cwd}.`;
|
|
1934
|
-
// The worker prompt body
|
|
1935
|
-
// `tmux send-keys`,
|
|
1936
|
-
//
|
|
1937
|
-
//
|
|
1938
|
-
//
|
|
1939
|
-
//
|
|
1940
|
-
//
|
|
1941
|
-
//
|
|
1942
|
-
// whitespace-only bodies fall back to a one-line placeholder so the
|
|
1943
|
-
// worker never sits idle at an empty prompt.
|
|
1934
|
+
// The worker prompt body must stay single-line because fallback startup on
|
|
1935
|
+
// Windows/psmux dispatches the command through `tmux send-keys`, where
|
|
1936
|
+
// embedded LF characters are treated as Enter keypresses. POSIX tmux starts
|
|
1937
|
+
// workers by passing the command directly to `split-window`, but keeping the
|
|
1938
|
+
// body normalized there too makes fake-tmux logs and shell argv handling
|
|
1939
|
+
// deterministic. Strip a defensive U+FEFF in case a caller managed to inject
|
|
1940
|
+
// a UTF-8 BOM into the task text. Empty / whitespace-only bodies fall back to
|
|
1941
|
+
// a one-line placeholder so the worker never sits idle at an empty prompt.
|
|
1944
1942
|
const normalizePrompt = (raw: string): string =>
|
|
1945
1943
|
raw
|
|
1946
1944
|
.replace(/[\uFEFF\u200B]/g, "")
|
|
@@ -1995,6 +1993,11 @@ export function buildWorkerCommand(
|
|
|
1995
1993
|
}
|
|
1996
1994
|
return `${joined} ${config.worker_command} ${quote(prompt)}`;
|
|
1997
1995
|
}
|
|
1996
|
+
|
|
1997
|
+
function shouldDispatchWorkerWithSendKeys(tmuxCommand: string, platform: NodeJS.Platform = process.platform): boolean {
|
|
1998
|
+
return platform === "win32" || path.basename(tmuxCommand).toLowerCase() === "psmux";
|
|
1999
|
+
}
|
|
2000
|
+
|
|
1998
2001
|
interface GjcTeamInitialLane {
|
|
1999
2002
|
label: string;
|
|
2000
2003
|
title: string;
|
|
@@ -2098,22 +2101,24 @@ async function startTmuxSession(
|
|
|
2098
2101
|
const splitDirection: string = worker.index === 1 ? "-h" : "-v";
|
|
2099
2102
|
const splitTarget: string =
|
|
2100
2103
|
worker.index === 1 ? config.leader.pane_id : (rightStackRootPaneId ?? config.leader.pane_id);
|
|
2101
|
-
const
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2104
|
+
const workerCommand = buildWorkerCommand(config, worker);
|
|
2105
|
+
const workerCwd = worker.worktree_path ?? config.leader.cwd;
|
|
2106
|
+
const useSendKeysFallback = shouldDispatchWorkerWithSendKeys(config.tmux_command);
|
|
2107
|
+
const splitArgs = [
|
|
2108
|
+
config.tmux_command,
|
|
2109
|
+
"split-window",
|
|
2110
|
+
splitDirection,
|
|
2111
|
+
"-t",
|
|
2112
|
+
splitTarget,
|
|
2113
|
+
"-d",
|
|
2114
|
+
"-P",
|
|
2115
|
+
"-F",
|
|
2116
|
+
"#{pane_id}",
|
|
2117
|
+
"-c",
|
|
2118
|
+
workerCwd,
|
|
2119
|
+
...(useSendKeysFallback ? [] : [workerCommand]),
|
|
2120
|
+
];
|
|
2121
|
+
const split: Bun.SyncSubprocess<"pipe", "pipe"> = Bun.spawnSync(splitArgs, { stdout: "pipe", stderr: "pipe" });
|
|
2117
2122
|
if (split.exitCode !== 0)
|
|
2118
2123
|
throw new Error(split.stderr.toString().trim() || `tmux_split_failed:${config.tmux_target}:${worker.id}`);
|
|
2119
2124
|
const paneId: string = split.stdout.toString().trim().split(/\r?\n/)[0]?.trim() ?? "";
|
|
@@ -2121,31 +2126,31 @@ async function startTmuxSession(
|
|
|
2121
2126
|
rollbackPaneIds.push(paneId);
|
|
2122
2127
|
if (worker.index === 1) rightStackRootPaneId = paneId;
|
|
2123
2128
|
workers.push({ ...worker, pane_id: paneId });
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2129
|
+
if (useSendKeysFallback) {
|
|
2130
|
+
// On psmux/ConPTY (Windows pwsh default-shell) panes, tmux writes the
|
|
2131
|
+
// split-window command argv to the new pane's stdin but pwsh's readline
|
|
2132
|
+
// never receives an Enter, so the worker never starts. Create the pane
|
|
2133
|
+
// without a command, then dispatch the worker command through send-keys +
|
|
2134
|
+
// Enter so it actually executes.
|
|
2135
|
+
// Two-step dispatch because tmux's `-l` (literal mode) flag is global per
|
|
2136
|
+
// send-keys invocation, not per arg: passing `-l <body> Enter` would treat
|
|
2137
|
+
// "Enter" as literal text too. Sending the body in literal mode first and
|
|
2138
|
+
// the Enter keypress second keeps the body verbatim while still submitting
|
|
2139
|
+
// the prompt as a keystroke.
|
|
2140
|
+
Bun.spawnSync([config.tmux_command, "send-keys", "-l", "-t", paneId, workerCommand], {
|
|
2141
|
+
stdout: "ignore",
|
|
2142
|
+
stderr: "ignore",
|
|
2143
|
+
});
|
|
2144
|
+
const sendKeys = Bun.spawnSync([config.tmux_command, "send-keys", "-t", paneId, "Enter"], {
|
|
2145
|
+
stdout: "ignore",
|
|
2146
|
+
stderr: "ignore",
|
|
2147
|
+
});
|
|
2148
|
+
// void-cast the exit code so the linter does not flag an unused expression;
|
|
2149
|
+
// the value is intentionally discarded here because the actual spawn outcome
|
|
2150
|
+
// is recovered by the leader through the worker startup-ack watcher, not via
|
|
2151
|
+
// the spawn exit code.
|
|
2152
|
+
void sendKeys.exitCode;
|
|
2153
|
+
}
|
|
2149
2154
|
}
|
|
2150
2155
|
Bun.spawnSync([config.tmux_command, "select-layout", "-t", config.tmux_target, "main-vertical"], {
|
|
2151
2156
|
stdout: "ignore",
|