@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
|
@@ -6,93 +6,23 @@
|
|
|
6
6
|
* - Project: .windsurf
|
|
7
7
|
*
|
|
8
8
|
* Supports:
|
|
9
|
-
* - MCP servers from mcp_config.json
|
|
10
9
|
* - Rules from .windsurf/rules/*.md and ~/.codeium/windsurf/memories/global_rules.md
|
|
11
10
|
* - Legacy .windsurfrules file
|
|
11
|
+
*
|
|
12
|
+
* MCP servers are intentionally NOT inherited live from Windsurf config: GJC
|
|
13
|
+
* owns MCP runtime execution. Copy definitions into GJC's own mcp.json instead.
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
|
-
import { tryParseJson } from "@gajae-code/utils";
|
|
15
16
|
import { registerProvider } from "../capability";
|
|
16
17
|
import { readFile } from "../capability/fs";
|
|
17
|
-
import { type MCPServer, mcpCapability } from "../capability/mcp";
|
|
18
18
|
import { type Rule, ruleCapability } from "../capability/rule";
|
|
19
19
|
import type { LoadContext, LoadResult } from "../capability/types";
|
|
20
|
-
import {
|
|
21
|
-
buildRuleFromMarkdown,
|
|
22
|
-
createSourceMeta,
|
|
23
|
-
expandEnvVarsDeep,
|
|
24
|
-
getProjectPath,
|
|
25
|
-
getUserPath,
|
|
26
|
-
loadFilesFromDir,
|
|
27
|
-
} from "./helpers";
|
|
20
|
+
import { buildRuleFromMarkdown, createSourceMeta, getProjectPath, getUserPath, loadFilesFromDir } from "./helpers";
|
|
28
21
|
|
|
29
22
|
const PROVIDER_ID = "windsurf";
|
|
30
23
|
const DISPLAY_NAME = "Windsurf";
|
|
31
24
|
const PRIORITY = 50;
|
|
32
25
|
|
|
33
|
-
// =============================================================================
|
|
34
|
-
// MCP Servers
|
|
35
|
-
// =============================================================================
|
|
36
|
-
|
|
37
|
-
function parseServerConfig(
|
|
38
|
-
name: string,
|
|
39
|
-
serverConfig: unknown,
|
|
40
|
-
path: string,
|
|
41
|
-
scope: "user" | "project",
|
|
42
|
-
): { server?: MCPServer; warning?: string } {
|
|
43
|
-
if (typeof serverConfig !== "object" || serverConfig === null) {
|
|
44
|
-
return { warning: `Invalid server config for "${name}" in ${path}` };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const server = expandEnvVarsDeep(serverConfig as Record<string, unknown>);
|
|
48
|
-
return {
|
|
49
|
-
server: {
|
|
50
|
-
name,
|
|
51
|
-
command: server.command as string | undefined,
|
|
52
|
-
args: server.args as string[] | undefined,
|
|
53
|
-
env: server.env as Record<string, string> | undefined,
|
|
54
|
-
url: server.url as string | undefined,
|
|
55
|
-
headers: server.headers as Record<string, string> | undefined,
|
|
56
|
-
transport: server.type as "stdio" | "sse" | "http" | undefined,
|
|
57
|
-
timeout: typeof server.timeout === "number" ? server.timeout : undefined,
|
|
58
|
-
_source: createSourceMeta(PROVIDER_ID, path, scope),
|
|
59
|
-
},
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>> {
|
|
64
|
-
const items: MCPServer[] = [];
|
|
65
|
-
const warnings: string[] = [];
|
|
66
|
-
|
|
67
|
-
const userPath = getUserPath(ctx, "windsurf", "mcp_config.json");
|
|
68
|
-
const [userContent, projectPath] = await Promise.all([
|
|
69
|
-
userPath ? readFile(userPath) : Promise.resolve(null),
|
|
70
|
-
getProjectPath(ctx, "windsurf", "mcp_config.json"),
|
|
71
|
-
]);
|
|
72
|
-
|
|
73
|
-
const projectContent = projectPath ? await readFile(projectPath) : null;
|
|
74
|
-
|
|
75
|
-
const configs: Array<{ content: string | null; path: string | null; scope: "user" | "project" }> = [
|
|
76
|
-
{ content: userContent, path: userPath, scope: "user" },
|
|
77
|
-
{ content: projectContent, path: projectPath, scope: "project" },
|
|
78
|
-
];
|
|
79
|
-
|
|
80
|
-
for (const { content, path, scope } of configs) {
|
|
81
|
-
if (!content || !path) continue;
|
|
82
|
-
|
|
83
|
-
const config = tryParseJson<{ mcpServers?: Record<string, unknown> }>(content);
|
|
84
|
-
if (!config?.mcpServers) continue;
|
|
85
|
-
|
|
86
|
-
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
|
|
87
|
-
const result = parseServerConfig(name, serverConfig, path, scope);
|
|
88
|
-
if (result.warning) warnings.push(result.warning);
|
|
89
|
-
if (result.server) items.push(result.server);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return { items, warnings };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
26
|
// =============================================================================
|
|
97
27
|
// Rules
|
|
98
28
|
// =============================================================================
|
|
@@ -130,14 +60,6 @@ async function loadRules(ctx: LoadContext): Promise<LoadResult<Rule>> {
|
|
|
130
60
|
// Provider Registration
|
|
131
61
|
// =============================================================================
|
|
132
62
|
|
|
133
|
-
registerProvider<MCPServer>(mcpCapability.id, {
|
|
134
|
-
id: PROVIDER_ID,
|
|
135
|
-
displayName: DISPLAY_NAME,
|
|
136
|
-
description: "Load MCP servers from Windsurf config (mcp_config.json)",
|
|
137
|
-
priority: PRIORITY,
|
|
138
|
-
load: loadMCPServers,
|
|
139
|
-
});
|
|
140
|
-
|
|
141
63
|
registerProvider<Rule>(ruleCapability.id, {
|
|
142
64
|
id: PROVIDER_ID,
|
|
143
65
|
displayName: DISPLAY_NAME,
|
package/src/edit/read-file.ts
CHANGED
|
@@ -14,6 +14,13 @@ import { isNotebookPath, readEditableNotebookText, serializeEditedNotebookText }
|
|
|
14
14
|
*/
|
|
15
15
|
export const MAX_EDIT_FILE_BYTES = 8 * 1024 * 1024;
|
|
16
16
|
|
|
17
|
+
const UTF8_BOM = [0xef, 0xbb, 0xbf] as const;
|
|
18
|
+
|
|
19
|
+
async function fileHasUtf8Bom(file: Bun.BunFile): Promise<boolean> {
|
|
20
|
+
const bytes = await file.slice(0, UTF8_BOM.length).bytes();
|
|
21
|
+
return UTF8_BOM.every((byte, index) => bytes[index] === byte);
|
|
22
|
+
}
|
|
23
|
+
|
|
17
24
|
export async function readEditFileText(absolutePath: string, path: string): Promise<string> {
|
|
18
25
|
try {
|
|
19
26
|
const file = Bun.file(absolutePath);
|
|
@@ -28,7 +35,8 @@ export async function readEditFileText(absolutePath: string, path: string): Prom
|
|
|
28
35
|
// Guard BEFORE the notebook fast-path: a >8 MiB .ipynb would otherwise load + JSON-parse
|
|
29
36
|
// + convert the whole file via readEditableNotebookText, bypassing the F19 freeze guard.
|
|
30
37
|
if (isNotebookPath(absolutePath)) return await readEditableNotebookText(absolutePath, path);
|
|
31
|
-
|
|
38
|
+
const text = await file.text();
|
|
39
|
+
return (await fileHasUtf8Bom(file)) && !text.startsWith("\uFEFF") ? `\uFEFF${text}` : text;
|
|
32
40
|
} catch (error) {
|
|
33
41
|
if (isEnoent(error)) {
|
|
34
42
|
throw new Error(`File not found: ${path}`);
|
|
@@ -37,6 +37,7 @@ export const GJC_TMUX_LAUNCHED_ENV = "GJC_TMUX_LAUNCHED";
|
|
|
37
37
|
export const GJC_LAUNCH_POLICY_ENV = "GJC_LAUNCH_POLICY";
|
|
38
38
|
export const GJC_TMUX_WINDOW_LABEL_MAX_WIDTH = 48;
|
|
39
39
|
export const GJC_PSMUX_PROFILE_FORCE_ENV = "GJC_PSMUX_PROFILE_FORCE";
|
|
40
|
+
const WINDOWS_PSMUX_ATTACH_RETRY_DELAY_MS = 100;
|
|
40
41
|
const TERMINAL_TITLE_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/g;
|
|
41
42
|
|
|
42
43
|
type LaunchPolicy = "direct" | "tmux";
|
|
@@ -59,6 +60,7 @@ export interface TmuxLaunchContext {
|
|
|
59
60
|
tty?: TtyState;
|
|
60
61
|
spawnSync?: TmuxSpawnSync;
|
|
61
62
|
tmuxAvailable?: boolean;
|
|
63
|
+
tmuxStatusLines?: number;
|
|
62
64
|
worktreeBranch?: string | null;
|
|
63
65
|
currentBranch?: string | null;
|
|
64
66
|
existingBranchSessionName?: string | null;
|
|
@@ -107,6 +109,8 @@ export interface TmuxLaunchPlan {
|
|
|
107
109
|
project?: string | null;
|
|
108
110
|
sessionId?: string | null;
|
|
109
111
|
sessionStateFile?: string | null;
|
|
112
|
+
isPsmux: boolean;
|
|
113
|
+
platform: NodeJS.Platform;
|
|
110
114
|
}
|
|
111
115
|
|
|
112
116
|
function explicitTmuxSessionName(env: NodeJS.ProcessEnv): string | undefined {
|
|
@@ -117,7 +121,9 @@ function hasCurrentGjcVersion(session: GjcTmuxSessionStatus | undefined): boolea
|
|
|
117
121
|
}
|
|
118
122
|
|
|
119
123
|
function allowsExistingTmuxAttach(parsed: Args, env: NodeJS.ProcessEnv): boolean {
|
|
120
|
-
|
|
124
|
+
// `--resume` belongs to the inner GJC session resolver. Let it reach main.ts so
|
|
125
|
+
// value-less resume can show the session picker and valued resume can honor the target.
|
|
126
|
+
return Boolean(parsed.continue || explicitTmuxSessionName(env));
|
|
121
127
|
}
|
|
122
128
|
|
|
123
129
|
function findExistingSessionForLaunch(context: {
|
|
@@ -544,42 +550,88 @@ function isTmuxAttachDisconnectError(result: TmuxSpawnResult): boolean {
|
|
|
544
550
|
const stderr = result.stderr?.toLowerCase() ?? "";
|
|
545
551
|
return stderr.includes("eio") || stderr.includes("input/output error");
|
|
546
552
|
}
|
|
553
|
+
function isWindowsPsmuxAttachConnectionRefused(plan: TmuxLaunchPlan, result: TmuxSpawnResult): boolean {
|
|
554
|
+
if (plan.platform !== "win32" || !plan.isPsmux) return false;
|
|
555
|
+
return result.stderr?.toLowerCase().includes("os error 10061") === true;
|
|
556
|
+
}
|
|
557
|
+
function waitForWindowsPsmuxAttachRetry(): void {
|
|
558
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, WINDOWS_PSMUX_ATTACH_RETRY_DELAY_MS);
|
|
559
|
+
}
|
|
547
560
|
function normalizeTmuxTerminalDimension(value: number | undefined): number | undefined {
|
|
548
561
|
if (value === undefined || !Number.isSafeInteger(value) || value <= 0) return undefined;
|
|
549
562
|
return value;
|
|
550
563
|
}
|
|
551
564
|
|
|
552
|
-
function
|
|
565
|
+
function normalizeTmuxStatusLineCount(value: number | undefined): number {
|
|
566
|
+
if (value === undefined || !Number.isSafeInteger(value) || value <= 0) return 0;
|
|
567
|
+
return value;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function parseTmuxStatusLineCount(value: string): number {
|
|
571
|
+
const normalized = value.trim().toLowerCase();
|
|
572
|
+
if (normalized.length === 0 || normalized === "off" || normalized === "0") return 0;
|
|
573
|
+
if (normalized === "on") return 1;
|
|
574
|
+
const parsed = Number.parseInt(normalized, 10);
|
|
575
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : 1;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function readTmuxStatusLineCount(tmuxCommand: string, cwd: string, env: NodeJS.ProcessEnv): number {
|
|
579
|
+
const result = Bun.spawnSync([tmuxCommand, "show-options", "-gqv", "status"], {
|
|
580
|
+
cwd,
|
|
581
|
+
env,
|
|
582
|
+
stdin: "pipe",
|
|
583
|
+
stdout: "pipe",
|
|
584
|
+
stderr: "pipe",
|
|
585
|
+
});
|
|
586
|
+
if (result.exitCode !== 0) return 0;
|
|
587
|
+
return parseTmuxStatusLineCount(new TextDecoder().decode(result.stdout));
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function resolveCallerTmuxTerminalSize(tty: TtyState, tmuxStatusLines = 0): TmuxTerminalSize | undefined {
|
|
553
591
|
if (!tty.stdout) return undefined;
|
|
554
592
|
const columns = normalizeTmuxTerminalDimension(tty.columns);
|
|
555
593
|
const rows = normalizeTmuxTerminalDimension(tty.rows);
|
|
556
594
|
if (columns === undefined || rows === undefined) return undefined;
|
|
557
|
-
|
|
595
|
+
const adjustedRows = Math.max(1, rows - normalizeTmuxStatusLineCount(tmuxStatusLines));
|
|
596
|
+
return { columns, rows: adjustedRows };
|
|
558
597
|
}
|
|
559
598
|
|
|
560
599
|
function buildTmuxNewSessionSizeArgs(size: TmuxTerminalSize | undefined): string[] {
|
|
561
600
|
return size ? ["-x", String(size.columns), "-y", String(size.rows)] : [];
|
|
562
601
|
}
|
|
563
602
|
|
|
564
|
-
|
|
603
|
+
// Ensure the freshly created window fits the terminal that ultimately attaches.
|
|
604
|
+
// `new-session` already starts the window (and the inner TUI) at the caller's
|
|
605
|
+
// captured `-x/-y` size; this step governs what happens on `attach-session`.
|
|
606
|
+
//
|
|
607
|
+
// On native tmux we must NOT reassert with `resize-window`: that command flips
|
|
608
|
+
// the window's `window-size` option to `manual`, pinning it to the capture-time
|
|
609
|
+
// dimensions and stopping `attach-session` from resizing the window to the real
|
|
610
|
+
// client. When the attaching terminal is larger than the capture — e.g. a GUI
|
|
611
|
+
// terminal that reports a smaller size before it finishes sizing — the pinned
|
|
612
|
+
// window stays small and tmux paints the uncovered client area with `·` fill
|
|
613
|
+
// (the "window smaller than client" symptom). Keeping `window-size` on `latest`
|
|
614
|
+
// lets tmux size the window to the attaching client (status line included).
|
|
615
|
+
//
|
|
616
|
+
// psmux (Windows) does not share tmux's `window-size` semantics, so preserve the
|
|
617
|
+
// historical explicit `resize-window` reassert there rather than sending an
|
|
618
|
+
// option its server may reject and echo into the user's pane.
|
|
619
|
+
function ensureCreatedTmuxWindowTracksCallerTerminal(
|
|
565
620
|
plan: TmuxLaunchPlan,
|
|
566
621
|
spawnSync: TmuxSpawnSync,
|
|
567
622
|
options: TmuxSpawnOptions,
|
|
568
623
|
): void {
|
|
569
624
|
if (!plan.initialSize) return;
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
"-t",
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
],
|
|
581
|
-
options,
|
|
582
|
-
);
|
|
625
|
+
const target = buildGjcTmuxExactOptionTarget(plan.sessionName, { env: options.env });
|
|
626
|
+
if (plan.isPsmux) {
|
|
627
|
+
spawnSync(
|
|
628
|
+
plan.tmuxCommand,
|
|
629
|
+
["resize-window", "-t", target, "-x", String(plan.initialSize.columns), "-y", String(plan.initialSize.rows)],
|
|
630
|
+
options,
|
|
631
|
+
);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
spawnSync(plan.tmuxCommand, ["set-window-option", "-t", target, "window-size", "latest"], options);
|
|
583
635
|
}
|
|
584
636
|
|
|
585
637
|
export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaunchPlan | undefined {
|
|
@@ -641,9 +693,14 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
|
|
|
641
693
|
},
|
|
642
694
|
context.rawArgs,
|
|
643
695
|
);
|
|
644
|
-
const
|
|
696
|
+
const tmuxStatusLines =
|
|
697
|
+
context.tmuxStatusLines ??
|
|
698
|
+
(context.tmuxAvailable === undefined ? readTmuxStatusLineCount(tmuxCommand, cwd, env) : 0);
|
|
699
|
+
const initialSize = resolveCallerTmuxTerminalSize(tty, tmuxStatusLines);
|
|
645
700
|
return {
|
|
646
701
|
tmuxCommand,
|
|
702
|
+
isPsmux: resolvedBinary.isPsmux,
|
|
703
|
+
platform,
|
|
647
704
|
sessionName,
|
|
648
705
|
cwd,
|
|
649
706
|
innerCommand,
|
|
@@ -742,6 +799,30 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
742
799
|
const rootTerminalTitle = shouldSetGjcTmuxRootTerminalTitle(context.parsed, env)
|
|
743
800
|
? buildGjcTmuxRootTerminalTitle(plan.project ?? plan.cwd, plan.branch)
|
|
744
801
|
: undefined;
|
|
802
|
+
const buildProfileInputs = (): GjcTmuxProfileContext => ({
|
|
803
|
+
tmuxCommand: plan.tmuxCommand,
|
|
804
|
+
target: plan.sessionName,
|
|
805
|
+
cwd: plan.cwd,
|
|
806
|
+
env,
|
|
807
|
+
spawnSync,
|
|
808
|
+
branch: plan.branch,
|
|
809
|
+
project: plan.project,
|
|
810
|
+
sessionId: plan.sessionId ?? null,
|
|
811
|
+
sessionStateFile: plan.sessionStateFile ?? null,
|
|
812
|
+
version: VERSION,
|
|
813
|
+
});
|
|
814
|
+
const probeHasSession = (): TmuxSpawnResult =>
|
|
815
|
+
spawnSync(
|
|
816
|
+
plan.tmuxCommand,
|
|
817
|
+
["has-session", "-t", buildGjcTmuxExactSessionTarget(plan.sessionName, { env })],
|
|
818
|
+
probeOptions,
|
|
819
|
+
);
|
|
820
|
+
const attachCreatedSession = (): TmuxSpawnResult =>
|
|
821
|
+
spawnSync(
|
|
822
|
+
plan.tmuxCommand,
|
|
823
|
+
["attach-session", "-t", buildGjcTmuxExactSessionTarget(plan.sessionName, { env })],
|
|
824
|
+
attachOptions,
|
|
825
|
+
);
|
|
745
826
|
|
|
746
827
|
if (plan.attachSessionName) {
|
|
747
828
|
applyGjcTmuxRootTerminalTitleProfile({
|
|
@@ -767,24 +848,6 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
767
848
|
// server first, and if the race fired, retry new-session before
|
|
768
849
|
// we hand the session to rename-window / applyGjcTmuxProfile so the
|
|
769
850
|
// profile-tagging path always sees a registered session.
|
|
770
|
-
const buildProfileInputs = (): GjcTmuxProfileContext => ({
|
|
771
|
-
tmuxCommand: plan.tmuxCommand,
|
|
772
|
-
target: plan.sessionName,
|
|
773
|
-
cwd: plan.cwd,
|
|
774
|
-
env,
|
|
775
|
-
spawnSync,
|
|
776
|
-
branch: plan.branch,
|
|
777
|
-
project: plan.project,
|
|
778
|
-
sessionId: plan.sessionId ?? null,
|
|
779
|
-
sessionStateFile: plan.sessionStateFile ?? null,
|
|
780
|
-
version: VERSION,
|
|
781
|
-
});
|
|
782
|
-
const probeHasSession = (): TmuxSpawnResult =>
|
|
783
|
-
spawnSync(
|
|
784
|
-
plan.tmuxCommand,
|
|
785
|
-
["has-session", "-t", buildGjcTmuxExactSessionTarget(plan.sessionName, { env })],
|
|
786
|
-
probeOptions,
|
|
787
|
-
);
|
|
788
851
|
const probeResult = probeHasSession();
|
|
789
852
|
if (probeResult.exitCode !== 0) {
|
|
790
853
|
const retry = spawnSync(plan.tmuxCommand, plan.newSessionArgs, newSessionOptions);
|
|
@@ -851,7 +914,7 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
851
914
|
}
|
|
852
915
|
// Recovery succeeded via retry — fall through to attach-session below.
|
|
853
916
|
}
|
|
854
|
-
|
|
917
|
+
ensureCreatedTmuxWindowTracksCallerTerminal(plan, spawnSync, controlOptions);
|
|
855
918
|
applyGjcTmuxRootTerminalTitleProfile({
|
|
856
919
|
tmuxCommand: plan.tmuxCommand,
|
|
857
920
|
target: plan.sessionName,
|
|
@@ -875,16 +938,75 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
875
938
|
return false;
|
|
876
939
|
}
|
|
877
940
|
// attach-session needs PTY inherit for the user-facing attach; keep it unchanged.
|
|
878
|
-
const attached =
|
|
879
|
-
plan.tmuxCommand,
|
|
880
|
-
["attach-session", "-t", buildGjcTmuxExactSessionTarget(plan.sessionName, { env })],
|
|
881
|
-
attachOptions,
|
|
882
|
-
);
|
|
941
|
+
const attached = attachCreatedSession();
|
|
883
942
|
if (attached.exitCode === 0) return true;
|
|
884
943
|
if (isTmuxAttachDisconnectError(attached)) {
|
|
885
944
|
(context.diagnosticWriter ?? safeStderrWrite)(formatTmuxLaunchDiagnostic("attach disconnected", attached.stderr));
|
|
886
945
|
return true;
|
|
887
946
|
}
|
|
947
|
+
if (isWindowsPsmuxAttachConnectionRefused(plan, attached)) {
|
|
948
|
+
waitForWindowsPsmuxAttachRetry();
|
|
949
|
+
const probeAfterAttach = probeHasSession();
|
|
950
|
+
if (probeAfterAttach.exitCode === 0) {
|
|
951
|
+
const retryAttached = attachCreatedSession();
|
|
952
|
+
if (retryAttached.exitCode === 0) return true;
|
|
953
|
+
if (isTmuxAttachDisconnectError(retryAttached)) {
|
|
954
|
+
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
955
|
+
formatTmuxLaunchDiagnostic("attach disconnected", retryAttached.stderr),
|
|
956
|
+
);
|
|
957
|
+
return true;
|
|
958
|
+
}
|
|
959
|
+
} else {
|
|
960
|
+
const retry = spawnSync(plan.tmuxCommand, plan.newSessionArgs, newSessionOptions);
|
|
961
|
+
const retryProbe = probeHasSession();
|
|
962
|
+
if (retry.exitCode === 0 && retryProbe.exitCode === 0) {
|
|
963
|
+
renameTmuxWindow(
|
|
964
|
+
plan.tmuxCommand,
|
|
965
|
+
windowTitle,
|
|
966
|
+
spawnSync,
|
|
967
|
+
controlOptions,
|
|
968
|
+
buildGjcTmuxExactSessionTarget(plan.sessionName, { env }),
|
|
969
|
+
);
|
|
970
|
+
const retryProfile = applyGjcTmuxProfile(buildProfileInputs());
|
|
971
|
+
const retryOwnershipFailure = retryProfile.failures.find(item =>
|
|
972
|
+
item.command.args.includes("@gjc-profile"),
|
|
973
|
+
);
|
|
974
|
+
if (!retryOwnershipFailure) {
|
|
975
|
+
ensureCreatedTmuxWindowTracksCallerTerminal(plan, spawnSync, controlOptions);
|
|
976
|
+
applyGjcTmuxRootTerminalTitleProfile({
|
|
977
|
+
tmuxCommand: plan.tmuxCommand,
|
|
978
|
+
target: plan.sessionName,
|
|
979
|
+
title: rootTerminalTitle,
|
|
980
|
+
spawnSync,
|
|
981
|
+
options,
|
|
982
|
+
});
|
|
983
|
+
const retryAttached = attachCreatedSession();
|
|
984
|
+
if (retryAttached.exitCode === 0) return true;
|
|
985
|
+
if (isTmuxAttachDisconnectError(retryAttached)) {
|
|
986
|
+
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
987
|
+
formatTmuxLaunchDiagnostic("attach disconnected", retryAttached.stderr),
|
|
988
|
+
);
|
|
989
|
+
return true;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
// Closing an SSH/Windows Terminal tab can make `tmux attach-session`
|
|
996
|
+
// exit with code 1 and no captured stderr while the tmux server correctly
|
|
997
|
+
// keeps the just-created session alive. Preserve that live session so the
|
|
998
|
+
// user can reattach instead of treating the parent client teardown as a
|
|
999
|
+
// launch failure.
|
|
1000
|
+
const attachFailureStderr = attached.stderr?.trim() ?? "";
|
|
1001
|
+
if (attachFailureStderr.length === 0) {
|
|
1002
|
+
const probeAfterAttachFailure = probeHasSession();
|
|
1003
|
+
if (probeAfterAttachFailure.exitCode === 0) {
|
|
1004
|
+
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
1005
|
+
formatTmuxLaunchDiagnostic("attach disconnected", attached.stderr),
|
|
1006
|
+
);
|
|
1007
|
+
return true;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
888
1010
|
cleanupCreatedTmuxSession(plan, spawnSync, options);
|
|
889
1011
|
(context.diagnosticWriter ?? safeStderrWrite)(formatTmuxLaunchDiagnostic("attach failed", attached.stderr));
|
|
890
1012
|
return true;
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
type RalplanIndexRow,
|
|
12
12
|
summarizeRalplanIndex,
|
|
13
13
|
} from "./ledger-event-renderer";
|
|
14
|
-
import { isRestrictedRoleAgentBash } from "./restricted-role-agent-bash";
|
|
14
|
+
import { GJC_RALPLAN_ARTIFACT_ENV, isRestrictedRoleAgentBash } from "./restricted-role-agent-bash";
|
|
15
15
|
import { modeStatePath, sessionPlansDir } from "./session-layout";
|
|
16
16
|
import { resolveGjcSessionForWrite, writeSessionActivityMarker } from "./session-resolution";
|
|
17
17
|
import { migrateWorkflowState } from "./state-migrations";
|
|
@@ -35,8 +35,9 @@ import {
|
|
|
35
35
|
* that lives in the bundled `/skill:ralplan` skill — but it accepts every documented flag so
|
|
36
36
|
* scripted users see a useful response and the active run is visible to the TUI.
|
|
37
37
|
*
|
|
38
|
-
* 2. **Artifact write**: `gjc ralplan --write --stage <type> --stage_n <N>
|
|
39
|
-
* <path-or-string>
|
|
38
|
+
* 2. **Artifact write**: `gjc ralplan --write --stage <type> --stage_n <N>
|
|
39
|
+
* (--artifact <path-or-string> | --artifact-env GJC_RALPLAN_ARTIFACT)
|
|
40
|
+
* [--run-id <id>] [--session-id <id>] [--json]` persists Planner / Architect
|
|
40
41
|
* / Critic / revision / post-interview / ADR / final markdown under `.gjc/plans/ralplan/<run-id>/`, maintains
|
|
41
42
|
* an `index.jsonl` audit log, copies `final` stages to `pending-approval.md`, and advances
|
|
42
43
|
* the HUD chip to reflect the latest persisted stage.
|
|
@@ -81,6 +82,7 @@ const VALUE_FLAGS = new Set([
|
|
|
81
82
|
"--stage",
|
|
82
83
|
"--stage_n",
|
|
83
84
|
"--artifact",
|
|
85
|
+
"--artifact-env",
|
|
84
86
|
"--run-id",
|
|
85
87
|
"--session-id",
|
|
86
88
|
"--architect",
|
|
@@ -409,8 +411,16 @@ async function resolveArtifactArgs(args: readonly string[], cwd: string): Promis
|
|
|
409
411
|
const stageN = parseStageN(flagValue(args, "--stage_n"));
|
|
410
412
|
|
|
411
413
|
const rawArtifact = flagValue(args, "--artifact");
|
|
412
|
-
|
|
413
|
-
|
|
414
|
+
const artifactEnvName = flagValue(args, "--artifact-env");
|
|
415
|
+
const artifactSources = [rawArtifact, artifactEnvName].filter(value => value !== undefined);
|
|
416
|
+
if (artifactSources.length === 0 || artifactSources.some(value => value === "")) {
|
|
417
|
+
throw new RalplanCommandError(2, "--artifact or --artifact-env is required for ralplan --write");
|
|
418
|
+
}
|
|
419
|
+
if (artifactSources.length > 1) {
|
|
420
|
+
throw new RalplanCommandError(2, "--artifact and --artifact-env are mutually exclusive");
|
|
421
|
+
}
|
|
422
|
+
if (artifactEnvName !== undefined && artifactEnvName !== GJC_RALPLAN_ARTIFACT_ENV) {
|
|
423
|
+
throw new RalplanCommandError(2, `--artifact-env must be ${GJC_RALPLAN_ARTIFACT_ENV}`);
|
|
414
424
|
}
|
|
415
425
|
|
|
416
426
|
const session = resolveGjcSessionForWrite(cwd, {
|
|
@@ -430,7 +440,13 @@ async function resolveArtifactArgs(args: readonly string[], cwd: string): Promis
|
|
|
430
440
|
const runId = explicitRunId || (await readActiveRunId(cwd, sessionId)) || sessionIdRaw || defaultRunId();
|
|
431
441
|
assertSafePathComponent(runId, "run-id");
|
|
432
442
|
|
|
433
|
-
const artifact =
|
|
443
|
+
const artifact =
|
|
444
|
+
artifactEnvName !== undefined
|
|
445
|
+
? (process.env[GJC_RALPLAN_ARTIFACT_ENV] ?? "")
|
|
446
|
+
: await resolveArtifactContent(rawArtifact!, cwd);
|
|
447
|
+
if (artifact === "") {
|
|
448
|
+
throw new RalplanCommandError(2, "artifact content is empty");
|
|
449
|
+
}
|
|
434
450
|
return { stage: stage as RalplanStage, stageN, runId, artifact, sessionId, json: hasFlag(args, "--json") };
|
|
435
451
|
}
|
|
436
452
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export const GJC_RESTRICTED_ROLE_AGENT_BASH_ENV = "GJC_RESTRICTED_ROLE_AGENT_BASH";
|
|
2
|
+
export const GJC_RALPLAN_ARTIFACT_ENV = "GJC_RALPLAN_ARTIFACT";
|
|
2
3
|
|
|
3
4
|
export function isRestrictedRoleAgentBash(): boolean {
|
|
4
5
|
return process.env[GJC_RESTRICTED_ROLE_AGENT_BASH_ENV] === "1";
|