@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.
Files changed (148) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +2 -2
  3. package/dist/types/capability/mcp.d.ts +2 -0
  4. package/dist/types/config/keybindings.d.ts +11 -5
  5. package/dist/types/config/settings-schema.d.ts +25 -2
  6. package/dist/types/defaults/gjc-defaults.d.ts +7 -0
  7. package/dist/types/discovery/cursor.d.ts +4 -1
  8. package/dist/types/discovery/index.d.ts +0 -1
  9. package/dist/types/discovery/windsurf.d.ts +3 -1
  10. package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -0
  11. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +3 -2
  12. package/dist/types/gjc-runtime/restricted-role-agent-bash.d.ts +1 -0
  13. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +5 -0
  14. package/dist/types/gjc-runtime/state-writer.d.ts +1 -0
  15. package/dist/types/modes/components/custom-editor.d.ts +6 -1
  16. package/dist/types/modes/components/settings-selector.d.ts +1 -0
  17. package/dist/types/modes/components/skill-message.d.ts +1 -0
  18. package/dist/types/modes/components/status-line.d.ts +8 -0
  19. package/dist/types/modes/components/two-column-body.d.ts +9 -0
  20. package/dist/types/modes/components/welcome.d.ts +9 -1
  21. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  22. package/dist/types/modes/controllers/input-controller.d.ts +5 -1
  23. package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
  24. package/dist/types/modes/interactive-mode.d.ts +2 -0
  25. package/dist/types/modes/prompt-action-autocomplete.d.ts +2 -0
  26. package/dist/types/modes/rpc/rpc-types.d.ts +1 -0
  27. package/dist/types/modes/slash-command-visibility.d.ts +1 -0
  28. package/dist/types/modes/types.d.ts +2 -0
  29. package/dist/types/runtime-mcp/config-writer.d.ts +9 -0
  30. package/dist/types/runtime-mcp/config.d.ts +2 -0
  31. package/dist/types/runtime-mcp/loader.d.ts +2 -0
  32. package/dist/types/runtime-mcp/manager.d.ts +2 -0
  33. package/dist/types/runtime-mcp/types.d.ts +6 -0
  34. package/dist/types/sdk.d.ts +1 -1
  35. package/dist/types/session/agent-session.d.ts +8 -5
  36. package/dist/types/slash-commands/types.d.ts +2 -0
  37. package/dist/types/system-prompt.d.ts +7 -0
  38. package/dist/types/utils/cmux-workspace.d.ts +30 -1
  39. package/dist/types/web/search/index.d.ts +3 -1
  40. package/dist/types/web/search/provider.d.ts +9 -0
  41. package/dist/types/web/search/providers/utils.d.ts +42 -13
  42. package/dist/types/workflow/workflow-intent-diff.d.ts +19 -0
  43. package/package.json +10 -10
  44. package/src/capability/mcp.ts +2 -0
  45. package/src/cli/update-cli.ts +24 -0
  46. package/src/cli/web-search-cli.ts +2 -5
  47. package/src/cli.ts +1 -1
  48. package/src/commands/launch.ts +1 -1
  49. package/src/commands/ralplan.ts +1 -0
  50. package/src/config/keybindings.ts +14 -5
  51. package/src/config/mcp-schema.json +5 -1
  52. package/src/config/model-profiles.ts +16 -0
  53. package/src/config/settings-schema.ts +18 -2
  54. package/src/coordinator-mcp/server.ts +15 -3
  55. package/src/defaults/gjc/skills/ralplan/SKILL.md +16 -14
  56. package/src/defaults/gjc-defaults.ts +16 -0
  57. package/src/discovery/builtin.ts +12 -0
  58. package/src/discovery/cursor.ts +5 -84
  59. package/src/discovery/gemini.ts +3 -89
  60. package/src/discovery/index.ts +4 -1
  61. package/src/discovery/mcp-json.ts +11 -0
  62. package/src/discovery/opencode.ts +5 -122
  63. package/src/discovery/windsurf.ts +4 -82
  64. package/src/edit/read-file.ts +9 -1
  65. package/src/extensibility/extensions/runner.ts +2 -1
  66. package/src/gjc-runtime/launch-tmux.ts +164 -42
  67. package/src/gjc-runtime/ralplan-runtime.ts +22 -6
  68. package/src/gjc-runtime/restricted-role-agent-bash.ts +1 -0
  69. package/src/gjc-runtime/session-state-sidecar.ts +167 -24
  70. package/src/gjc-runtime/state-runtime.ts +8 -5
  71. package/src/gjc-runtime/state-writer.ts +5 -5
  72. package/src/gjc-runtime/team-runtime.ts +56 -51
  73. package/src/internal-urls/docs-index.generated.ts +8 -7
  74. package/src/main.ts +2 -2
  75. package/src/migrate/executor.ts +23 -6
  76. package/src/modes/components/agent-dashboard.ts +1 -34
  77. package/src/modes/components/custom-editor.ts +26 -0
  78. package/src/modes/components/extensions/extension-dashboard.ts +2 -48
  79. package/src/modes/components/model-selector.ts +64 -4
  80. package/src/modes/components/session-selector.ts +19 -9
  81. package/src/modes/components/settings-selector.ts +13 -2
  82. package/src/modes/components/skill-message.ts +18 -10
  83. package/src/modes/components/status-line.ts +215 -86
  84. package/src/modes/components/tree-selector.ts +28 -8
  85. package/src/modes/components/two-column-body.ts +36 -0
  86. package/src/modes/components/welcome.ts +171 -35
  87. package/src/modes/controllers/command-controller.ts +23 -0
  88. package/src/modes/controllers/event-controller.ts +4 -0
  89. package/src/modes/controllers/input-controller.ts +192 -39
  90. package/src/modes/controllers/selector-controller.ts +25 -28
  91. package/src/modes/interactive-mode.ts +36 -30
  92. package/src/modes/prompt-action-autocomplete.ts +23 -1
  93. package/src/modes/rpc/rpc-types.ts +1 -0
  94. package/src/modes/shared/agent-wire/deep-interview-gate.ts +9 -3
  95. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +20 -0
  96. package/src/modes/slash-command-visibility.ts +9 -0
  97. package/src/modes/types.ts +2 -0
  98. package/src/modes/utils/hotkeys-markdown.ts +5 -2
  99. package/src/modes/utils/ui-helpers.ts +76 -3
  100. package/src/notifications/index.ts +20 -1
  101. package/src/notifications/lifecycle-control-runtime.ts +12 -3
  102. package/src/notifications/telegram-daemon.ts +4 -4
  103. package/src/prompts/agents/architect.md +7 -4
  104. package/src/prompts/agents/critic.md +15 -6
  105. package/src/prompts/agents/planner.md +8 -3
  106. package/src/prompts/system/system-prompt.md +15 -1
  107. package/src/prompts/tools/bash.md +1 -1
  108. package/src/runtime-mcp/config-writer.ts +28 -0
  109. package/src/runtime-mcp/config.ts +7 -0
  110. package/src/runtime-mcp/loader.ts +3 -0
  111. package/src/runtime-mcp/manager.ts +3 -0
  112. package/src/runtime-mcp/types.ts +6 -0
  113. package/src/sdk.ts +7 -6
  114. package/src/session/agent-session.ts +81 -18
  115. package/src/session/session-manager.ts +58 -18
  116. package/src/slash-commands/builtin-registry.ts +30 -10
  117. package/src/slash-commands/types.ts +2 -0
  118. package/src/system-prompt.ts +8 -0
  119. package/src/task/executor.ts +5 -0
  120. package/src/tools/ast-grep.ts +10 -7
  121. package/src/tools/bash.ts +39 -14
  122. package/src/tools/index.ts +8 -1
  123. package/src/utils/cmux-workspace.ts +134 -7
  124. package/src/utils/title-generator.ts +1 -1
  125. package/src/web/kagi.ts +1 -1
  126. package/src/web/parallel.ts +2 -2
  127. package/src/web/search/index.ts +118 -45
  128. package/src/web/search/provider.ts +65 -0
  129. package/src/web/search/providers/anthropic.ts +1 -1
  130. package/src/web/search/providers/brave.ts +1 -1
  131. package/src/web/search/providers/codex.ts +1 -1
  132. package/src/web/search/providers/duckduckgo.ts +1 -1
  133. package/src/web/search/providers/exa.ts +1 -1
  134. package/src/web/search/providers/gemini.ts +7 -3
  135. package/src/web/search/providers/jina.ts +1 -1
  136. package/src/web/search/providers/kimi.ts +6 -1
  137. package/src/web/search/providers/openai-compatible.ts +1 -1
  138. package/src/web/search/providers/parallel.ts +1 -1
  139. package/src/web/search/providers/perplexity.ts +2 -2
  140. package/src/web/search/providers/searxng.ts +1 -1
  141. package/src/web/search/providers/synthetic.ts +1 -1
  142. package/src/web/search/providers/tavily.ts +1 -1
  143. package/src/web/search/providers/utils.ts +67 -17
  144. package/src/web/search/providers/xai.ts +1 -1
  145. package/src/web/search/providers/zai.ts +1 -1
  146. package/src/workflow/workflow-intent-diff.ts +199 -0
  147. package/dist/types/discovery/vscode.d.ts +0 -1
  148. 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,
@@ -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
- return await file.text();
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}`);
@@ -347,7 +347,8 @@ export class ExtensionRunner {
347
347
  "ctrl+t",
348
348
  "ctrl+g",
349
349
  "shift+tab",
350
- "shift+ctrl+p",
350
+ "alt+n",
351
+ "alt+shift+n",
351
352
  "ctrl+enter",
352
353
  "alt+enter",
353
354
  "escape",
@@ -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
- return Boolean(parsed.continue || parsed.resume || explicitTmuxSessionName(env));
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 resolveCallerTmuxTerminalSize(tty: TtyState): TmuxTerminalSize | undefined {
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
- return { columns, rows };
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
- function resizeCreatedTmuxWindowToCallerTerminalSize(
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
- spawnSync(
571
- plan.tmuxCommand,
572
- [
573
- "resize-window",
574
- "-t",
575
- buildGjcTmuxExactOptionTarget(plan.sessionName, { env: options.env }),
576
- "-x",
577
- String(plan.initialSize.columns),
578
- "-y",
579
- String(plan.initialSize.rows),
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 initialSize = resolveCallerTmuxTerminalSize(tty);
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
- resizeCreatedTmuxWindowToCallerTerminalSize(plan, spawnSync, controlOptions);
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 = spawnSync(
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> --artifact
39
- * <path-or-string> [--run-id <id>] [--session-id <id>] [--json]` persists Planner / Architect
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
- if (rawArtifact === undefined || rawArtifact === "") {
413
- throw new RalplanCommandError(2, "--artifact is required for ralplan --write");
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 = await resolveArtifactContent(rawArtifact, cwd);
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";