@gajae-code/coding-agent 0.2.1 → 0.2.2

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 (101) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/dist/types/commands/contribution-prep.d.ts +18 -0
  3. package/dist/types/commands/session.d.ts +24 -0
  4. package/dist/types/config/model-registry.d.ts +2 -2
  5. package/dist/types/config/models-config-schema.d.ts +17 -9
  6. package/dist/types/config/settings-schema.d.ts +1 -24
  7. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +15 -0
  8. package/dist/types/gjc-runtime/goal-mode-request.d.ts +1 -1
  9. package/dist/types/gjc-runtime/launch-tmux.d.ts +12 -11
  10. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +25 -0
  11. package/dist/types/gjc-runtime/state-runtime.d.ts +13 -0
  12. package/dist/types/gjc-runtime/team-runtime.d.ts +37 -5
  13. package/dist/types/gjc-runtime/tmux-common.d.ts +41 -0
  14. package/dist/types/gjc-runtime/tmux-sessions.d.ts +17 -0
  15. package/dist/types/goals/runtime.d.ts +3 -9
  16. package/dist/types/goals/state.d.ts +3 -6
  17. package/dist/types/goals/tools/goal-tool.d.ts +1 -69
  18. package/dist/types/modes/components/status-line/types.d.ts +0 -3
  19. package/dist/types/modes/components/status-line.d.ts +0 -3
  20. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  21. package/dist/types/modes/interactive-mode.d.ts +1 -12
  22. package/dist/types/modes/theme/defaults/index.d.ts +0 -2
  23. package/dist/types/modes/theme/theme.d.ts +1 -2
  24. package/dist/types/modes/types.d.ts +1 -7
  25. package/dist/types/session/agent-session.d.ts +2 -0
  26. package/dist/types/session/contribution-prep.d.ts +47 -0
  27. package/dist/types/skill-state/active-state.d.ts +4 -0
  28. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +6 -1
  29. package/dist/types/skill-state/workflow-hud.d.ts +9 -4
  30. package/dist/types/skill-state/workflow-state-contract.d.ts +34 -0
  31. package/package.json +7 -7
  32. package/src/cli/args.ts +3 -2
  33. package/src/cli.ts +6 -1
  34. package/src/commands/contribution-prep.ts +41 -0
  35. package/src/commands/deep-interview.ts +6 -22
  36. package/src/commands/launch.ts +10 -1
  37. package/src/commands/ralplan.ts +10 -22
  38. package/src/commands/session.ts +150 -0
  39. package/src/commands/state.ts +14 -4
  40. package/src/commands/team.ts +23 -3
  41. package/src/config/model-registry.ts +10 -2
  42. package/src/config/models-config-schema.ts +120 -102
  43. package/src/config/settings-schema.ts +1 -25
  44. package/src/config.ts +1 -1
  45. package/src/defaults/gjc/skills/deep-interview/SKILL.md +14 -13
  46. package/src/defaults/gjc/skills/ralplan/SKILL.md +14 -2
  47. package/src/defaults/gjc/skills/team/SKILL.md +29 -7
  48. package/src/defaults/gjc/skills/ultragoal/SKILL.md +23 -25
  49. package/src/eval/py/prelude.py +1 -1
  50. package/src/gjc-runtime/deep-interview-runtime.ts +279 -0
  51. package/src/gjc-runtime/goal-mode-request.ts +2 -19
  52. package/src/gjc-runtime/launch-tmux.ts +83 -43
  53. package/src/gjc-runtime/ralplan-runtime.ts +460 -0
  54. package/src/gjc-runtime/state-runtime.ts +562 -0
  55. package/src/gjc-runtime/team-runtime.ts +708 -52
  56. package/src/gjc-runtime/tmux-common.ts +119 -0
  57. package/src/gjc-runtime/tmux-sessions.ts +165 -0
  58. package/src/gjc-runtime/ultragoal-guard.ts +6 -3
  59. package/src/gjc-runtime/ultragoal-runtime.ts +5 -4
  60. package/src/goals/runtime.ts +38 -144
  61. package/src/goals/state.ts +36 -7
  62. package/src/goals/tools/goal-tool.ts +15 -172
  63. package/src/hooks/skill-state.ts +31 -12
  64. package/src/internal-urls/docs-index.generated.ts +4 -3
  65. package/src/modes/components/skill-hud/render.ts +4 -0
  66. package/src/modes/components/status-line/segments.ts +5 -16
  67. package/src/modes/components/status-line/types.ts +0 -3
  68. package/src/modes/components/status-line.ts +0 -6
  69. package/src/modes/controllers/command-controller.ts +25 -1
  70. package/src/modes/controllers/input-controller.ts +0 -15
  71. package/src/modes/interactive-mode.ts +18 -219
  72. package/src/modes/theme/defaults/dark-poimandres.json +0 -1
  73. package/src/modes/theme/defaults/light-poimandres.json +0 -1
  74. package/src/modes/theme/theme.ts +0 -6
  75. package/src/modes/types.ts +1 -7
  76. package/src/prompts/goals/goal-continuation.md +1 -4
  77. package/src/prompts/goals/goal-mode-active.md +3 -5
  78. package/src/prompts/system/system-prompt.md +5 -7
  79. package/src/prompts/tools/goal.md +4 -4
  80. package/src/sdk.ts +1 -1
  81. package/src/session/agent-session.ts +18 -0
  82. package/src/session/contribution-prep.ts +320 -0
  83. package/src/skill-state/active-state.ts +38 -0
  84. package/src/skill-state/deep-interview-mutation-guard.ts +88 -24
  85. package/src/skill-state/workflow-hud.ts +23 -5
  86. package/src/skill-state/workflow-state-contract.ts +121 -0
  87. package/src/slash-commands/builtin-registry.ts +24 -12
  88. package/src/task/commands.ts +1 -5
  89. package/src/tools/gh.ts +212 -2
  90. package/src/tools/index.ts +2 -5
  91. package/dist/types/commands/gjc-runtime-bridge.d.ts +0 -30
  92. package/dist/types/commands/question.d.ts +0 -7
  93. package/dist/types/modes/loop-limit.d.ts +0 -22
  94. package/src/commands/gjc-runtime-bridge.ts +0 -227
  95. package/src/commands/question.ts +0 -12
  96. package/src/modes/loop-limit.ts +0 -140
  97. package/src/prompts/commands/orchestrate.md +0 -49
  98. package/src/prompts/goals/goal-budget-limit.md +0 -16
  99. package/src/prompts/tools/create-goal.md +0 -3
  100. package/src/prompts/tools/get-goal.md +0 -3
  101. package/src/prompts/tools/update-goal.md +0 -3
@@ -0,0 +1,279 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { syncSkillActiveState } from "../skill-state/active-state";
5
+ import { buildDeepInterviewHudSummary } from "../skill-state/workflow-hud";
6
+
7
+ /**
8
+ * Native implementation of `gjc deep-interview`.
9
+ *
10
+ * The CLI itself does not run the Socratic interview; that lives inside the `/skill:deep-interview`
11
+ * skill executed by the agent. This handler validates the documented argument-hint surface
12
+ * (`[--quick|--standard|--deep] <idea>`), seeds `.gjc/state/deep-interview-state.json`, and
13
+ * updates the shared HUD rail via `syncSkillActiveState` so the active interview is visible to
14
+ * the TUI.
15
+ */
16
+
17
+ export interface DeepInterviewCommandResult {
18
+ status: number;
19
+ stdout?: string;
20
+ stderr?: string;
21
+ }
22
+
23
+ const PATH_COMPONENT_RE = /^[A-Za-z0-9_-][A-Za-z0-9._-]{0,63}$/;
24
+
25
+ const DEFAULT_AMBIGUITY_THRESHOLD = 0.05;
26
+
27
+ const RESOLUTION_THRESHOLDS = {
28
+ quick: 0.6,
29
+ standard: 0.5,
30
+ deep: 0.35,
31
+ } as const;
32
+
33
+ type DeepInterviewResolution = keyof typeof RESOLUTION_THRESHOLDS;
34
+
35
+ class DeepInterviewCommandError extends Error {
36
+ constructor(
37
+ public readonly exitStatus: number,
38
+ message: string,
39
+ ) {
40
+ super(message);
41
+ this.name = "DeepInterviewCommandError";
42
+ }
43
+ }
44
+
45
+ const VALUE_FLAGS = new Set(["--session-id", "--threshold", "--threshold-source"]);
46
+
47
+ function flagValue(args: readonly string[], flag: string): string | undefined {
48
+ const index = args.indexOf(flag);
49
+ if (index < 0) return undefined;
50
+ return args[index + 1];
51
+ }
52
+
53
+ function hasFlag(args: readonly string[], flag: string): boolean {
54
+ return args.includes(flag);
55
+ }
56
+
57
+ function assertSafePathComponent(value: string, label: string): void {
58
+ if (!PATH_COMPONENT_RE.test(value) || value.includes("..")) {
59
+ throw new DeepInterviewCommandError(2, `invalid path component for --${label}: ${value}`);
60
+ }
61
+ }
62
+
63
+ function encodeSessionSegment(value: string): string {
64
+ return encodeURIComponent(value).replaceAll(".", "%2E");
65
+ }
66
+
67
+ interface ResolvedDeepInterviewArgs {
68
+ resolution: DeepInterviewResolution;
69
+ threshold: number;
70
+ thresholdSource: string;
71
+ sessionId?: string;
72
+ idea: string;
73
+ json: boolean;
74
+ }
75
+
76
+ async function readSettingsAmbiguityThreshold(
77
+ settingsPath: string,
78
+ ): Promise<{ threshold: number; source: string } | undefined> {
79
+ let raw: string;
80
+ try {
81
+ raw = await fs.readFile(settingsPath, "utf-8");
82
+ } catch (error) {
83
+ const err = error as NodeJS.ErrnoException;
84
+ if (err.code === "ENOENT") return undefined;
85
+ return undefined;
86
+ }
87
+ let parsed: unknown;
88
+ try {
89
+ parsed = JSON.parse(raw);
90
+ } catch {
91
+ return undefined;
92
+ }
93
+ const candidate = (parsed as { gjc?: { deepInterview?: { ambiguityThreshold?: unknown } } })?.gjc?.deepInterview
94
+ ?.ambiguityThreshold;
95
+ if (typeof candidate !== "number" || !Number.isFinite(candidate) || candidate <= 0 || candidate > 1) {
96
+ return undefined;
97
+ }
98
+ return { threshold: candidate, source: settingsPath };
99
+ }
100
+
101
+ async function resolveConfiguredAmbiguityThreshold(
102
+ cwd: string,
103
+ ): Promise<{ threshold: number; source: string } | undefined> {
104
+ const projectSettings = path.join(cwd, ".gjc", "settings.json");
105
+ const projectValue = await readSettingsAmbiguityThreshold(projectSettings);
106
+ if (projectValue) return projectValue;
107
+ const configDir = process.env.GJC_CONFIG_DIR?.trim() || path.join(os.homedir(), ".gjc");
108
+ const userSettings = path.join(configDir, "settings.json");
109
+ return await readSettingsAmbiguityThreshold(userSettings);
110
+ }
111
+
112
+ async function resolveDeepInterviewArgs(args: readonly string[], cwd: string): Promise<ResolvedDeepInterviewArgs> {
113
+ const sessionId = flagValue(args, "--session-id")?.trim() || undefined;
114
+ if (sessionId) assertSafePathComponent(sessionId, "session-id");
115
+
116
+ const explicitResolutions = (["quick", "standard", "deep"] as const).filter(name => hasFlag(args, `--${name}`));
117
+ if (explicitResolutions.length > 1) {
118
+ throw new DeepInterviewCommandError(2, "pass at most one of --quick, --standard, --deep");
119
+ }
120
+ const resolution: DeepInterviewResolution | undefined = explicitResolutions[0];
121
+
122
+ // Precedence: --threshold > settings.json (project then user) > resolution flag default > 0.05.
123
+ let threshold: number = DEFAULT_AMBIGUITY_THRESHOLD;
124
+ let thresholdSource = "default";
125
+ const thresholdOverride = flagValue(args, "--threshold");
126
+ if (thresholdOverride !== undefined) {
127
+ const parsed = Number(thresholdOverride);
128
+ if (!Number.isFinite(parsed) || parsed <= 0 || parsed > 1) {
129
+ throw new DeepInterviewCommandError(
130
+ 2,
131
+ `invalid --threshold: ${thresholdOverride}. Expected 0 < threshold <= 1.`,
132
+ );
133
+ }
134
+ threshold = parsed;
135
+ thresholdSource = flagValue(args, "--threshold-source")?.trim() || "flag:--threshold";
136
+ } else {
137
+ const configured = await resolveConfiguredAmbiguityThreshold(cwd);
138
+ if (configured) {
139
+ threshold = configured.threshold;
140
+ thresholdSource = configured.source;
141
+ } else if (resolution) {
142
+ threshold = RESOLUTION_THRESHOLDS[resolution];
143
+ thresholdSource = `flag:--${resolution}`;
144
+ }
145
+ }
146
+
147
+ const ideaParts: string[] = [];
148
+ let skipNext = false;
149
+ for (const arg of args) {
150
+ if (skipNext) {
151
+ skipNext = false;
152
+ continue;
153
+ }
154
+ if (VALUE_FLAGS.has(arg)) {
155
+ skipNext = true;
156
+ continue;
157
+ }
158
+ if (arg === "--quick" || arg === "--standard" || arg === "--deep" || arg === "--json") continue;
159
+ if (arg.startsWith("-")) {
160
+ throw new DeepInterviewCommandError(2, `unknown flag for gjc deep-interview: ${arg}`);
161
+ }
162
+ ideaParts.push(arg);
163
+ }
164
+ const idea = ideaParts.join(" ").trim();
165
+ const effectiveResolution: DeepInterviewResolution = resolution ?? "standard";
166
+ return {
167
+ resolution: effectiveResolution,
168
+ threshold,
169
+ thresholdSource,
170
+ sessionId,
171
+ idea,
172
+ json: hasFlag(args, "--json"),
173
+ };
174
+ }
175
+
176
+ async function seedDeepInterviewState(cwd: string, resolved: ResolvedDeepInterviewArgs): Promise<string> {
177
+ const stateDir = resolved.sessionId
178
+ ? path.join(cwd, ".gjc", "state", "sessions", encodeSessionSegment(resolved.sessionId))
179
+ : path.join(cwd, ".gjc", "state");
180
+ await fs.mkdir(stateDir, { recursive: true });
181
+ const statePath = path.join(stateDir, "deep-interview-state.json");
182
+ const now = new Date().toISOString();
183
+ const payload: Record<string, unknown> = {
184
+ active: true,
185
+ current_phase: "interviewing",
186
+ skill: "deep-interview",
187
+ resolution: resolved.resolution,
188
+ threshold: resolved.threshold,
189
+ threshold_source: resolved.thresholdSource,
190
+ state: {
191
+ initial_idea: resolved.idea,
192
+ rounds: [],
193
+ current_ambiguity: 1.0,
194
+ threshold: resolved.threshold,
195
+ threshold_source: resolved.thresholdSource,
196
+ },
197
+ updated_at: now,
198
+ };
199
+ if (resolved.sessionId) payload.session_id = resolved.sessionId;
200
+ await fs.writeFile(statePath, `${JSON.stringify(payload, null, 2)}\n`);
201
+ return statePath;
202
+ }
203
+
204
+ async function syncDeepInterviewHud(options: {
205
+ cwd: string;
206
+ sessionId?: string;
207
+ phase: string;
208
+ ambiguity?: number;
209
+ threshold?: number;
210
+ roundCount?: number;
211
+ specStatus?: string;
212
+ }): Promise<void> {
213
+ try {
214
+ await syncSkillActiveState({
215
+ cwd: options.cwd,
216
+ skill: "deep-interview",
217
+ active: options.phase !== "complete",
218
+ phase: options.phase,
219
+ sessionId: options.sessionId,
220
+ source: "gjc-deep-interview-native",
221
+ hud: buildDeepInterviewHudSummary({
222
+ phase: options.phase,
223
+ ambiguity: options.ambiguity,
224
+ threshold: options.threshold,
225
+ roundCount: options.roundCount,
226
+ specStatus: options.specStatus,
227
+ updatedAt: new Date().toISOString(),
228
+ }),
229
+ });
230
+ } catch {
231
+ // HUD sync is best-effort and must not change command semantics.
232
+ }
233
+ }
234
+
235
+ export async function runNativeDeepInterviewCommand(
236
+ args: string[],
237
+ cwd = process.cwd(),
238
+ ): Promise<DeepInterviewCommandResult> {
239
+ try {
240
+ const resolved = await resolveDeepInterviewArgs(args, cwd);
241
+ if (!resolved.idea) {
242
+ throw new DeepInterviewCommandError(
243
+ 2,
244
+ 'gjc deep-interview requires an idea, e.g. `gjc deep-interview "<idea>"`.',
245
+ );
246
+ }
247
+ const statePath = await seedDeepInterviewState(cwd, resolved);
248
+ await syncDeepInterviewHud({
249
+ cwd,
250
+ sessionId: resolved.sessionId,
251
+ phase: "interviewing",
252
+ ambiguity: 1,
253
+ threshold: resolved.threshold,
254
+ roundCount: 0,
255
+ });
256
+
257
+ const summary = {
258
+ skill: "deep-interview",
259
+ resolution: resolved.resolution,
260
+ threshold: resolved.threshold,
261
+ threshold_source: resolved.thresholdSource,
262
+ idea: resolved.idea,
263
+ state_path: statePath,
264
+ handoff: "Run `/skill:deep-interview` inside the GJC agent to drive the Socratic interview loop.",
265
+ };
266
+ const stdout = resolved.json
267
+ ? `${JSON.stringify(summary, null, 2)}\n`
268
+ : [
269
+ `Seeded deep-interview ${resolved.resolution} run at ${statePath}.`,
270
+ `Threshold: ${(resolved.threshold * 100).toFixed(0)}% (source: ${resolved.thresholdSource}).`,
271
+ "Run `/skill:deep-interview` inside the GJC agent to execute the interview.",
272
+ "",
273
+ ].join("\n");
274
+ return { status: 0, stdout };
275
+ } catch (error) {
276
+ if (error instanceof DeepInterviewCommandError) return { status: error.exitStatus, stderr: `${error.message}\n` };
277
+ return { status: 1, stderr: `${error instanceof Error ? error.message : String(error)}\n` };
278
+ }
279
+ }
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import { Snowflake } from "@gajae-code/utils";
4
- import type { Goal, GoalModeState } from "../goals/state";
4
+ import { type Goal, type GoalModeState, normalizeGoal } from "../goals/state";
5
5
  import {
6
6
  buildSessionContext,
7
7
  loadEntriesFromFile,
@@ -94,24 +94,7 @@ export async function writePendingGoalModeRequest(input: {
94
94
  }
95
95
 
96
96
  function goalFromModeData(modeData: Record<string, unknown> | undefined): Goal | null {
97
- const candidate = modeData?.goal;
98
- if (typeof candidate !== "object" || candidate === null) return null;
99
- const goal = candidate as Partial<Goal>;
100
- if (
101
- typeof goal.id !== "string" ||
102
- typeof goal.objective !== "string" ||
103
- typeof goal.status !== "string" ||
104
- typeof goal.tokensUsed !== "number" ||
105
- typeof goal.timeUsedSeconds !== "number" ||
106
- typeof goal.createdAt !== "number" ||
107
- typeof goal.updatedAt !== "number"
108
- ) {
109
- return null;
110
- }
111
- if (!["active", "paused", "budget-limited", "complete", "dropped"].includes(goal.status)) {
112
- return null;
113
- }
114
- return goal as Goal;
97
+ return normalizeGoal(modeData?.goal);
115
98
  }
116
99
 
117
100
  function isNonTerminalGoal(goal: Goal | null): goal is Goal {
@@ -1,12 +1,30 @@
1
1
  import * as path from "node:path";
2
2
  import type { Args } from "../cli/args";
3
+ import {
4
+ buildGjcTmuxProfileCommands,
5
+ buildGjcTmuxSessionName,
6
+ buildGjcTmuxSessionSlug,
7
+ GJC_DEFAULT_TMUX_SESSION,
8
+ GJC_TMUX_COMMAND_ENV,
9
+ GJC_TMUX_MOUSE_ENV,
10
+ GJC_TMUX_PROFILE_ENV,
11
+ GJC_TMUX_SESSION_PREFIX,
12
+ type GjcTmuxProfileCommand,
13
+ resolveGjcTmuxCommand,
14
+ } from "./tmux-common";
15
+ import { findGjcTmuxSessionByBranch } from "./tmux-sessions";
16
+
17
+ export {
18
+ buildGjcTmuxProfileCommands,
19
+ GJC_DEFAULT_TMUX_SESSION,
20
+ GJC_TMUX_COMMAND_ENV,
21
+ GJC_TMUX_MOUSE_ENV,
22
+ GJC_TMUX_PROFILE_ENV,
23
+ GJC_TMUX_SESSION_PREFIX,
24
+ };
3
25
 
4
- export const GJC_DEFAULT_TMUX_SESSION = "gajae_code";
5
26
  export const GJC_TMUX_LAUNCHED_ENV = "GJC_TMUX_LAUNCHED";
6
27
  export const GJC_LAUNCH_POLICY_ENV = "GJC_LAUNCH_POLICY";
7
- export const GJC_TMUX_COMMAND_ENV = "GJC_TMUX_COMMAND";
8
- export const GJC_TMUX_PROFILE_ENV = "GJC_TMUX_PROFILE";
9
- export const GJC_TMUX_MOUSE_ENV = "GJC_MOUSE";
10
28
 
11
29
  type LaunchPolicy = "direct" | "tmux";
12
30
 
@@ -26,6 +44,10 @@ export interface TmuxLaunchContext {
26
44
  tty?: TtyState;
27
45
  spawnSync?: TmuxSpawnSync;
28
46
  tmuxAvailable?: boolean;
47
+ worktreeBranch?: string | null;
48
+ currentBranch?: string | null;
49
+ existingBranchSessionName?: string | null;
50
+ project?: string | null;
29
51
  }
30
52
 
31
53
  export interface TmuxSpawnResult {
@@ -50,12 +72,9 @@ export interface TmuxLaunchPlan {
50
72
  cwd: string;
51
73
  innerCommand: string;
52
74
  newSessionArgs: string[];
53
- attachSessionArgs: string[];
54
- }
55
-
56
- export interface GjcTmuxProfileCommand {
57
- description: string;
58
- args: string[];
75
+ branch?: string | null;
76
+ attachSessionName?: string;
77
+ project?: string | null;
59
78
  }
60
79
 
61
80
  export interface GjcTmuxProfileResult {
@@ -70,6 +89,9 @@ export interface GjcTmuxProfileContext {
70
89
  cwd?: string;
71
90
  env?: NodeJS.ProcessEnv;
72
91
  spawnSync?: TmuxSpawnSync;
92
+ branch?: string | null;
93
+ branchSlug?: string | null;
94
+ project?: string | null;
73
95
  }
74
96
 
75
97
  interface CommandResolutionContext {
@@ -103,35 +125,14 @@ function shellQuote(value: string): string {
103
125
  return `'${value.replace(/'/g, `'\\''`)}'`;
104
126
  }
105
127
 
106
- function envDisabled(value: string | undefined): boolean {
107
- const normalized = value?.trim().toLowerCase();
108
- return normalized === "0" || normalized === "false" || normalized === "off" || normalized === "no";
109
- }
110
-
111
- export function buildGjcTmuxProfileCommands(
112
- target: string,
113
- env: NodeJS.ProcessEnv = process.env,
114
- ): GjcTmuxProfileCommand[] {
115
- if (envDisabled(env[GJC_TMUX_PROFILE_ENV])) return [];
116
- const commands: GjcTmuxProfileCommand[] = [
117
- { description: "mark GJC tmux ownership", args: ["set-option", "-t", target, "@gjc-profile", "1"] },
118
- { description: "enable tmux clipboard integration", args: ["set-option", "-t", target, "set-clipboard", "on"] },
119
- {
120
- description: "make copy-mode selection readable",
121
- args: ["set-window-option", "-t", target, "mode-style", "fg=colour231,bg=colour60"],
122
- },
123
- ];
124
- if (!envDisabled(env[GJC_TMUX_MOUSE_ENV]))
125
- commands.unshift({
126
- description: "enable tmux mouse scrolling",
127
- args: ["set-option", "-t", target, "mouse", "on"],
128
- });
129
- return commands;
130
- }
131
-
132
128
  export function applyGjcTmuxProfile(context: GjcTmuxProfileContext): GjcTmuxProfileResult {
133
129
  const env = context.env ?? process.env;
134
- const commands = buildGjcTmuxProfileCommands(context.target, env);
130
+ const branchSlug = context.branch ? buildGjcTmuxSessionSlug(context.branch) : (context.branchSlug ?? null);
131
+ const commands = buildGjcTmuxProfileCommands(context.target, env, {
132
+ branch: context.branch ?? null,
133
+ branchSlug,
134
+ project: context.project ?? null,
135
+ });
135
136
  if (commands.length === 0) return { skipped: true, commands: [], failures: [] };
136
137
  const spawnSync = context.spawnSync ?? defaultSpawnSync;
137
138
  const cwd = context.cwd ?? process.cwd();
@@ -160,6 +161,25 @@ function buildInnerCommand(context: CommandResolutionContext, rawArgs: string[])
160
161
  return `exec env ${GJC_TMUX_LAUNCHED_ENV}=1 ${quoted}`;
161
162
  }
162
163
 
164
+ function readCurrentBranch(cwd: string): string | null {
165
+ try {
166
+ const result = Bun.spawnSync(["git", "symbolic-ref", "--quiet", "--short", "HEAD"], {
167
+ cwd,
168
+ stdout: "pipe",
169
+ stderr: "ignore",
170
+ });
171
+ if (result.exitCode !== 0) return null;
172
+ const branch = result.stdout.toString().trim();
173
+ return branch || null;
174
+ } catch {
175
+ return null;
176
+ }
177
+ }
178
+
179
+ function cleanupCreatedTmuxSession(plan: TmuxLaunchPlan, spawnSync: TmuxSpawnSync, options: TmuxSpawnOptions): void {
180
+ spawnSync(plan.tmuxCommand, ["kill-session", "-t", `=${plan.sessionName}`], options);
181
+ }
182
+
163
183
  export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaunchPlan | undefined {
164
184
  const env = context.env ?? process.env;
165
185
  const policy = parseLaunchPolicy(env);
@@ -171,10 +191,18 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
171
191
  if (policy === "tmux" && !isInteractiveRootLaunch(context.parsed, tty)) return undefined;
172
192
 
173
193
  const cwd = context.cwd ?? process.cwd();
174
- const sessionName = env.GJC_TMUX_SESSION?.trim() || GJC_DEFAULT_TMUX_SESSION;
175
- const tmuxCommand = env[GJC_TMUX_COMMAND_ENV]?.trim() || "tmux";
194
+ const branch = context.worktreeBranch ?? context.currentBranch ?? readCurrentBranch(cwd);
195
+ const project = context.project ?? cwd;
196
+ const sessionName = buildGjcTmuxSessionName(env, { branch });
197
+ const tmuxCommand = resolveGjcTmuxCommand(env);
176
198
  const tmuxAvailable = context.tmuxAvailable ?? Bun.which(tmuxCommand) !== null;
177
199
  if (!tmuxAvailable) return undefined;
200
+ const existingBranchSessionName =
201
+ "existingBranchSessionName" in context
202
+ ? (context.existingBranchSessionName ?? undefined)
203
+ : context.worktreeBranch
204
+ ? findGjcTmuxSessionByBranch(context.worktreeBranch, env, project)?.name
205
+ : undefined;
178
206
  const innerCommand = buildInnerCommand(
179
207
  {
180
208
  cwd,
@@ -189,7 +217,9 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
189
217
  cwd,
190
218
  innerCommand,
191
219
  newSessionArgs: ["new-session", "-d", "-s", sessionName, "-c", cwd, innerCommand],
192
- attachSessionArgs: ["attach-session", "-t", sessionName],
220
+ branch,
221
+ project,
222
+ attachSessionName: existingBranchSessionName,
193
223
  };
194
224
  }
195
225
 
@@ -217,17 +247,27 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
217
247
  stdout: "inherit",
218
248
  stderr: "inherit",
219
249
  };
250
+ if (plan.attachSessionName) {
251
+ const attached = spawnSync(plan.tmuxCommand, ["attach-session", "-t", `=${plan.attachSessionName}`], options);
252
+ return attached.exitCode === 0;
253
+ }
220
254
  const created = spawnSync(plan.tmuxCommand, plan.newSessionArgs, options);
221
255
  if (created.exitCode === 0) {
222
- applyGjcTmuxProfile({
256
+ const profile = applyGjcTmuxProfile({
223
257
  tmuxCommand: plan.tmuxCommand,
224
258
  target: plan.sessionName,
225
259
  cwd: plan.cwd,
226
260
  env,
227
261
  spawnSync,
262
+ branch: plan.branch,
263
+ project: plan.project,
228
264
  });
265
+ if (profile.failures.length > 0) {
266
+ cleanupCreatedTmuxSession(plan, spawnSync, options);
267
+ return false;
268
+ }
229
269
  }
230
- const attached = spawnSync(plan.tmuxCommand, plan.attachSessionArgs, options);
231
- if (created.exitCode === 0) return attached.exitCode === 0;
270
+ if (created.exitCode !== 0) return false;
271
+ const attached = spawnSync(plan.tmuxCommand, ["attach-session", "-t", plan.sessionName], options);
232
272
  return attached.exitCode === 0;
233
273
  }